# MDK Docs (/) } title={Learn what MDK is} href="/concepts" description={ Product overview, architecture, and the MDK packages } /> } title={Try the demo} href="/tutorials/run-a-site" description={ Mock hardware, a Gateway API, and a live dashboard with one command } /> } title={Install the CLI} href="/guides/cli" description={ Scaffold, run, and manage Bitcoin mining operations from your terminal } /> } title={Agent skills} href="/guides/ui/agent-skills" description={ Teach your agent MDK conventions and component props } /> } title={Full docs in one file} href="/llms-full.txt" description={ Every page of this site in one plain-text file—open in the browser to copy or save } /> } title={Monorepo docs} href="https://github.com/tetherto/mdk/blob/main/docs/README.md" description={ The source docs in the public MDK monorepo } /> # 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 conceptual layers, each containing one or more 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**. Browsers reach the Kernel through the Gateway, the consumer-facing integration boundary your team builds with the SDK; [AI agents](#ai-ready-with-unified-intelligence) reach it instead through the standalone MCP server. MDK ships no authentication of its own at any tier: your team supplies it in the Gateway plugin controllers you write (native MCP tools authenticate at the MCP endpoint instead, not through those controllers). Tying everything together is a **single contract per device type**: the same [`mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/workers/README.md#3-mdk-contractjson) is designed to serve the UI (data labels), the orchestrator (validation rules), and AI agents (reasoning context) — today, only the orchestrator's validation rules are actually wired to it; UI and agent tooling don't read it yet. One file, one intended source of truth for three audiences. ### The orchestration kernel [Kernel](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md), 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 Workers, never devices directly, through a standardized language called the **MDK Protocol**, a common set of messages every Worker in the system understands, regardless of the device manufacturer or model behind it. 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. - **Node.js today**: `@tetherto/mdk-client` ships as a Node.js package. The transport and protocol are designed to allow future clients in other languages (Python, Go, and others) without changes to Kernel or the protocol itself - **Lazy connection handling**: connects on first use and reconnects on the next call after a failure, with bounded retries on `getStatus()` and an optional connect warm-up. There is no background reconnect loop, and HRPC is the only transport - **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**](https://github.com/tetherto/mdk/blob/main/docs/concepts/app-toolkit.md) 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/ui)), framework adapters ([`@tetherto/mdk-react-adapter`](https://github.com/tetherto/mdk/blob/main/ui/README.md) for React today), and a production-tested React UI Kit ([`@tetherto/mdk-react-devkit`](https://github.com/tetherto/mdk/blob/main/ui/README.md)) for dashboards. - **Backend tools**: the Gateway itself, a Fastify-specific library handling command proxying and request-level caching, with hooks for custom routes and aggregations. There is no Express adapter today. - **Plugins**: a Gateway plugin's `mdk-plugin.json` declares its own routes; pairing one with a specific frontend tools widget is application code you write, not a manifest mechanism the Toolkit provides for you today. Third parties can ship whole features without forking the Gateway. The Gateway is the recommended integration path for applications — it adds HTTP routing, request caching, and a plugin system for extending routes. Nothing above the Gateway is required, though: for lightweight tools, standalone scripts, or CI/CD integrations, [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) can talk to Kernel directly. Some Worker-scoped operations (device provisioning, historical log/stat aggregation) go through a direct Worker connection instead of the usual Kernel round-trip; [when to use which](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md#when-to-use-which-client-path) is covered by the client's own guide. ## 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`](https://github.com/tetherto/mdk/blob/main/backend/workers/README.md#3-mdk-contractjson). No involvement from MDK maintainers needed. - **Software developers**: build custom mining applications in any language, or leverage the [MDK App Toolkit](https://github.com/tetherto/mdk/blob/main/docs/concepts/app-toolkit.md)'s frontend and backend tools for rapid development. - **AI/Automation teams**: [connect intelligent agents](#ai-ready-with-unified-intelligence) that can monitor and diagnose device issues autonomously, then act on them once an operator approves the write ## 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](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md) hosts plugins and adds request-level caching and an HTTP interface; each plugin builds its own `@tetherto/mdk-client` and does its own fleet aggregation. Authenticating callers is left to the plugin controllers you write. AI agents can drive the fleet over MCP — a standalone [`@tetherto/mdk-mcp`](https://github.com/tetherto/mdk/blob/main/backend/core/mcp/README.md) process that derives tools from a plugin's routes. - **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/ui), 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. Each layer names its canonical doc in [the MDK stack](/concepts/architecture#the-stack). The [round trip](/concepts/architecture#the-round-trip) traces one command end to end, and the [Workers discovery model](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/architecture.md#discovery-model) covers how Kernel finds Workers across local, same-process, and DHT modes. ## AI-ready with unified intelligence MDK is designed from the ground up for [AI-driven operations](https://github.com/tetherto/mdk/blob/main/backend/core/mcp/README.md). 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`](https://github.com/tetherto/mdk/blob/main/backend/workers/README.md#3-mdk-contractjson)) 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 an AI agent can diagnose against autonomously; the recovery action itself still waits for operator approval, the same as any other write The intent is that an AI agent connecting to MDK wouldn't need a separate knowledge base or custom prompts per device: the same contract that Kernel already validates commands against would also determine how AI reasons about that hardware. That wiring is not built yet: MCP tools today come from a hand-authored manifest or a Gateway plugin's routes, not from a Worker's contract (see [Connecting intelligent agents](https://github.com/tetherto/mdk/blob/main/backend/core/mcp/README.md)). ## 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, with human-approved control actions - Custom analytics and reporting pipelines - White-labeled hosted mining platforms - Third-party device integrations and plugins ## Scaling MDK [scales](/concepts/scalability) 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, each behind its own Gateway. No MDK component aggregates across sites: that view is your own application code calling each site's Gateway and merging the results. - **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](https://github.com/tetherto/mdk/blob/main/docs/concepts/app-toolkit.md) - [Connecting intelligent agents](https://github.com/tetherto/mdk/blob/main/backend/core/mcp/README.md) # Architecture (/concepts/architecture) ## How MDK works MDK is built around a three tier ownership model: - **Kernel is invariant** The Kernel provides small coordination layer every deployment runs unchanged: it routes validated commands to whichever Worker owns a device, and pulls telemetry back. - **Extensions are yours** [Worker plugins](/reference/worker) wrap a device family; [Gateway plugins](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md) add HTTP routes, aggregation, and auth. Both are code you write and own, isolated from the Kernel and from each other. Nothing above the Gateway is required: a deployment can dispatch commands and pull telemetry with just [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md). - **The UI devkit is optional** The [MDK App Toolkit](https://github.com/tetherto/mdk/blob/main/docs/concepts/app-toolkit.md) is the supported path for teams that want one. ## The round trip ```mermaid flowchart LR subgraph consumers [Consumers] Agent["AI Agent"] Dashboard["Dashboard"] end subgraph gw ["Gateway"] GP["Gateway plugin\n(extension point)"] end MCP["MCP server\n(@tetherto/mdk-mcp)"] K["Kernel"] subgraph wk ["Worker"] WP["Worker plugin\n(extension point)"] end Devices[("Devices")] Agent -->|"MCP"| MCP Dashboard -->|"HTTP"| GP GP <-->|"HRPC"| K MCP <-->|"HRPC"| K K <-->|"HRPC"| WP WP --> Devices style gw fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A style wk fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A ``` One request, traced end to end: an AI agent or a dashboard reaches a Gateway plugin's capability. A dashboard calls the plugin's HTTP route directly; an [AI agent](https://github.com/tetherto/mdk/blob/main/backend/core/mcp/README.md#ai-agents-and-the-mcp-server) arrives instead through an MCP endpoint — a standalone [`@tetherto/mdk-mcp`](https://github.com/tetherto/mdk/blob/main/backend/core/mcp/README.md) process that derives tools from that plugin's routes (the Gateway doesn't host the MCP itself). Either way the plugin builds its own [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) and dispatches a command through it. [Kernel](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md) resolves which Worker owns the target device, forwards the command over the Worker's own connection, and relays the result back through the same path: Gateway plugin, then caller. Telemetry travels the same round trip in reverse, on demand: the Gateway plugin's client asks Kernel for a device's telemetry, Kernel forwards that pull to the owning Worker and relays the answer straight back. Kernel also runs its own scheduled telemetry/health pulls on a fixed cadence, independent of any caller: the two are separate triggers into the same path, not one waiting on the other. Both extension points sit at the edges of this trip, never in the middle: a **Worker contract** teaches Kernel about one device family (the contract declares the capability surface; the plugin's handlers implement it), and a **Gateway plugin** teaches the Gateway a new route. Kernel itself never changes. ## The three tiers **[Gateway](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md)**: a container that hosts plugins and exposes them over HTTP. It is the active side of the Kernel connection (it dials Kernel, never the reverse). This is the tier where *user*-level authentication, aggregation, and business logic live. Aggregation here means the cross-Worker queries no single Worker can answer — site hashrate, average temperature, cross-rack efficiency — resolved in controller code, since Kernel computes none of them. Kernel's own allowlist, when configured, gates which *connections* it accepts: a [transport-level check, not a user identity](/concepts/security-boundaries). **[Workers](/reference/worker)**: the integration handlers between physical hardware and Kernel, and the source of truth for that hardware's state. A Worker answers only when Kernel asks (identity, capabilities, telemetry, or a command) and never calls Kernel unprompted. **[Kernel](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md)**: the passive coordination layer. It never initiates contact with a caller; it discovers Workers, routes commands to the one that owns a device, and pulls telemetry and health on its own cadence. It performs no aggregation and stores no telemetry itself. ## Why HRPC? Every hop above (Gateway to Kernel, Kernel to Worker) speaks [Hyperswarm RPC (HRPC)](/reference/glossary#hyperswarm-rpc): an encrypted, key-addressed peer-to-peer transport, not HTTP. A site network connects a fixed, known set of processes to each other, not the open web; HRPC's key-based addressing means a Worker or Gateway is reachable the same way whether it sits on the same host or across a DHT, with no separate TLS/cert story and no public-facing port to secure. The trade-off is a caller must hold or discover the callee's public key before it can connect: there is no URL to type into a browser. In practice: a caller sends one request and receives one response over that channel; a dropped connection is the client's problem to recover from: the next call through [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) reconnects; Kernel does not buffer or replay what it couldn't deliver while a link was down. ## What's authoritative, and what's cached Kernel's own store holds only its Worker registry, device capabilities, and the write-command log: never telemetry. Telemetry is Worker-owned: a Worker persists its own device history, and every telemetry read anywhere above it (Gateway plugin, dashboard, agent) is a live pull through that chain, not a read from a Kernel-side cache. ## The stack Discover each layer's canonical docs: | Layer | Package | Canonical doc | |---|---|---| | Workers | `@tetherto/mdk-worker-*`, one per vendor | [Device protocol adapters](/reference/worker) | | Worker Runtime | `@tetherto/mdk-worker` | [Worker runtime](https://github.com/tetherto/mdk/blob/main/backend/core/docs/README.md#worker-runtime-tethertomdk-worker) | | Kernel | `@tetherto/mdk-kernel` | [Coordination kernel](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md) | | Client SDK | `@tetherto/mdk-client` | [Protocol connector](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) | | Gateway | `@tetherto/mdk-gateway` | [Plugin host and HTTP surface](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md) | | Gateway plugins | `@tetherto/mdk-plugins` | [Bundled plugins and the manifest format](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md) | | MCP server | `@tetherto/mdk-mcp` | [Tools for AI agents](https://github.com/tetherto/mdk/blob/main/backend/core/mcp/README.md) | | App Toolkit | Frontend packages | [The supported development path](https://github.com/tetherto/mdk/blob/main/docs/concepts/app-toolkit.md) | For the per-package detail in each workspace, read the [core package index](https://github.com/tetherto/mdk/blob/main/backend/core/docs/README.md) and the [UI toolkit index](https://github.com/tetherto/mdk/blob/main/ui/README.md). How many of each a deployment runs is covered by [scalability](/concepts/scalability). ## Next steps - Understand [the integration model](/concepts/the-integration-model): what a Worker plugin and a Gateway plugin each get to do - Understand [the storage model](/concepts/the-storage-model): where state actually lives as you scale - Understand the [security boundaries](/concepts/security-boundaries): what each tier does and does not authenticate, and what you must build - Understand [what an app is](/concepts/whats-an-app) in MDK terms - Understand [scalability](/concepts/scalability): parallel Workers, parallel Kernels, and what's measured today - Understand the [MDK App Toolkit](https://github.com/tetherto/mdk/blob/main/docs/concepts/app-toolkit.md): the recommended development path from Gateway backend to frontend packages - [Connect an AI agent over MCP](https://github.com/tetherto/mdk/blob/main/backend/core/mcp/README.md#ai-agents-and-the-mcp-server): the endpoint agents reach the fleet through, and the security envelope you supply # Scalability (/concepts/scalability) ## Overview Scale refers to *how many* Workers and Kernels a deployment runs, this includes: - **Devices per Worker instance**: how many devices one running Worker manages. Bounded by the device protocol and the Worker's own connection model, not by Kernel - **Worker instances per Kernel**: how many Worker processes one Kernel coordinates. Kernel places no hard cap; the practical limit is how much command/telemetry traffic one Kernel process can route - **Kernels per Gateway**: today, one. `startGateway()` connects to exactly one Kernel (`kernelKey` is a single value), and `mdk.yaml` declares exactly one Kernel per stack ## Single-kernel versus multi-kernel This page is not about *how those processes are packaged* on a host (one process versus many machines). That's an independent [deployment topology](/guides/deployment) choice. The topology distinction this page does own is, does your deployment run: - One Kernel serving a site? - Several independent Kernels, each serving its own site? A single Kernel process routes commands and telemetry for every Worker registered to it, with no per-Worker partitioning. Each Kernel is paired with its own Gateway (`startGateway()` connects to exactly one Kernel, and `mdk.yaml` declares exactly one Kernel per stack). Add a second Kernel when you're adding a second physically or organizationally distinct site, not to work around a single site's device count. You can run multiple independent sites with one Kernel per physical site (for example, Site A and Site B). Each Kernel is fully isolated: Kernel instances do not federate registries, share queues, or synchronize state with each other, and each runs behind its own Gateway. ```mermaid flowchart TD App["Your application code"] subgraph site_a ["Site A"] GW_A["Gateway"] KERNEL_A["Kernel"] W1_A["Whatsminer Worker"] W2_A["Antminer Worker"] D1_A["Whatsminers"] D2_A["Antminers"] GW_A -->|MDK Protocol via HRPC| KERNEL_A KERNEL_A -->|Routes| W1_A KERNEL_A -->|Routes| W2_A W1_A --- D1_A W2_A --- D2_A end subgraph site_b ["Site B"] GW_B["Gateway"] KERNEL_B["Kernel"] W1_B["Whatsminer Worker"] W2_B["Avalon Worker"] D1_B["Whatsminers"] D2_B["Avalons"] GW_B -->|MDK Protocol via HRPC| KERNEL_B KERNEL_B -->|Routes| W1_B KERNEL_B -->|Routes| W2_B W1_B --- D1_B W2_B --- D2_B end App -->|HTTP| GW_A App -->|HTTP| GW_B style site_a fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A style site_b fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A ``` No MDK component spans sites. Combining them into one view is your own application code calling each site's Gateway separately, each Gateway still talking HRPC to its own Kernel, and merging the responses yourself. ## What parallel Workers and Kernels mean Multiple Workers of the same type (for example, `whatsminer-worker`) can be active concurrently, connected to the same Kernel instance. ```mermaid flowchart TD subgraph kernel ["Single 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 ``` Workers never share devices: device-to-Worker ownership is a strict, exclusive mapping the registry enforces, so adding Worker instances scales device count linearly with no coordination between them. When Kernel discovers a Worker, its identity response explicitly lists the `deviceId`s it exclusively manages, and the [Worker registry](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#workerregistry) holds that mapping. Kernel routes to whichever Worker owns a `deviceId`; it does not load-balance a device's traffic across multiple Workers, because only one Worker is ever registered as the owner of a given device at a time. ## Where state lives as you grow Each Kernel keeps its own [separate store](/concepts/the-storage-model): a multi-Kernel deployment means multiple independent stores, not one shared or federated one. ## Failure behavior - A single Worker going offline degrades reads/writes for that Worker's devices only: Kernel continues routing to every other registered Worker - A Kernel crash is recovered from its own command write-ahead log on restart: `recover()` restores in-flight commands to `QUEUED` and fails those out of retries, without re-sending either. It does not need to reconstruct device state, since it never owned it - In a multi-Kernel deployment, a crash at one site has zero effect on any other, there's no shared state to become inconsistent ## Next steps - Understand [the storage model](/concepts/the-storage-model): what grows with device count, and what doesn't - Choose a [deployment topology](/guides/deployment): how processes are packaged on a host - Understand [architecture](/concepts/architecture): the round trip every command and telemetry pull takes - Read the [Gateway's connection model](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md): how `startGateway()` resolves a single Kernel - Measure a deployment yourself with the [benchmark harness](https://github.com/tetherto/mdk/blob/main/backend/tests/benchmark/README.md) # Security boundaries (/concepts/security-boundaries) ## TL;DR No tier of MDK enforces user identity. The Gateway serves its plugin routes to any caller, Kernel inspects no user identity, and `WorkerRuntime` applies no caller allowlist, so the only user authentication on the Gateway path is what you build into your plugin controllers. Until you do, network policy and process isolation are the fleet's only boundaries. Assembling a site from UI, Gateway, Kernel, and Workers? The [site security blueprint](/guides/security) provides the steps, options, and suggested defaults. ## Overview This page describes the trust boundary at the Worker tier in depth and where it sits in the Worker → Kernel → Gateway → UI chain. Data flows outward along that chain; a request travels the other way, entering at the Gateway (or, for AI agents, the standalone MCP server). It covers what each layer does and does not authenticate. ## Worker security boundary ### Endpoint identity and key material [`WorkerRuntime`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/README.md) listens over HyperswarmRPC (HRPC) on an encrypted [Noise](https://noiseprotocol.org/) transport, and its HRPC public key identifies and addresses the endpoint. That proves which endpoint a backend peer is talking to: it does **not** establish a human or application identity, grant command permission, or substitute for the application-level authentication you implement above it. A Worker's identity is a pair of seeds, [`seedDht` and `seedRpc`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/lib/worker-runtime.js), from which its DHT and HRPC keypairs are derived. With a `storeDir`, they are written unencrypted as plain files in that directory; with a Hyperbee store, they live unencrypted in the store's `workerConf` space. Anyone who can read them can regenerate the keypair and impersonate the endpoint. Restrict access to the seeds with the same care as a private key: use restrictive file permissions, at-rest encryption where the host requires it, and treat backups as secrets. A DHT topic is only a discovery rendezvous — not a credential or an authorization token. ### Reachability and network controls The `WorkerRuntime` does not enforce a caller allowlist before dispatching the requests it supports: any backend peer that knows the Worker's public key and can reach it with HRPC over the network may send requests. The Kernel's [HRPC allowlist](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#hrpc-hyperswarm-rpc) protects clients connecting to Kernel, not direct callers to a Worker. Consumers must enter through the Gateway → Kernel path, with request authentication in the Gateway's plugin controllers. AI agents enter through the MCP server → Kernel path instead, where authentication is the MCP endpoint's concern — native MCP tools do not pass through those Gateway controllers. [Kernel's caller allowlist](/guides/security#step-2-admit-the-gateway-to-kernel) admits each path's caller by its HRPC public key. Given that the Worker does not turn an unwanted caller away, the surrounding controls are yours: - Restrict direct Worker reachability to trusted backend networks, and apply a host/container firewall policy - Never expose device management interfaces publicly - Treat Worker public keys and DHT topics as deployment configuration, distributed through an authenticated control plane - Run Kernel, Workers, and Gateway as separate processes or containers, so a compromise of one is contained and cannot spread to the others ### Production command path and secrets A host that starts a Worker with `services: null` — the minimal setup — provides no first-party service built-ins and no `write.calls.request` approval integration (MDK's built-in write-approval flow). Direct [`command.request`](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#commanddispatcher) dispatch still reaches plugin command handlers regardless, so a production command path must: - Authenticate the requester at the Gateway or control plane - Authorize each device and command, and optionally require approval for high-impact actions - Validate the request again in the handler, and rate-limit it - Write an audit record with actor, target, requested parameters, outcome, and correlation ID The handler context does not currently include actor identity, so actor-level auditing belongs upstream, with handler logs to supplement it. The control-plane security model covers 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`, and never place secrets in `mdk-contract.json`. Redact credentials and device responses from errors, debug logs, telemetry, and audit records. ## Next steps - Follow the [site security blueprint](/guides/security): steps, options, and suggested defaults for a real site - [Understand the Gateway's security model](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md#security-model): where user identity checks are meant to live - Review the control-plane security model: the production trust path for approval-gated writes - [Read the Worker Runtime store services reference](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/README.md#store-services): the full built-in surface `opts.services` can activate # The integration model (/concepts/the-integration-model) ## MDK integration Kernel is extended by building Worker and Gateway capabilities: | | Worker plugin | Gateway plugin | | --- | --- | --- | | Extends | The Worker tier | The Gateway tier | | Declares itself with | `mdk-contract.json` (the Worker contract) | `mdk-plugin.json` | | That declaration is read by | Kernel today (routing, validation); no other reader exists in this repo today | The Gateway loader (routes, auth flag) | | Job | Speak one device family's native protocol; expose it as telemetry + commands | Add an HTTP route: aggregate, authenticate, or otherwise sit between a caller and `@tetherto/mdk-client` | ["Worker Plugin"](/reference/glossary) refers to the **Worker Plugin** package on disk ([`mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/mdk-contract.schema.json) plus its handler files. ## `mdk-plugin.json` gets the same treatment, for Gateway plugins A Gateway plugin's manifest declares its routes (`id`, `handler`, `http.method`/`http.path`, response schema, constraints, examples, errors, `safety`). The Gateway's plugin loader reads it to mount routes and validate the manifest shape at load time; nothing about it is hand-wired into the Gateway's own code path per plugin. ## Workers are not only hardware A Worker plugin wraps whatever answers to "one device, one connection, one set of telemetry/commands"; that's just as often a non-hardware integration: - A **pool API** Worker: telemetry is your hashrate/earnings from the pool's own API, "commands" might be switching workers between pools; no physical device involved at all. - An **accounting sync** Worker: telemetry is a ledger balance or a sync status pulled from a third-party service, with no ASIC anywhere in the picture. Both get the exact same treatment from Kernel as a physical miner: identity, capabilities, telemetry pull, command dispatch. Kernel does not know or care that there is no hardware behind either one. ## What this buys you Write the integration once (one Worker plugin per device family, one Gateway plugin per route you need) and every consumer built against the standard round trip works with it for free: the same dashboard code, the same agent tooling, the same Gateway auth model, regardless of which device family or which route it's actually talking to underneath. ## Next steps - [Build a Worker plugin](/guides/workers/build-a-worker) for a new device family - Understand the [Gateway plugin authoring flow](/guides/gateway/plugins) - Understand [the storage model](/concepts/the-storage-model): what a Worker plugin decides to persist - Understand [architecture](/concepts/architecture): where both extension points sit in the round trip # The storage model (/concepts/the-storage-model) ## Where data lives Workers own their own telemetry storage. Kernel's Hyperbee holds three things only: the Worker/device registry, published capabilities, and the write-command log. | Data | Lives where | Engine | | --- | --- | --- | | Live device state | The device itself | None | | Worker registry, device capabilities, command log | Kernel's own store | [Hyperbee](https://github.com/holepunchto/hyperbee), via `@tetherto/hp-svc-facs-store` | | Historical telemetry | Each Worker, in its own store | [Worker-defined](/concepts/the-integration-model) | | Credentials and per-device config | Wherever the Worker plugin author put them | [Worker-defined](/concepts/the-integration-model) | ## What's authoritative, and what's cached The physical device is the one source of truth. Everything above it is a view: - A **Worker** is the authoritative record of its own device's state - **Kernel's registry** is authoritative for *routing* (which Worker owns which device), not for device state itself - **Kernel's command log** is authoritative for write-command lifecycle: every state transition (`QUEUED` → `DISPATCHED` → `EXECUTING` → `SUCCESS`/`FAILED`/`TIMEOUT`) is written to a write-ahead log (WAL) before it takes effect, so a crash mid-command recovers cleanly on restart. This WAL guarantee is scoped to that one component: the registry and capability stores next to it are plain Hyperbee, not WAL-backed. - Every telemetry read anywhere above Kernel (a Gateway plugin, a dashboard, an agent) is a live pull through the chain back to the Worker, never a read from a Kernel-side cache. Kernel caches nothing on your behalf. ## Why this model? MDK's storage choices favor **local-first, zero-external-dependency operation** over the query flexibility that alternatives such as a dedicated time-series database or a managed cloud store would give you: a site can run fully offline, with no database server to provision, back up, or pay for beyond the process itself. The [cost](/concepts/scalability) is that cross-device queries (a time range across every miner on a site) are the caller's job, not a stored-procedure or index the platform gives you for free. ## Retention Kernel's command log and registry retain what they need for correctness (routing state, in-flight command lifecycle) with no separate pruning policy documented today. Telemetry retention is entirely up to whatever a Worker plugin's author implemented: MDK does not impose or enforce a retention window. ## Can you swap the backend? Not today. Kernel's stores are Hyperbee via `@tetherto/hp-svc-facs-store` with no alternate-backend interface: a different storage engine is not a supported extension point the way a Worker plugin or Gateway plugin is. ## Getting data out There is no built-in export or streaming-to-a-warehouse path today. Anything you need outside of a live pull through a Gateway plugin (a scheduled export, a mirror into another system) is code you write yourself against `@tetherto/mdk-client`, the same way a plugin controller would. ## Failure A full disk or an unreachable Worker degrades the specific read that touches it: `telemetryCollector.pull()` returns nothing for that device rather than blocking every other request. A Kernel restart replays its command WAL (`recover()` sweeps non-terminal command states) but does not need to reconstruct device state, since it never owned it. Each Kernel instance keeps its own separate store: there is no shared or federated storage across multiple Kernels. A [multi-site deployment](/concepts/scalability) means multiple independent stores, one per site, with no cross-site consistency to reason about. ## Next steps - Understand [the integration model](/concepts/the-integration-model): what a Worker plugin decides to persist, and how - Understand [architecture](/concepts/architecture): the round trip a read or write actually takes - Understand [scalability](/concepts/scalability): what changes about storage as a fleet grows # What's an app? (/concepts/whats-an-app) ## Definition An MDK app is a `mdk.yaml` declaring a Gateway with its plugins, one or more Workers, and their configuration, plus, optionally, a UI or headless consumer built against that same Gateway. ## Anatomy ```mermaid flowchart TB subgraph reused ["MDK: the stack"] Kernel["Kernel"] Gateway["Gateway container"] Devkit["UI devkit (optional)"] end subgraph yours ["Yours: the app"] Spec["mdk.yaml: Worker + Gateway plugin selection, config"] WP["Worker plugins"] GP["Gateway plugins"] UI["UI or headless consumer"] end Spec --> Kernel Spec --> Gateway WP --> Kernel GP --> Gateway UI --> Gateway Devkit --> Gateway style reused fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A ``` - **`mdk.yaml`**: the deployment unit. Names the stack, ports, which Worker packages run with what device config, and which Gateway plugins load with what config. This is what `mdk onboard`/`mdk create` write and `mdk run` reads. - **Worker plugins**: yours if you're integrating a new device family; reused if you picked one that already ships (an Antminer worker, a demo worker). - **Gateway plugins**: yours if you need a route no existing plugin exposes; reused for anything already bundled. - **UI or headless consumer**: optional. A scaffolded dashboard, a script calling `@tetherto/mdk-client` directly, or nothing at all: Kernel and the Gateway don't require one. - **Kernel and the Gateway container**: never yours to modify. They're the invariant core every app runs unchanged (see [Architecture](/concepts/architecture)). ## What an app is not - Not a fork of Kernel or the Gateway: you never edit their source to build an app. - Not a monolith: a Worker plugin, a Gateway plugin, and a UI are independently swappable pieces, not one codebase. - Not a replacement for the Kernel: an app always sits on top of it, never instead of it. ## Next steps - Understand [the integration model](/concepts/the-integration-model): what a Worker plugin and a Gateway plugin each get to do - Understand [architecture](/concepts/architecture): how the pieces of an app talk to each other - Understand the [MDK App Toolkit](https://github.com/tetherto/mdk/blob/main/docs/concepts/app-toolkit.md): the layers a UI or headless consumer builds on - [Build an app](/tutorials/build-a-dashboard) from an empty directory # Guides (/guides) ## Security considerations } title="Site security blueprint" href="/guides/security" description="Secure an enterprise MDK site: identity, allowlists, and network isolation" /> ## Get started } title="CLI" href="/guides/cli" description="Install and verify the mdk CLI from a source checkout" /> } title="Run an MDK site" href="/guides/deployment" description="Choose a deployment topology and run a production or single-process MDK site" /> ## Build and extend } title="Gateway" href="/guides/gateway" description="Run, configure, and extend the MDK Gateway" /> } title="Operator agent" href="/guides/agent" description="Run MDK's conversational operator agent, standalone or behind the Gateway" /> } title="UI" href="/guides/ui" description="Compose reporting layouts and wire the React UI Devkit into your app" /> } title="Build a third-party Worker" href="/guides/workers/build-a-worker" description="Integrate your own hardware, firmware, or data feed with MDK by shipping a Worker package from your own repo" /> ## Connect hardware } title="Run containers" href="/guides/containers" description="Connect an Antspace or Bitdeer container to an MDK stack" /> } title="Miner Workers" href="/guides/miners" description="Connect Antminer, Avalon, or Whatsminer hardware to an MDK stack" /> # Operator agent how-to guides (/guides/agent) ## Overview `@tetherto/mdk-agent` is a conversational operator agent that answers plain-language questions about a mining fleet, calls fleet tools over MCP, and gates writes behind human approval. [What the agent is and how it fits the stack](https://github.com/tetherto/mdk/blob/main/backend/core/agent/README.md) covers the concepts; these guides cover running it. The agent never rides on another stack's own Gateway. A model (`:11500`), an MCP tool server (`:3008` in the [full-site example](https://github.com/tetherto/mdk/blob/main/examples/full-site/README.md)), and the agent's own Gateway (`:3847`) are three separate processes Full-site's own Gateway (`:3007`) has no `/agent` routes at all. ```text model ──► agent gateway ──► MCP tool server ──► kernel + workers :11500 :3847 :3008 (the fleet) ▲ └── UI shell :3030, proxying /agent ``` The agent connects to MCP once, when a session is created (the first `POST /agent/sessions`), so a session started before the tool server is up gets no tools and answers from the model alone, with no error to tell you. ## Choose a guide | Goal | Guide | | --- | --- | | Run the agent as a standalone CLI, for local development or evaluation | [Run the agent as a standalone CLI](/guides/agent/run-standalone) | | Deploy the agent behind the Gateway as a chat API for an operator UI | [Deploy the agent behind the Gateway](/guides/agent/gateway-deployment) | | Score a battery run against a fleet and compare models | [Evaluate the agent](https://github.com/tetherto/mdk/blob/main/backend/core/agent/docs/EVALUATION.md) | Exposing a plugin's own routes to the agent as tools is a detail of [building Gateway plugins](/guides/gateway/plugins), not its own deployment path. See [Expose data to the agent](/guides/agent/expose-data) there. ## Next steps - If Gateway, Kernel, or plugin are unfamiliar, see the [terminology](/reference/glossary) - [Understand the agent as a stack component](https://github.com/tetherto/mdk/blob/main/backend/core/agent/README.md) - [Understand the Gateway as a development surface](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md) # Expose data to the agent (/guides/agent/expose-data) ## Overview The operator agent calls fleet data and actions as MCP tools. A Gateway plugin's routes become those tools with no separate MCP manifest to author and keep in sync: the standalone MCP server reads the plugin's own `mdk-plugin.json` contract — the same file the Gateway loads — and converts each route into a tool. The MCP server is its own process. The Gateway hosts no MCP code and needs no MCP configuration; the two share nothing but the plugin directories they each read. This page shows the `mdk.yaml` shape that wires this up, the same way [mounting a plugin](/guides/gateway/plugins) does. It's an addition to your own site config, not a fixture to boot as written: the Kernel, Gateway, and plugin directory are whatever your app already has. Nothing MCP-related goes in your `startGateway()` call — the standalone `mdk run mcp` process reads the same `spec.gateway.plugins` list the Gateway loads. ## Prerequisites - A [Gateway plugin](/guides/gateway/plugins) declared in `spec.gateway.plugins` - The Kernel is running — a standalone MCP server dials it by key, so `mdk run kernel` comes first. [Kernel's caller allowlist](https://github.com/tetherto/mdk/blob/main/examples/full-site/docs/mcp-server.md) governs whether that MCP connection is admitted ### Serve a plugin's routes as tools Nothing is added to the plugin. Declare it in `mdk.yaml` as usual and start the MCP server: ```yaml spec: gateway: port: 3000 plugins: - package: ./plugins/custom-metrics mcp: port: 3100 # optional — defaults to gateway.port + 100 ``` ```bash mdk run kernel # first, in its own terminal mdk run mcp # then, in another ``` `mdk run mcp` reads the same `spec.gateway.plugins` list the Gateway loads, so a plugin's routes are reachable over HTTP and over MCP without being declared twice. Each route becomes a tool named after its `id` (dots and other non-alphanumeric characters become underscores), with the description, safety hint, and input schema derived from the route's `http` block. Path, query, and header parameters and the `requestBody`'s top-level properties become the tool's input fields, and the same route handler serves both interfaces — each plugin builds its own `mdkClient` from the ambient config, so it behaves identically whichever process loaded it. `mcp` is never part of the default `all` target; it is always started explicitly. ### Write tools by hand instead A plugin that needs a different tool granularity, richer descriptions, or direct `mdkClient` calls can author an `mcp-plugin.json` by hand. A standalone MCP server serves both kinds at once — hand-authored tool plugins alongside Gateway-plugin-derived ones — so a curated tool set and auto-derived routes can live on one endpoint. Tool ids must be unique across both sources; a collision is a startup error, not a silent override. Serving hand-authored tools this way means embedding the standalone server through `createMcpServer()`, as the `@tetherto/mdk-mcp` README shows. `mdk run mcp` reads no `spec.mcp.plugins` list yet, so from `mdk.yaml` alone it serves only the Gateway-plugin-derived tools above. ## Next steps - [Build the plugin whose routes you want to expose](/guides/gateway/plugins) - [Enable the operator agent](/guides/agent/gateway-deployment) to call the tools this produces - [Understand the agent as a stack component](https://github.com/tetherto/mdk/blob/main/backend/core/agent/README.md) # Deploy the agent behind the Gateway (/guides/agent/gateway-deployment) ## Overview [`@tetherto/mdk-plugin-agent`](https://github.com/tetherto/mdk/blob/main/backend/plugins/agent/README.md) mounts [`@tetherto/mdk-agent`](https://github.com/tetherto/mdk/blob/main/backend/core/agent/README.md) behind the Gateway as a chat API. Enable the plugin for session, message, and approval routes: every write the agent proposes pauses for an operator's decision. The agent itself still reaches fleet data the way [any AI agent does](https://github.com/tetherto/mdk/blob/main/backend/core/mcp/README.md#ai-agents-and-the-mcp-server), over an MCP server reachable at `agent.mcp.url` — the standalone `@tetherto/mdk-mcp` process; this plugin gives a human operator a chat surface to talk to it through. This is one of [two ways to run the agent](/guides/agent). If you've already run it [standalone](/guides/agent/run-standalone), it's the same agent with HTTP on it, not a second product: the same provider, the same MCP client, the same approval gate, just reached over sessions instead of a REPL. ## Prerequisites - The [Gateway is running](/guides/gateway/run) - A model is [served on this machine, or reachable on another](/guides/agent/run-standalone#serve-the-model-with-qvac), because the plugin dials the provider rather than starting one - An [MCP tool server](https://github.com/tetherto/mdk/blob/main/examples/full-site/docs/mcp-server.md) is reachable, so the agent has fleet tools to call The Gateway never starts a model. `config.agent.provider` is a URL it dials, so a first session against a `baseURL` with nothing behind it fails with `ERR_AGENT_UNAVAILABLE`. [Serving a model locally with QVAC](/guides/agent/run-standalone#serve-the-model-with-qvac) covers the install, the first-run download, and the flags a lazily loaded model needs. ### Mount the plugin Either: - [Select it during onboarding](#11-a-select-it-during-onboarding), or - [Already have an `mdk.yaml`? Add it by hand](#11-b-add-it-to-an-existing-mdkyaml) #### 1.1 A. Select it during onboarding Run [`mdk onboard`](/guides/cli/install#command-groups) and select [`mdk-plugin-agent`](https://github.com/tetherto/mdk/blob/main/backend/plugins/agent/README.md) from its Gateway plugin catalog. `mdk onboard`'s wizard supports creating a compliant `mdk.yaml` from scratch, interactively. Its entry carries a real `repoPath` ([`backend/plugins/agent`](https://github.com/tetherto/mdk/blob/main/backend/plugins/agent/README.md)), not a stub, so selecting it installs a working plugin rather than a placeholder. #### 1.1 B. Add it to an existing `mdk.yaml` Already have one from a previous onboarding run? Add the plugin under `spec.gateway.plugins` by hand, with the model provider, the MCP url, and the approval timeout under `agent`. Once published, that directory is `node_modules/@tetherto/mdk-plugin-agent`; in this monorepo checkout it is [`backend/plugins/agent`](https://github.com/tetherto/mdk/blob/main/backend/plugins/agent/README.md). For example, a standalone gateway carrying just this plugin, reaching across to the [full-site example](https://github.com/tetherto/mdk/blob/main/examples/full-site/README.md)'s MCP tool server: ```yaml apiVersion: mdk/v1 kind: Stack metadata: name: agent-gateway spec: workers: [] gateway: port: 3847 plugins: - package: "@tetherto/mdk-plugin-agent" config: agent: provider: { kind: qvac, model: qwen3-4b, baseURL: http://127.0.0.1:11500/v1 } mcp: { url: http://127.0.0.1:3008/mcp } approvalTimeoutMs: 120000 ``` #### 1.2 Run it Once the `mdk.yaml` names the plugin, either way, run `mdk run all`. A stack built for just this plugin declares no Workers, so `all` boots a Kernel it never actually uses (it only proxies chat to full-site's separate MCP server) alongside the Gateway. No auth plugin means every request binds to a single `local` operator, so a perimeter-trusted deployment gets the full chat and approval flow with no identity setup at all. A missing [`config.agent`](https://github.com/tetherto/mdk/blob/main/backend/plugins/agent/README.md#configuration) block answers `503 ERR_AGENT_UNAVAILABLE` instead of failing to load. ### Create a session and send a message Use the port your `mdk.yaml` gave the gateway: `3847` in the example above. Note that [full-site](https://github.com/tetherto/mdk/blob/main/examples/full-site/README.md)'s own gateway (`3007`) never carries the agent plugin; this is a separate gateway, reaching across to full-site's MCP tool server on `3008`. ```bash curl -X POST http://localhost:/agent/sessions # {"sessionId":"..."} curl -N -X POST http://localhost:/agent/sessions//messages \ -H 'Content-Type: application/json' \ -d '{"text":"how many miners are on the site?"}' ``` The response streams as `text/event-stream`. A read-only question ends in `tool_call`, `tool_result`, `token`, and `done` events, each stamped with the turn's `turnId` and a monotonic `seq`. ### Approve a write A write action pauses the turn instead of running it: ```text event: pending_approval data: {"type":"pending_approval","name":"act_device","args":{"ref":"whatsminer-0","action":"reboot"},"approvalId":"..."} ``` Decide it from the paused stream's `approvalId`: ```bash curl -X POST http://localhost:/agent/sessions//approvals/ \ -H 'Content-Type: application/json' \ -d '{"approved":true}' ``` Approving resumes the same stream: the tool runs for real, and the turn continues to its `token` and `done` events. Rejecting, or letting the approval window expire, resolves to false, and the write never runs. ## Troubleshooting - **Sessions and messages work, but the agent never calls a tool.** `agent.mcp` (or its `url`) is missing from the config — [how to fix it](https://github.com/tetherto/mdk/blob/main/backend/plugins/agent/README.md#troubleshooting) is in the plugin's troubleshooting entry - Every other failure (`503`, `404`, `409`, `400`) maps to a specific cause and fix in [the plugin's error reference](https://github.com/tetherto/mdk/blob/main/backend/plugins/agent/README.md#errors) - **`npm ci` fails to resolve a stack that mounts only this plugin.** `@tetherto/mdk-plugin-agent` declares `@tetherto/mdk-agent` as a required dependency, not an optional peer, so the agent package must be installed alongside it for `npm ci` to resolve. ## Next steps - [Read the agent plugin's route reference](https://github.com/tetherto/mdk/blob/main/backend/plugins/agent/README.md): session, message, and approval routes, plus the manifest's `setup` fields - [Understand the underlying agent](https://github.com/tetherto/mdk/blob/main/backend/core/agent/README.md): the model, its fleet tools, and the eval battery that scores it - [Submit and approve write actions](/guides/gateway/write-actions) from a React app, for the UI-driven shape of this same approval gate # Run the agent as a standalone CLI (/guides/agent/run-standalone) ## Overview [`@tetherto/mdk-agent`](https://github.com/tetherto/mdk/blob/main/backend/core/agent/README.md) runs as a small library and CLI for local development or evaluation, talking to a served model and an MCP tool server directly, with no Gateway in front of it. This is one of [two ways to run the agent](/guides/agent). ## Prerequisites - Node.js ≥ 24 - npm 11 [(< 12)](/reference/environment) - GPU — see [the agent's prerequisites](https://github.com/tetherto/mdk/blob/main/backend/core/agent/README.md#prerequisites) for the exact backend requirements - Dependencies installed — see [the agent's prerequisites](https://github.com/tetherto/mdk/blob/main/backend/core/agent/README.md#prerequisites): a single root `npm install`, not one run inside the package - Language model: this guide uses [QVAC](https://qvac.tether.io/) serving an OpenAI-compatible API ### Serve the model with QVAC The agent needs a language model listening locally. Already have a QVAC server running elsewhere? Skip this step, then add `--base-url ` when you [start the agent](#start-the-agent-and-ask). #### 1.1 Note the agent package's path ```bash cd backend/core/agent # From the repo root (dependencies come from the root npm install) pwd # Note this: the serve command below needs it after it cd's elsewhere ``` #### 1.2 Serve the model QVAC installs into a separate directory, so a fresh terminal, started from wherever you like: ```bash mkdir -p ~/qvac-runtime && cd ~/qvac-runtime && npm init -y # once npm i @qvac/cli@^0.12.0 # ~6 GB of prebuilt engines ./node_modules/.bin/qvac doctor # confirms this host can serve # --config pins the model: Qwen3-4B, 4-bit quantized, 16k context ./node_modules/.bin/qvac serve openai \ --config /qvac-runtime/qvac.config.json \ --port 11500 --verbose --no-cancel-load-on-disconnect # --verbose: load + GPU offload; wait until listening on 11500 ```
Why install @qvac/cli outside this package? `@qvac/cli` is an optional peer dependency because the agent only needs it to *serve* a model, not to talk to one. Installing its ~6 GB into this tree perturbs a `package-lock.json` that CI gates on. External mode never loads `@qvac/cli` at all, so a separate directory costs nothing and keeps the lockfile clean.
The port and flag above aren't arbitrary: `11500` avoids Ollama's default `11434` (whichever starts second on a machine with both dies on `EADDRINUSE`), and `--no-cancel-load-on-disconnect` stops a lazily loaded model's first request from failing with a `503` that isn't really about a disconnect. If this doesn't work first try, see [troubleshooting](#troubleshooting).
### Optional: Start the demo fleet If you already have an MCP tool server, skip this and point the agent at its URL in the [next step](#start-the-agent-and-ask). To try it end-to-end, boot the full-site example. It brings up a simulated fleet (miners, containers, powermeters, sensors, pools) and exposes the MDK tools over MCP on port `3008`: ```bash cd ../../../examples/full-site npm run setup # first time only. NOT npm install: examples/full-site is a root workspace member, # and this also builds the UI toolkit and installs its own nested ui/ app npm start # kernel + workers + gateway + MCP server on :3008 ``` Leave it running. ### Start the agent and ask In a third terminal, start the CLI — pointing it at the model (per the previous step) and the MCP tool server: ```bash node bin/mdk-agent.js --model qwen3-4b --mcp-url http://127.0.0.1:3008/mcp ``` You may now query the site in plain language, for example: ```text you › how many miners are on the site? → tool count_devices({"family":"miner","state":"all"}) ← data { "summary": "30 miners.", "count": 30 } ▌ There are 30 miners on the site. you › list the devices that are not ready you › reboot antminer-3 ← a write: the agent stops and asks you to approve ``` REPL commands: `/about` (what this is) · `/tools` · `/info` · `/new` · `/exit`.
## Troubleshooting ### `npx qvac` fails with a 404 There is no `qvac` package on npm; the binary comes from `@qvac/cli`, installed in [Serve the model](#serve-the-model-with-qvac). ### `qvac serve openai` dies with `EADDRINUSE` `qvac serve openai` defaults to port `11434`, the same as Ollama's. Pass `--port 11500` (as the command above does) if both run on this machine. ### The first request 503s with `model_load_failed`, blaming a disconnect that didn't happen `serve.load.cancelOnDisconnect` defaults to `true`, which cancels a lazily loaded model's first request. `--no-cancel-load-on-disconnect` fixes it. Models declared `preload: true` (like `qwen3-4b` in `qvac-runtime/qvac.config.json`) never hit this, but the flag costs nothing. ### The first run looks stalled, or the server "never starts" The first run fetches ~2.5 GB peer-to-peer. The log says `registry://s3/…`, but the transfer is actually Hyperswarm (a DHT plus UDP hole-punching), at roughly ~110 MB/min, so ~25 minutes. There's no HTTP fallback, so a network that blocks UDP stalls it, and since `preload: true` means the port doesn't open until the model is resident, a stalled download presents as a dead server, not a failed fetch. Three things help: 1. Copy `qvac.config.json` and set `preload: false`. The port opens in ~5 s and a stalled fetch surfaces as a `503` on the first request instead of a server that looks dead. Switch back once `~/.qvac/models` holds the weights. 2. Always pass `--verbose`; without it there's no progress output at all. 3. Bypass the registry entirely: `serve.models` accepts an explicit `{ "src", "type" }` entry, and `*ModelSrc` fields accept URLs and filesystem paths, so a GGUF fetched by hand over HTTPS, or copied from a machine that already has one, works. This is the path for a locked-down site. ### Disk usage is larger than expected Budget ~6 GB for the CLI, 2.5 GB for the model, and a KV cache that grows. Of that 6 GB, ~5 GB is prebuilt binaries for platforms this host can't run (every modality engine is pulled whether used or not). Deleting the foreign `prebuilds/` directories takes the install to ~400 MB and is safe, verified by cold restart, but `npm i` restores them, so it's disk relief rather than a fix. ### The fleet has fewer devices than expected Each Worker seeds its device list once, into a persistent on-disk store, and only when that store is empty. A later `npm start` reuses whatever's already there rather than reseeding to the current `--miners` count, so a fleet from an earlier run (a different `--miners` value, or an interrupted seed) sticks around. Clear `examples/full-site/.mdk-data/workers/*/store` for a fresh fleet on the next start. ## Next steps - [Look up a flag, the capability table, or a hosted-model setup](https://github.com/tetherto/mdk/blob/main/backend/core/agent/README.md): the CLI's full reference - [Score a battery run against a fleet and compare models](https://github.com/tetherto/mdk/blob/main/backend/core/agent/docs/EVALUATION.md) - [Deploy the agent behind the Gateway instead](/guides/agent/gateway-deployment), for a chat API an operator UI can call # Install the CLI (/guides/cli) ## TL;DR - Operate the stack from your terminal - Until available as an npm package, link `mdk` on your PATH with: - `npm install` at mdk/packages - `npm run build && npm link` at mdk/packages/cli ## Overview `mdk` is MDK's command-line tool for standing up and operating a stack from your terminal. It scaffolds a stack (`mdk create`), boots it (`mdk run`), and reports its health (`mdk status`), all driven by the `mdk.yaml` spec `mdk onboard` writes for you. ## Prerequisites - [Node.js](https://nodejs.org/) >=24 (LTS) - npm 11 [(< 12)](/reference/environment) ## Install Until `@tetherto/mdk-cli` is published to npm, link `mdk` on your PATH from a source checkout.
Install the CLI steps If `mdk --version` already prints a version, skip to step 6. 1. Clone the repo and `cd` into `packages`. ```bash git clone https://github.com/tetherto/mdk.git && cd mdk/packages ``` 2. Stay on `main`, or pin a tagged release. Pick a tag from [mdk tags](https://github.com/tetherto/mdk/tags) and check it out. ```bash git checkout -b /v v ``` For example, `git checkout -b local/v0.7.0 v0.7.0`. 3. Install dependencies. ```bash npm install ``` 4. Build and link the `mdk` binary. ```bash cd cli && npm run build && npm link ``` 5. Confirm `mdk` is on your PATH. ```bash mdk --version ``` 6. Run project commands from the app or UI monorepo you are working in. Commands such as `mdk skill add` and `mdk onboard` write into your current directory, so run them from your project, not from `mdk/packages/cli`. Only `mdk --version` is global once linked.
## Command groups A selection of commands you can run today: | Group | Commands | | ----------------- | ----------------------------------------| | Onboarding | `mdk onboard` | | Scaffold | `mdk create worker\|plugin\|dashboard` | | Run & manage | `mdk run [target]`, `mdk status` | | Agent enablement | `mdk skill add` | | Meta | `mdk version` | The [CLI's command surface](https://github.com/tetherto/mdk/blob/main/packages/cli/README.md#command-surface) documents the full flag reference. ## Update ```bash git pull && npm install && npm run build ``` ## Uninstall ```bash npm rm -g @tetherto/mdk-cli ``` ## Next steps - Try the [demo](/tutorials/run-a-site) to see a stack running end to end - [Build with your agent](/guides/ui/agent-skills) so a coding agent knows MDK conventions # Run a container Worker (/guides/containers) ## Overview MDK drives each container system 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). Covered so far: - [Run a Bitdeer Worker](/guides/containers/run-bitdeer-worker) - [Run an Antspace Worker](/guides/containers/run-antspace-worker) ## Prerequisites Every guide assumes: - [Node.js](https://nodejs.org/) >=24 (LTS) - npm 11 [(< 12)](/reference/environment) - Dependencies installed (`npm run setup` from the repo root) - Commands are run from the repo root - Outbound network access for Kernel discovery For the mock or development path: - No physical container is required - The runnable example for your model starts the bundled mock 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 container system reachable from the machine or container running the Worker - The Worker's README 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 [miner troubleshooting](/guides/miners/troubleshooting), the same HRPC and DHT checks apply - Drive the registered device from a dashboard: [run a mining site end to end](/tutorials/run-a-site) # Run an Antspace Worker (/guides/containers/run-antspace-worker) ## Overview This page details how to run the Bitmain Antspace container Worker. Select the development (mock) or real-container path. ## Prerequisites - Review the [common deployment prerequisites](/guides/containers#prerequisites) before you start Deployment-specific requirements: - A Node.js service or script in your deployment that runs the MDK Worker and registers containers - A supported Antspace container reachable from the machine or container running the Worker over its REST HTTP API, typically port `8000` ### Development
Run against a mock To support development, this repo ships a runnable example that starts the bundled mock, boots the Worker against it, starts a Kernel, and registers the container: ```bash node examples/backend/containers/antspace/index.js ``` It prints the Kernel HRPC key and the registered device ID, then stays running until Ctrl+C. For the mock's model and port options, see [the Antspace README](https://github.com/tetherto/mdk/blob/main/backend/workers/containers/antspace/README.md).
### Connect a container #### 2.1 Pick your model Use [the Antspace README](https://github.com/tetherto/mdk/blob/main/backend/workers/containers/antspace/README.md) to confirm the `model` value for your container: `hk3` or `immersion`. This guide uses `hk3`, replace it with the value for your container. #### 2.2 Register your container 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 Antspace container, replace the example address and credentials with your container's values: ```js const { getKernel } = require('@tetherto/mdk-core') const { startAntspaceWorker } = require('@tetherto/mdk-worker-antspace') const kernel = await getKernel() const worker = await startAntspaceWorker({ workerId: 'antspace-rack-1', model: 'hk3', storeDir: './store/antspace-rack-1', seedDevices: [{ info: { serialNum: 'HK3-A', container: 'container-A', location: 'site-a-01.container' }, opts: { address: '192.168.1.100', port: 18001 } }] }) await kernel.registerWorker(worker.runtime.getPublicKey()) ``` Make sure each container's address is reachable from the machine or container running the Worker before registering. Commands affect live cooling for racks of miners, prioritize thermal safety. `seedDevices` only seeds a fresh, empty `storeDir`, once persisted, the device set survives restarts on its own. To add a container to an already-running fleet, send the `registerThing` command to the live Worker instead: ```js const { createMdkClient } = require('@tetherto/mdk-client') const client = createMdkClient({ kernelKey: kernel.getPublicKey() }) await client.connect() await client.sendWorkerCommand('antspace-rack-1', null, 'registerThing', { id: 'HK3-B', info: { serialNum: 'HK3-B', container: 'container-A' }, opts: { address: '192.168.1.101', port: 18001 } }) ``` `registerThing` persists the container config immediately, but the running Worker does not pick it up until it is stopped and restarted (`await worker.stop()`, then call `startAntspaceWorker` again with the same `storeDir` and no `seedDevices`), there is no hot-add. For the full `seedDevices` and `registerThing` option reference, the telemetry and command tables, and the shared install pattern, see [the Antspace README](https://github.com/tetherto/mdk/blob/main/backend/workers/containers/antspace/README.md) and [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/containers/antspace/index.js`. A working run prints the Kernel HRPC key and the registered device ID, then stays running until Ctrl+C. If it does not print those values, or if the mock port is already in use, the network and port checks in [miner troubleshooting](/guides/miners/troubleshooting) apply here too, the underlying HRPC and DHT requirements are the same across every Worker. ## Next steps - Decide how to run the Worker service, [Deployment topologies](/guides/deployment) - Review telemetry units, command shapes, and error codes, [the Antspace README](https://github.com/tetherto/mdk/blob/main/backend/workers/containers/antspace/README.md) # Run a Bitdeer Worker (/guides/containers/run-bitdeer-worker) ## Overview This page details how to run the Bitdeer D40 container Worker. Select the development (mock) or real-container path. The Bitdeer D40 speaks MQTT, and the Worker embeds the broker (one per Worker process) that a container publishes into, rather than the Worker connecting out to the container. Device specs are keyed by `containerId`, not by an address and port. ## Prerequisites - Review the [common deployment prerequisites](/guides/containers#prerequisites) before you start Deployment-specific requirements: - A Node.js service or script in your deployment that runs the MDK Worker and registers containers - A supported D40 container configured to publish into the Worker's embedded MQTT broker, reachable on that broker's port (default `10883`) ### Development
Run against a mock To support development, this repo ships a runnable example that starts the Worker (embedding its MQTT broker), points a mock D40 container at that broker as an MQTT client, starts a Kernel, and registers the container: ```bash node examples/backend/containers/bitdeer/index.js ``` It prints the Kernel HRPC key and the registered device ID, then stays running until Ctrl+C. For the mock's model and container ID options, see [the Bitdeer README](https://github.com/tetherto/mdk/blob/main/backend/workers/containers/bitdeer/README.md).
### Connect a container #### 2.1 Pick your model Use [the Bitdeer README](https://github.com/tetherto/mdk/blob/main/backend/workers/containers/bitdeer/README.md) to confirm the `model` value for your D40 variant: `a1346`, `m30`, `m56`, or `s19xp`. This guide uses `m56`, replace it with the value for your container. #### 2.2 Register your container 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 D40 container, replace the example container ID with your container's value: ```js const { getKernel } = require('@tetherto/mdk-core') const { startBitdeerWorker } = require('@tetherto/mdk-worker-bitdeer') const kernel = await getKernel() const worker = await startBitdeerWorker({ workerId: 'bitdeer-rack-1', model: 'm56', storeDir: './store/bitdeer-rack-1', mqttPort: 10883, seedDevices: [{ info: { serialNum: 'D40-M56-001', container: 'container-A' }, opts: { containerId: 'D40-M56-001' } }] }) await kernel.registerWorker(worker.runtime.getPublicKey()) ``` Make sure the container is configured to publish into this Worker's broker port before registering. Commands act on physical cooling and power hardware, prioritize thermal safety. `seedDevices` only seeds a fresh, empty `storeDir`, once persisted, the device set survives restarts on its own. To add a container to an already-running fleet, send the `registerThing` command to the live Worker instead: ```js const { createMdkClient } = require('@tetherto/mdk-client') const client = createMdkClient({ kernelKey: kernel.getPublicKey() }) await client.connect() await client.sendWorkerCommand('bitdeer-rack-1', null, 'registerThing', { id: 'D40-M56-002', info: { serialNum: 'D40-M56-002', container: 'container-A' }, opts: { containerId: 'D40-M56-002' } }) ``` `registerThing` persists the container config immediately, but the running Worker does not pick it up until it is stopped and restarted (`await worker.stop()`, then call `startBitdeerWorker` again with the same `storeDir` and no `seedDevices`), there is no hot-add. For the full `seedDevices` and `registerThing` option reference, the telemetry and command tables, and the shared install pattern, see [the Bitdeer README](https://github.com/tetherto/mdk/blob/main/backend/workers/containers/bitdeer/README.md) and [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/containers/bitdeer/index.js`. A working run prints the Kernel HRPC key and the registered device ID, then stays running until Ctrl+C. If it does not print those values, or if the broker port is already in use, the network and port checks in [miner troubleshooting](/guides/miners/troubleshooting) apply here too, the underlying HRPC and DHT requirements are the same across every Worker. ## Next steps - Decide your [deployment topology](/guides/deployment) to run the Worker service - [Review telemetry units, command shapes, and error codes](https://github.com/tetherto/mdk/blob/main/backend/workers/containers/bitdeer/README.md) # 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](https://github.com/tetherto/mdk/blob/main/backend/core/docs/README.md#connection-and-deployment-model). ## Choose a guide - Run Kernel, Gateway, and Workers in one Node.js process with the [single-process topology](/guides/deployment/run-single-process-site) - Run a multi-Worker site as separate, PM2-supervised processes, from one machine up to a cross-host deployment, using [supervised services](/guides/deployment/run-all-workers-site) ## Next steps - Understand the trade-offs before you choose your [deployment topology](https://github.com/tetherto/mdk/blob/main/backend/core/docs/README.md#connection-and-deployment-model) - Measure real CPU, RAM, disk, and latency for your own hardware and device count with [the benchmark harness](/guides/deployment/benchmark-your-site) - Browse the [functions](https://github.com/tetherto/mdk/blob/main/backend/core/mdk/README.md) that wire together the [Kernel](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md), [device Workers](https://github.com/tetherto/mdk/blob/main/backend/workers/README.md), and the [Gateway](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md) HTTP - Follow the [site security blueprint](/guides/security): identity, allowlists, and network options for a real site # Benchmark your site (/guides/deployment/benchmark-your-site) Turn a sizing guess into a number you can trust: real Kernel, Gateway, and Worker processes under real load, for your own hardware. Use the benchmark harness when you want a filled [capacity and metrics template](/guides/deployment/capacity-metrics-template) from measured runs on your own hardware, instead of by hand. See [when to use this](/guides/deployment/benchmark-your-site#when-to-use-this) for the specific cases. ## Run the benchmark - Follow the benchmark harness [config](https://github.com/tetherto/mdk/blob/main/backend/tests/benchmark/config/benchmark.config.json.example) - Use the [quick start](/guides/deployment/benchmark-your-site#quickstart) Learn more from the [benchmark readme](/guides/deployment/benchmark-your-site#overview). The benchmark harness fills in the [capacity and metrics template](/guides/deployment/capacity-metrics-template) from measured runs. Every run uses the same process topology a real MDK deployment runs under PM2 — mocks, Kernel, Gateway, and each Worker as their own OS process (never blended into one Node process). ## Overview The benchmark harness boots real Kernel, Gateway, Worker, and mock-device processes (one mock TCP/HTTP listener per **Worker** — every device that Worker owns shares it, real network round trips, not in-memory fakes; any [Worker family](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/README.md) can be mixed into a single run). The harness: - Drives read/action/Gateway-request loads - Samples CPU/RSS/open-FDs per process - Runs real failure drills (Worker restart, Kernel restart, a fleet-wide unreachable-device outage) - Writes a filled profile (JSON + Markdown) per run plus a comparison matrix across a sweep. The generated Markdown mirrors the template's own section headings and table shapes exactly — real measurements where this harness has them, `_` (the template's own placeholder) everywhere it doesn't; see [What's measured vs. left blank](#whats-measured-vs-left-blank) for which is which. A single JSON config is the source of truth for the fleet to boot; see [Layout](#layout) for what each file does and [The config file](#the-config-file) for what it controls. ## When to use this - You want a real sizing answer for your own hardware and device count, not published numbers from a different tier - You need to confirm a configuration change still meets the pass/fail thresholds in [`capacity-metrics-template.md`](/guides/deployment/capacity-metrics-template) before it reaches production - You want failure-drill numbers (Worker restart, Kernel restart, a fleet-wide device outage) measured on your own hardware, not assumed from another run ## Quickstart ```bash # fast correctness check (5 devices, ~seconds) — wired into `npm test` npm test # the one benchmark run: boots every family in config.workers # simultaneously and sweeps the Cartesian product of every family's own # device-count range, lowest total device count first, stopping at the # first combination that goes red (the fleet's breaking point — see # capacity-metrics-template.md "Ceiling profiles") npm run benchmark ``` Every combination step writes `results/.json` and `.md`, named by that step's total device count and Worker count (e.g. `cap-150devices-2workers` for 100 Antminers + 50 Avalons on 2 Workers). The filename encodes only those two numbers, not the per-family split, so two combinations that reach the same total device count and Worker count collide: the later one's report overwrites the earlier one's. The run as a whole additionally writes one combined `results/sweep-benchmark-matrix.md` (generated under gitignored `results/` — not committed) across every combination tried. Commit the specific reports you want to keep alongside a sizing decision, not the whole directory. ## The config file [`config/benchmark.config.json`](https://github.com/tetherto/mdk/blob/main/backend/tests/benchmark/config/benchmark.config.json.example) (copy from the linked `.example`) is the only file you edit to change what gets measured: | Section | Feeds | | ---------- | --------------------------------------------------------------------------------------------------------------- | | `hardware` | The template's "Reference hardware" block. Fill this in manually per host: the harness can't detect vendor/tier | | `workers` | One entry per device family, all booted simultaneously and swept together | Each `workers` entry is `{ type, model, simulateMocks, ceiling: { startDeviceCount, stepDeviceCount, maxDeviceCount } }`, for example, one entry from [`config/benchmark.config.json.example`](https://github.com/tetherto/mdk/blob/main/backend/tests/benchmark/config/benchmark.config.json.example): ```json { "type": "mdk-worker-antminer", "model": "s21", "simulateMocks": true, "ceiling": { "startDeviceCount": 10, "stepDeviceCount": 10, "maxDeviceCount": 30 } } ``` Note that `type` must be one of `Object.keys(WORKER_REGISTRY)` in [`lib/constants.js`](https://github.com/tetherto/mdk/blob/main/backend/tests/benchmark/lib/constants.js) (one entry per package under [`backend/workers/miners/`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/README.md)), and `model` one of that `type`'s supported models. `ceiling.startDeviceCount` and `stepDeviceCount` must each be `>= 1`. `maxDeviceCount` caps how far that entry's dimension of the sweep goes, and both other fields must additionally fall within `[1, maxDeviceCount]`, unless there's only one entry in `workers`, in which case `maxDeviceCount` may be `0` (uncapped: keep raising the device count until a step actually goes red). With more than one entry, every `maxDeviceCount` must be a real number, since the sweep is the Cartesian product of every entry's range and an unbounded dimension can't be combined into a finite product. All of this is validated eagerly when the config loads. ### Fixed operating defaults Everything that isn't about sizing a run: - Host/discovery/data root - Kernel cadence - Worker operating intervals/timeouts/concurrency - `allowDuplicateIPs` - Mock port/auth password - Alert-induction thresholds - Failure-drill toggles - Run-reproducibility shape (`n`, soak duration, resource-sample interval, read/action load) - Pass/fail thresholds (headroom, action submit p99, steady CPU, rejects+timeouts, RSS slope) all this lives in [`lib/constants.js`](https://github.com/tetherto/mdk/blob/main/backend/tests/benchmark/lib/constants.js). Workers always boot on their own package defaults (plus `ALERT_INDUCTION`'s threshold override, the one intentional exception: see [`lib/site.js`](https://github.com/tetherto/mdk/blob/main/backend/tests/benchmark/lib/site.js)). `allowDuplicateIPs` is always on (every mock lives on `127.0.0.1`, one server per Worker; its devices share it, differentiated only by port). Mock ports are picked randomly per run (one per Worker, not per device), each Worker's password is read from its Worker type's own mock default, and `RUN_REPRODUCIBILITY`/`THRESHOLDS` are recorded verbatim into every report. ### Run a real leak-detection soak `RUN_REPRODUCIBILITY.soakMs` defaults to a short 60s so sweeps and CI stay fast. The template requires **≥ 24h** of soak before an RSS slope means anything as a real growth or leak signal rather than noise. To get that signal, bump `soakMs` to 24h or more in [`lib/constants.js`](https://github.com/tetherto/mdk/blob/main/backend/tests/benchmark/lib/constants.js) and run a single profile, not a full sweep: a sweep repeats the soak once per combination it tries, so a 24h soak across a whole sweep would take days. Any run shorter than 24h still produces an RSS slope, but reports label it **indicative** rather than a confirmed signal. ## What's measured vs. left blank Measured automatically: - **Read path**: single-device telemetry both through the Gateway (`GET /api/fleet/device/{id}/telemetry`, a plugin route this harness adds) and bypassing it (Client → Kernel → Worker directly); the aggregate fleet-wide read (Gateway → Kernel → every Worker, through the harness's own generic [`plugin/fleet-summary`](https://github.com/tetherto/mdk/tree/main/backend/tests/benchmark/plugin/fleet-summary) plugin); device list/registry read. - **Write/action path**: submit through the Gateway (`POST /api/fleet/device/{id}/action`, another plugin route this harness adds) and the direct Client → Kernel path — both real HTTP/RPC round trips, not simulated. Submit and execute collapse into one measured step; see "Action-approval workflow steps" under Left blank for why. - **Cycle headroom** (worst Worker) and **sustained read/action throughput** (reads/s, actions/s, rejected, timed-out, peak queue depth). - **Per-process CPU/RSS** (sampled every tick via `ps -o rss,pcpu -p `) and **open file descriptors** (sampled at profile start and end via `lsof -p ` — more expensive than `ps`, so not sampled every tick), each with a real slope across the run (`ResourceSampler` in [`lib/metrics.js`](https://github.com/tetherto/mdk/blob/main/backend/tests/benchmark/lib/metrics.js)) — **indicative** below a 24h soak, same as everywhere else growth/leak claims show up in this harness. - An **approximate** device-only baseline (raw TCP connect to the mock's port — a floor, not the full vendor-protocol round trip). - **Alerts path**: every device's temperature-warning threshold is forced below any real reading (see `ALERT_INDUCTION` in [`lib/constants.js`](https://github.com/tetherto/mdk/blob/main/backend/tests/benchmark/lib/constants.js)), so the family's own alert genuinely trips on the first snap the Worker collects at its own (never-overridden) cadence. The harness measures how long after it starts watching that alert first becomes visible both via the Kernel directly (`pollAlerts` in [`lib/load.js`](https://github.com/tetherto/mdk/blob/main/backend/tests/benchmark/lib/load.js)) and via the Gateway (`pollAlertsViaGateway`, hitting `GET /api/fleet/device/{id}/alerts` — a third plugin route this harness adds, since the Gateway had no way to surface `last.alerts` before) — `n = 0` on either just means the Worker's own snap interval (default 60s) didn't complete a cycle within this run's soak, not that induction failed. - **Storage breakdown**: real on-disk size per Worker store, plus a real growth/day computed from size at profile start vs. now, divided by the run's own elapsed time (same short-soak "indicative" caveat). - **Failure behaviour** (`runFailureDrills` in [`processes/run-process.js`](https://github.com/tetherto/mdk/blob/main/backend/tests/benchmark/processes/run-process.js), toggled by `RUN_REPRODUCIBILITY.runFailureDrills`, on by default): real kill+respawn drills for Worker restart and Kernel restart (both processes' identity persists across a restart against the same on-disk root, confirmed empirically, so the existing client reconnects on its own), plus a device-outage drill. The outage drill can only make the **whole fleet** unreachable, not one device — every device behind a Worker shares one mock server — so it reports both the measured (fast-refusal, since a closed port is refused immediately rather than timing out) and an analytical hung-device worst case (`timeout × ⌈device count / concurrency⌉`) from already-known operating parameters. Runs once, after the steady-state checklist finishes, never during (so it can't contaminate the capacity numbers above). Left blank, with a note in the generated report: - **Action-approval workflow steps**: the full push → vote → execute workflow as distinct submit/approve/exec/e2e rows, vote/approve as its own step, and batch actions across N devices in one call. All three shipped Workers allowlist their write actions at a single required vote, and this harness only ever submits via `sendCommand` and its Gateway-mirrored route, never the Kernel's separate `pushAction`/`voteAction`/`queryActions` pipeline - **Alert generation latency in isolation**: synchronous inside the Worker process, the same reason heap/external memory is blank, needs in-process instrumentation this harness doesn't have - **Alert fan-out to N subscribers and historical alert queries**: need Gateway capabilities, a push/subscription mechanism and a history store, beyond a single request/response endpoint - **Kernel-internal scheduled telemetry pull and health ping**: no client-observable start/end signal distinct from the reads already measured ## Layout ```text config/benchmark.config.json the one input file lib/constants.js Worker-type registry + fixed defaults (never edited to size a run) + ALERT_INDUCTION (per-family threshold used to trip a real alert) + RUN_REPRODUCIBILITY.runFailureDrills/*TimeoutMs/deviceOutageMs lib/site.js boot primitives (bootKernel/bootWorker/bootGateway/startMocks) — one mock server per Worker, and bootWorker wires ALERT_INDUCTION in lib/metrics.js per-process CPU/RSS sampler (ps -o rss,pcpu -p , every tick) + open-FD sampler (lsof -p , start/stop only) + dirSizeBytes lib/latency.js per-operation latency recorder (p50/p95/p99/max/n/errors) lib/load.js read/action load generators, cycle headroom, device baseline, pollAlerts/pollAlertsViaGateway (alert-visibility latency pollers) lib/report.js pass/fail evaluation + Markdown/JSON report + comparison matrix lib/sweep-runner.js spawns one child process per profile, folds results into entries plugin/fleet-summary/ Gateway plugin every profile run loads: fleet-wide aggregate (listWorkers→pullTelemetry) plus per-device telemetry/action/alerts routes (controllers/device-telemetry.js, device-action.js, device-alerts.js) processes/run-process.js --role mocks|kernel|worker|gateway|profile (defaults to profile); profile spawns the other four roles as child processes, coordinates the checklist, then (unless disabled) runs runFailureDrills scenarios/benchmark.js sweeps the Cartesian product of every config.workers entry's device range, writes one comparison matrix tests/benchmark.smoke.test.js fast correctness check (not a capacity claim; skips failure drills for speed) results/ generated reports (gitignored) ``` ## Add a device family [`lib/constants.js`](https://github.com/tetherto/mdk/blob/main/backend/tests/benchmark/lib/constants.js)'s `WORKER_REGISTRY` covers every package under [`backend/workers/miners/`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/README.md) (Antminer, Avalon) — any of them can be used in `config.workers[].type`. To benchmark a family outside that directory (container, power meter, ...), add an entry to the registry's `WORKER_PACKAGES` map pointing at the target package's `startWorker` export; everything else (config, load generators, metrics, reporting) is device-family agnostic because it only talks to the family through `createMdkClient`. ## Next steps - Fill in the [capacity and metrics template](/guides/deployment/capacity-metrics-template) by hand for a topology this harness doesn't cover yet - Run [a multi-Worker site](/guides/deployment/run-all-workers-site) or [a single-process site](/guides/deployment/run-single-process-site) to reproduce a profile's topology before benchmarking it # Capacity and metrics template (/guides/deployment/capacity-metrics-template) ## Overview This template turns measured runs into **sizing answers**: given your hardware and `N` miners, how many Workers do you need, and what do they cost in CPU, RAM, and disk? Fill each profile with what is running, record host resources and end-to-end latencies, then derive per-device cost, cycle headroom, and pass/fail status. Use the same percentile set, soak rules, and load definition across profiles so rows stay comparable. Absolute numbers without reference hardware, device baseline, and a zero-device floor are not enough to size a site. Prefer formulas (`RAM ≈ base + n × per-device`) and headroom ratios over one-off tables. The [benchmark harness](/guides/deployment/benchmark-your-site) fills this template in from measured runs automatically. Fill it in by hand only for a topology it doesn't cover yet. ## What a filled profile should answer | Question | Where it comes from | | --- | --- | | How many devices per Worker? | Cycle time / configured interval (headroom); ceiling sweeps | | How many Workers for `N` devices? | Devices-per-Worker recommendation × `N`, validated by Worker-split sweeps | | CPU / RAM / disk for that layout | Zero-device baseline + per-device marginal cost | | Is this configuration supported? | Pass/fail thresholds (green / amber / red) | | What breaks first? | Ceiling profiles and failure-behaviour section | ## Reference hardware Record one hardware block per profile. ### Hardware block (per profile) | Field | Value | | --- | --- | | Reference tier | `edge` / `site-server` / `custom` | | CPU model | `_` | | CPU cores (physical / logical) | `_` / `_` | | RAM | `_` GiB | | Disk type | NVMe / SATA SSD / HDD / SD-class / other | | Disk size | `_` GiB | | OS | `_` | | Node.js version | `_` | | MDK version / commit | `_` | | Network path to devices | local LAN / VLAN / WAN / simulated-in-process | | Notes | `_` | ### Reference tiers | Tier | Intent | Typical shape (fill with your lab hosts) | | --- | --- | --- | | `edge` | Small box colocated with a rack or container | `_` cores, `_` GiB RAM, SD-class or SATA | | `site-server` | Proper site host for hundreds–thousands of devices | `_` cores, `_` GiB RAM, NVMe | Map a customer host to the nearest tier before quoting sizing numbers. If hardware differs materially (especially disk class or core count), treat results as indicative only. ## Capacity profile Define what is running before you measure anything. ### Workers List Worker types and device counts. | Worker type | Device count | Max Device count | real / simulated | | --- | --- | --- | - | | `_` | `_` | `_` | - | | `_` | `_` | `_` | - | ### Run reproducibility Define enough that someone outside the team can repeat the run. | Field | Value | | --- | --- | | Profile id | e.g. `cap-100dev-w2` | | `n` (samples per latency row) | `_` (minimum recommended: `_`) | | Soak duration | `_` (minimum **24 h** for growth / leak claims) | | Load generator | name / script / commit | | Read load definition | e.g. `R` concurrent telemetry reads, mix `_` | | Action load definition | e.g. `A` actions/s, types `_`, batch size `_` | | Alert induction method | real condition / injected / wait | | Config artifact path / hash | `_` | | Start time (UTC) | `_` | | End time (UTC) | `_` | ## Resource metrics template Record process-level and site-level resources. CPU and RSS are sampled cross-process (e.g. `ps -o rss,pcpu`), which any consumer can do without being inside the Node process — heap and external/buffer memory need in-process instrumentation instead, so they're out of scope here even though Hypercore traffic shows up outside the V8 heap. ### Per process Repeat one table per process (Kernel, each Worker, Gateway, MCP). | Metric | Unit | Warm (after READY) | Steady (end of soak) | Peak | Notes | | --- | --- | --- | --- | --- | --- | | CPU average | % of 1 core or host | `_` | `_` | `_` | | | CPU peak | % | `_` | `_` | `_` | | | RSS | MiB | `_` | `_` | `_` | | ### Site aggregate | Metric | Unit | Value | | --- | --- | --- | | Sum of process RSS | MiB / GiB | `_` | | Host CPU (all MDK processes) | % | `_` | ### Storage breakdown Per-Worker-store disk size and growth (Kernel/Gateway/alerts/logs storage isn't broken out per worker, so it's out of scope here). Growth/day is a real delta — size at profile start vs. now — divided by the run's own elapsed time; label **indicative** unless the soak is at least 24 h. Projections are linear extrapolations of that rate, not a model of Hyperbee/Corestore's actual (non-linear) growth curve — do not extrapolate a short soak to 12 months and treat it as a commitment. | Store | Size now (MiB) | Growth / day (MiB) | Projected 6 mo (GiB) | Projected 12 mo (GiB) | | --- | --- | --- | --- | --- | | `_` | `_` | `_` | `_` | `_` | | **Total** | `_` | `_` | `_` | `_` | ### Zero-device baseline and per-device marginal cost Sums do not extrapolate. Measure a **zero-device** (or idle Worker with no owned devices) baseline, then subtract to get marginal cost. | Metric | Zero-device baseline | At `D` devices | Per-device marginal | Unit | | --- | --- | --- | --- | --- | | CPU (site, all MDK processes) | `_` | `_` | `_` | % of 1 core | | RSS (site, all MDK processes) | `_` | `_` | `_` | MiB | **Sizing formulas** (fill coefficients from the table): ```text RSS ≈ RSS_base + D × RSS_per_device CPU ≈ CPU_base + D × CPU_per_device ``` State whether coefficients are per Worker process or site-wide, and which reference tier they belong to. ## Device baseline (MDK overhead) Firmware round-trip dominates on real hardware. Record device-only response time so you can subtract it from MDK path latencies. | Measurement | Boundary | p50 ms | p95 ms | p99 ms | max ms | n | Real / simulated | | --- | --- | --- | --- | --- | --- | --- | --- | | Device-only telemetry / status read | Direct to device (no MDK) or mock equivalent | `_` | `_` | `_` | `_` | `_` | | | Device-only action ack | Direct to device | `_` | `_` | `_` | `_` | `_` | | | Derived | Formula | Value | | --- | --- | --- | | MDK overhead (read p99) | MDK path p99 − device-only p99 | `_` ms | | MDK overhead (action exec p99) | Exec path p99 − device-only p99 | `_` ms | The number that makes MDK credible for sizing is the **overhead MDK adds** on top of what the device itself takes. ## Cycle headroom (devices per Worker signal) For each profile, record how long a full collection cycle takes against the interval it is configured for. The ratio is headroom; the point where it crosses **1.0** is the practical answer to "how many devices per Worker." | Metric | Value | Unit | | --- | --- | --- | | Configured telemetry interval | `_` | ms | | Full collection cycle time (all owned devices) | `_` | ms (p50 / p95 / p99: `_` / `_` / `_`) | | Cycle headroom ratio | `cycle_time` / interval | `_` | | Devices owned by this Worker | `_` | count | | Unreachable devices during cycle | `_` | count | | Timeout budget consumed by unreachable devices | `_` | ms | | Headroom | Meaning | | --- | --- | | `< 0.7` | Comfortable | | `0.7–1.0` | Amber — little spare capacity | | `≥ 1.0` | Overrun — reduce devices per Worker or raise concurrency / interval | ## Throughput under load Record sustained rates and backpressure. | Metric | Value | Unit | | --- | --- | --- | | Sustained telemetry reads / s | `_` | 1/s | | Sustained actions / s | `_` | 1/s | | Queue depth (peak / steady) | `_` / `_` | count | | Rejected requests | `_` | count | | Timed-out requests | `_` | count | ## Latency metrics template All latencies in **milliseconds** use the same percentile set across profiles so rows are comparable. Record `n` on every row (see run reproducibility). ### Read path (telemetry and state) Time from consumer request until usable payload returns. | Operation | Boundary | p50 ms | p95 ms | p99 ms | max ms | n | errors | | --- | --- | --- | --- | --- | --- | --- | --- | | Telemetry read (single device) | Gateway → Kernel → Worker → Gateway | `_` | `_` | `_` | `_` | `_` | `_` | | Telemetry read (single device) | Kernel → Worker only | `_` | `_` | `_` | `_` | `_` | `_` | | Telemetry / overview read (aggregate) | Gateway (+ plugin) → Kernel → Workers | `_` | `_` | `_` | `_` | `_` | `_` | | Device list / registry read | Gateway → Kernel | `_` | `_` | `_` | `_` | `_` | `_` | Scheduled Kernel telemetry pull and health ping are Kernel-internal timers with no client-observable start/end signal distinct from the reads above — not independently measurable without in-process Kernel instrumentation, so they're not tracked as separate rows. ### Write / action path Split **submit** and **execution** so bottlenecks are visible. A separate approval round only exists if the Worker's write-action whitelist requires more than one vote — every shipped worker whitelists at a single required vote, so submit and execute collapse into one step. | Operation | Boundary | p50 ms | p95 ms | p99 ms | max ms | n | errors | | --- | --- | --- | --- | --- | --- | --- | --- | | Send / submit action | Client → Gateway → Kernel accept | `_` | `_` | `_` | `_` | `_` | `_` | | Action execution (Kernel dispatch → Worker → device ack) | Kernel ActionCaller → Worker → device | `_` | `_` | `_` | `_` | `_` | `_` | | End-to-end write action (submit → executed / terminal state) | Client → … → device → client-visible result | `_` | `_` | `_` | `_` | `_` | `_` | Record separately by action type (for example reboot vs setPowerMode vs `updateThing`). | Action type | `reqVotes` | e2e p50 ms | e2e p99 ms | exec-only p50 ms | exec-only p99 ms | n | | --- | --- | --- | --- | --- | --- | --- | | `_` | `_` | `_` | `_` | `_` | `_` | `_` | ### Alerts path Track delivery — when Kernel or Gateway consumers can read the alert — separately by boundary. Alert generation (condition → persisted alert record) happens synchronously inside the Worker process and isn't independently observable without in-process instrumentation, so it isn't tracked as its own row. | Operation | Boundary | p50 ms | p95 ms | p99 ms | max ms | n | errors | | --- | --- | --- | --- | --- | --- | --- | --- | | Alert visible via Kernel (pull / list / query) | Consumer → Kernel → alert source | `_` | `_` | `_` | `_` | `_` | `_` | | Alert visible via Gateway (HTTP / WebSocket / plugin) | Consumer → Gateway → Kernel / store | `_` | `_` | `_` | `_` | `_` | `_` | Fan-out to N subscribers and historical (range-read) alert queries need Gateway capabilities (a subscription/push mechanism, a history store) beyond a single request/response endpoint — out of scope until the Gateway carries one. ## Pass / fail thresholds Set site-wide defaults, then mark each profile green / amber / red. | Criterion | Green | Amber | Red | Profile result | | --- | --- | --- | --- | --- | | Telemetry freshness (cycle ≤ interval) | headroom `< 0.7` | `0.7–1.0` | `≥ 1.0` | `_` | | Action e2e p99 | `≤ _` ms | `≤ _` ms | above amber | `_` | | Steady-state CPU (host or busiest process) | `≤ _` % | `≤ _` % | above amber | `_` | | Rejected + timed-out under sustained load | `0` | `< _` | otherwise | `_` | | RSS slope over 24 h soak | flat / `_` MiB/h | `_` | clear leak | `_` | **Supported up to** means the densest green profile on that reference tier (devices per Worker and total `D`). Publish that bound explicitly after ceiling sweeps. ## Failure behaviour Measure process failure recovery on at least one profile per reference tier. "Unreachable device" can only be induced fleet-wide if every device behind a Worker shares one mock/gateway endpoint rather than an isolated one per device — note the actual scope achieved, not just "one device" by assumption. | Scenario | Metric | Value | Notes | | --- | --- | --- | --- | | Worker restart | Time to healthy / owning devices again | `_` ms | | | Kernel restart | Time to READY | `_` ms | | | Unreachable device | Effect on cycle time (measured) | `_` | live drill result; note whether the failure mode was a fast refusal or an actual hang | | Unreachable device | Effect on cycle time (analytical, hung-device worst case) | `_` | timeout × ⌈unreachable count / concurrency⌉ | | Unreachable device | Timeout budget per device | `_` ms | from operating parameters | | 24 h soak | RSS slope (MiB / h) per process | `_` | leak detection | | 24 h soak | FD / socket slope (per hour) per process | `_` | | ## Profile comparison matrix One row per capacity profile. Copy columns as needed. Leave cells blank until measured. Prefer profile ids like `cap-10dev` / `cap-100dev-w2` (not `cap-10m`, which reads as ten minutes). | Profile id | Tier | Real/sim | Kernels | Gateways | Plugins | W | D | Alert rules | CPU sum % | RSS sum | Heap sum | Disk now | Disk/day | Headroom | Reads/s | Actions/s | Rejects+timeouts | Read p99 | Action e2e p99 | Alert gen p99 | Status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | `cap-10dev` | `_` | `_` | 1 | 1 | 1 | 3 | 12 | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | | `cap-50dev` | `_` | `_` | 1 | 1 | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | | `cap-100dev` | `_` | `_` | 1 | 1 | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | | `cap-500dev` | `_` | `_` | 1 | 1 | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | | `cap-1000dev` | `_` | `_` | 1 | 1 | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | | `cap-5000dev` | `_` | `_` | 1 | 1 | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | | `cap-custom` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | `_` | ## Measurement checklist Use the same checklist for every profile so results stay comparable. 1. Record reference hardware (tier, CPU, RAM, disk class, OS, Node, MDK, network path) 2. Record the capacity profile (control plane, Workers, devices, real vs simulated, operating parameters including alert rule count) 3. Record run reproducibility (`n`, soak ≥ 24 h for growth/leak claims, load generator, config hash) 4. Bring the site to READY; capture zero-device baseline if this run includes marginal-cost derivation 5. Sample CPU and RSS on a fixed interval throughout the soak; sample open FDs/sockets and on-disk store size at start and end (heap/external memory need in-process instrumentation and stay out of scope) 6. Measure device-only baseline response times; derive MDK overhead 7. Measure full telemetry cycle time vs configured interval (headroom) 8. Drive a defined read load; record read-path latencies and sustained reads/s 9. Drive a defined action load (single and batch); record submit, exec, e2e, actions/s, queue depth, rejects/timeouts 10. Induce or wait for alerts; record Kernel and Gateway visibility latencies (alert generation itself is Worker-internal and not independently observable) 11. Run failure drills (Worker restart, Kernel READY, unreachable-device impact — measured live plus the analytical hung-device worst case) for the profile set 12. Compute storage growth per Worker store from the soak (size at start vs. now, divided by elapsed time); label 6/12-month projections **indicative** 13. Apply pass/fail thresholds; copy summary into the comparison matrix and relevant sweep tables 14. Note failures, timeouts, config drift, and which single variable changed vs the previous profile # Run a multi-process site (/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 to run a demo for multiple configured Workers across device families - a miner, a mining pool, and a powermeter - each supervised as its own separate process. 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 the [local topology](/guides/deployment) under PM2 supervision. Use this when: - You want to explore a multi-Worker site and its telemetry in one running system - You need supervisor-managed restarts and logs, and want to restart or scale one service without restarting the others - You are testing PM2 orchestration before deploying to hardware, or want a production-like layout for Gateway and Workers - 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](/guides/deployment) from single-process to distributed microservices. This example's [`config/site.deploy.json`](https://github.com/tetherto/mdk/blob/main/examples/mvp-site/config/site.deploy.json.example) sets `discovery` to `"local"` by default (Kernel and Workers share one machine, discovery via shared directory). Setting it to `"dht"` moves discovery onto Hyperswarm so Workers can run on separate hosts, but the example's own README doesn't walk through that mode end-to-end. For a worked cross-host walkthrough today, see [`examples/full-site`'s `cli.js --discovery dht`](https://github.com/tetherto/mdk/blob/main/examples/full-site/README.md#how-out-of-process-workers-find-the-kernel). ## Run the example Follow the [Starter site example](https://github.com/tetherto/mdk/tree/main/examples/mvp-site): - Start with the [prerequisites](https://github.com/tetherto/mdk/tree/main/examples/mvp-site#prerequisites) - Use [PM2](https://github.com/tetherto/mdk/tree/main/examples/mvp-site#start-the-site) for local process supervision on one host - [Verify](https://github.com/tetherto/mdk/tree/main/examples/mvp-site#start-the-site) the fleet is up ## Next steps - Understand the trade-offs between [deployment topologies](/guides/deployment) - Run [a single-process site](/guides/deployment/run-single-process-site) for the simpler single-process topology - [Register a single miner](/guides/miners) before building a site config - 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](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md), [device Workers](https://github.com/tetherto/mdk/blob/main/backend/workers/README.md), and the [Gateway](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md) HTTP - Build your own [Worker from scratch](/guides/workers/build-a-worker) # 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](/guides/deployment) concept explains when to choose single-process instead of a supervised, multi-process deployment. ## 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/full-site): - Start with its [prerequisites](https://github.com/tetherto/mdk/tree/main/examples/full-site#prerequisites) - Use the example [quick smoke test and full run](https://github.com/tetherto/mdk/tree/main/examples/full-site#quick-smoke-test-recommended-first-run) ## Next steps - Compare the supported shapes: [Deployment topologies](/guides/deployment) - Run the supervised topology — [Run a multi-Worker site as supervised services](/guides/deployment/run-all-workers-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 is a container that hosts plugins and delivers an HTTP interface for your frontend: each plugin builds its own [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) from its context. These guides cover how to run it and extend it with the plugin system. An AI agent reaches MDK over MCP, not the Gateway's HTTP surface directly. MCP is served by a standalone [`@tetherto/mdk-mcp`](https://github.com/tetherto/mdk/blob/main/backend/core/mcp/README.md) process that derives tools from a mounted plugin's routes; the Gateway hosts no MCP itself. If Gateway, Kernel, or plugin are unfamiliar, [terminology](/reference/glossary) defines them. The [Gateway concept page](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md) covers the full developer model (extension, data access, auth design). ## Choose a guide | Goal | Guide | | --- | --- | | Start the Gateway for the first time | [Run the Gateway](/guides/gateway/run) | | Declare the plugins MDK ships, 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) | | Secure the site you are assembling | [Site security blueprint](/guides/security) | ## Next steps - [Understand the Gateway as a development surface](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md) - Read the [Gateway API reference](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md) - Choose a [deployment shape](/guides/deployment) - [Give an operator a chat interface to the fleet](/guides/agent) by deploying the operator agent behind the Gateway - Follow the [site security blueprint](/guides/security): identity in controllers, Kernel allowlist, and UI session options # 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. A Gateway loads exactly the plugins your stack declares in `spec.gateway.plugins[]` and nothing else, whether they are yours or ones MDK ships. A plugin builds its own [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) to call into the Kernel — no knowledge of the MDK Protocol envelope or internal message shapes is required. ## Prerequisites - The [Gateway is running](/guides/gateway/run) - A Kernel instance running and reachable, or `kernelKey: false` to start without a Kernel connection (development only) ## Plugins MDK ships The [supported plugins reference](/reference/supported-plugins) is the source of truth for what currently ships and the routes each serves. The site plugins it bundles today are subpaths of the [`@tetherto/mdk-plugins`](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md) package, so each is named by its subpath: ```yaml # mdk.yaml spec: gateway: port: 3847 plugins: - package: "@tetherto/mdk-plugins/telemetry" - package: "@tetherto/mdk-plugins/site-monitor" ``` A stack serves only the plugins it declares. A route from a [bundled plugin](/reference/supported-plugins#bundled-site-plugins) you have not declared answers `404`. If a [React adapter](https://github.com/tetherto/mdk/blob/main/ui/packages/react-adapter/README.md) hook reads one of those routes, declare the plugin that serves it. The [`auth` plugin](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md#the-bundled-auth-plugin) (`@tetherto/mdk-plugin-auth`) ships in the same package but is not among them, and declaring it does not give you working identity endpoints: its controllers still expect a second handler parameter and a populated `req._info` that the Gateway does not provide. Supply your own identity layer. Plugins you mount yourself are documented by their own manifests. ### Mount a plugin `extraPluginDirs` is the complete list of plugins `startGateway()` loads — there is no set it is added to: ```js const { startGateway, bundledPluginDir } = require('@tetherto/mdk-core') await startGateway({ kernel, port: 3000, extraPluginDirs: [ path.join(__dirname, 'plugins/custom-metrics'), path.join(__dirname, 'plugins/alerts'), bundledPluginDir('telemetry') // one MDK ships, by name rather than by path ] }) ``` 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. [Exposing a plugin's routes to the operator agent](/guides/agent/expose-data) turns them into MCP tools with no separate manifest, using this same `extraPluginDirs` entry plus one flag. ### 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 either an `http` block with a `method` and `path`, or those same `method`/`path` fields flattened to the route's top level; the bundled agent plugin uses the flat form. Rather than copy a synthetic example, start from a real manifest and trim it: - [`examples/backend/mdk-plugin-e2e/gateway-plugin/mdk-plugin.json`](https://github.com/tetherto/mdk/blob/main/examples/backend/mdk-plugin-e2e/gateway-plugin/mdk-plugin.json): one route, fully annotated with a response schema, `constraints`, `errors`, and `safety`. The easiest starting point, and [seeing a plugin serve your data](https://github.com/tetherto/mdk/blob/main/examples/backend/mdk-plugin-e2e/run.js) runs it end to end - [`examples/mvp-site/backend/gateway-plugins/site/mdk-plugin.json`](https://github.com/tetherto/mdk/blob/main/examples/mvp-site/backend/gateway-plugins/site/mdk-plugin.json): four routes including `GET`s with query parameters, and `POST`s 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 A controller builds its own [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) once, from the plugin's context config, in a [`lib/client.js`](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/telemetry/lib/client.js) every controller in the plugin requires. Every controller exports an `async function (req)`: ```js // controllers/live.js — read live telemetry const mdkClient = require('../lib/client') module.exports = async function live (req) { const deviceId = req.query.deviceId const telemetry = await mdkClient.pullTelemetry(deviceId, 'metrics') return { deviceId, ...telemetry } } ``` ```js // controllers/command.js — dispatch a command const mdkClient = require('../lib/client') module.exports = async function command (req) { const deviceId = req.params.deviceId const { mode } = req.body const result = await mdkClient.sendCommand(deviceId, 'setPowerMode', { mode }) return { deviceId, commandId: result.commandId, status: result.status } } ``` ### The `req` object A controller's only argument. [The controller reference](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md#controllers) documents every field (`params`, `query`, `body`, `headers`, `_info`) and how it's assembled. ### The plugin's context module `require('@tetherto/mdk-gateway/plugin')` resolves, inside a loaded plugin, to that plugin's own frozen context. [The controller reference](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md#controllers) shows a controller building its own client from it: | Field | Type | Contains | | --- | --- | --- | | `config` | `object` | The Gateway's runtime config, with `kernelKey`/`kernelBootstrap` folded in, and this plugin's own per-plugin config layered over the top key-by-key | | `logger` | `object` | A `child()` of the Gateway's own logger, tagged with this plugin's name — its lines interleave with the Gateway's own on stdout, ordered and formatted the same | | `onReady` | `function` | Registers a callback that runs once this Gateway is serving. Errors thrown inside it are [caught and logged as a warning, not fatal](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md#runtime-plugin-errors) | ### Supplying per-plugin config That per-plugin config isn't declared in `mdk-plugin.json` — it comes from the stack spec (`spec.gateway.plugins[].config`), passed as a `config` key alongside `dir` in the `extraPluginDirs` entry: ```js extraPluginDirs: [ { dir: path.join(__dirname, 'plugins/custom-metrics'), config: { apiKey: process.env.METRICS_API_KEY } } ] ``` Build a [`lib/client.js`](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/telemetry/lib/client.js) from it once per plugin and `require` that module from every controller that needs one — there is no per-request Kernel access to guard, only the client's own connect failures: `createMdkClient` connects on first use and memoizes the connection. A failure maps to `ERR_MDK_CLIENT_UNAVAILABLE` (or your own `opts.errorCode`) and resets so the next call retries — guard the call, not a null client: ```js try { return await mdkClient.pullTelemetry(deviceId, 'metrics') } catch (err) { if (err.message === 'ERR_MDK_CLIENT_UNAVAILABLE') throw new Error('ERR_KERNEL_UNREACHABLE') throw err } ``` ### Read hardware data Call the client directly for live device data — [`pullTelemetry`, `getCapabilities`, and `listWorkers`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md#client-methods) are documented with their return shapes in the client's own reference. A Worker is single-device, so a live fleet-wide total — hashrate across every miner on site, say — is the controller's own job: list every Worker, pull each device's live telemetry, and add the numbers up: ```js const { workers } = await mdkClient.listWorkers() const pulls = workers.flatMap((w) => (w.deviceIds || []).map(async (deviceId) => { const { metrics } = await mdkClient.pullTelemetry(deviceId, 'metrics') return metrics?.stats?.hashrate_mhs?.avg || 0 })) const totalHashrateMhs = (await Promise.all(pulls)).reduce((sum, v) => sum + v, 0) ``` [`demo/controllers/summary.js`](https://github.com/tetherto/mdk/blob/main/backend/plugins/demo/controllers/summary.js) is the smallest worked example of this fan-out, worth starting from if you're authoring your own plugin. There is no separate Gateway-side store for historical or aggregated data, either. Fan [`pullWorkerTelemetry`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md#client-methods) out across every registered Worker and read the series from the Worker's own persisted tail-log: ```js const { workers } = await mdkClient.listWorkers() const results = await Promise.allSettled( workers.map((w) => mdkClient.pullWorkerTelemetry(w.workerId, { type: 'logs', key: 'stat-1D', tag: 't-miner', start, end })) ) ``` [`demo/controllers/history.js`](https://github.com/tetherto/mdk/blob/main/backend/plugins/demo/controllers/history.js) is the smallest worked example — a single-file version with an optional per-device filter and a Kernel-unavailable fallback. Both live and historical calls are network calls through the client — guard both [the same way](#supplying-per-plugin-config). An unguarded `pullTelemetry` or `pullWorkerTelemetry` throws `ERR_MDK_CLIENT_UNAVAILABLE` straight through to the caller as an unhandled `500` the moment a Worker or the Kernel drops; neither path degrades more gracefully than the other. ### Send a command [`sendCommand`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md#client-methods) dispatches via the Kernel to the Worker that owns the device — the command must be declared in the Worker's [`mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/mdk-contract.schema.json). `controllers/command.js` above shows the pattern; return shape is `commandId`, `status`, `result`, `error`. ### Caching Add a `"cache"` array of dot-path strings to a route to enable request-level caching. The cache is bypassed for a forced refresh only when `overwriteCache` is literally `true` and the request carries a non-empty `Authorization` header; any other value, including `false`, uses normal cache behavior. The Gateway does not authenticate or validate the header, so [protecting the route](#auth-and-permissions) stays the controller's job. [The manifest reference](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md#manifest-format) shows the field in a real manifest. ### Stream routes Add `"stream": true` to a route to own the raw `ServerResponse` instead of returning a plain value — for SSE or any other response Fastify shouldn't serialize. [The manifest reference](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md#manifest-format) covers the mechanism and the handler's error behavior. [`backend/plugins/agent`](https://github.com/tetherto/mdk/blob/main/backend/plugins/agent/README.md) is a shipping example — its message route streams `text/event-stream` this way; see the [agent Gateway-deployment guide](/guides/agent/gateway-deployment) for the consumer side. ### Auth and permissions The Gateway applies no authentication of its own, as [its authentication design](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md) describes. Every route a plugin declares is served to any caller, so a route that needs protecting carries that logic in its own controller. Identity is yours to supply: the manifest `"auth"` and `"permissions"` fields have no reader and change nothing. The [bundled `auth` plugin](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md#the-bundled-auth-plugin) is not a substitute — the Gateway neither registers it nor gives its controllers what they still expect. Validate the token with your own identity layer and check it in the handler: ```js const { validateToken } = require('../lib/my-identity-layer') module.exports = async function protectedRoute (req) { const token = req.headers.authorization?.replace('Bearer ', '') if (!token) throw Object.assign(new Error('ERR_UNAUTHORIZED'), { statusCode: 401 }) const { permissions, email } = validateToken(token) if (!permissions.includes('miner:w')) throw Object.assign(new Error('ERR_FORBIDDEN'), { statusCode: 403 }) // Pass email and permissions into Kernel yourself. Do not take them from req.body. } ``` A controller's return value always goes out as `200`. A thrown error is shaped by [`errorResponse`](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/workers/lib/plugin-adapter.js): its status comes from `err.statusCode` when it is an integer 400 or higher, and falls back to `400` otherwise. Attach `.statusCode` to get a specific code, the way `protectedRoute` does here; the bundled agent plugin uses this same pattern for its 503, 404, and 409 responses. The body is `{ statusCode, error, message }`, and `message` carries the error's own text for `ERR_*` codes and for Fastify's own 4xx errors (validation, content type, body size); any other message is replaced with the status text and logged. ### Manifest validation errors The plugin loader validates every manifest and handler at startup and throws if anything is wrong — see [the loader's error codes](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md#manifest-validation-errors) for the full list. ## 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](/tutorials/build-a-dashboard) — end-to-end worked example of the single-plugin + controller pattern - Read the [demo plugin](https://github.com/tetherto/mdk/blob/main/backend/plugins/demo/README.md) for the smallest complete worked example of both fan-out patterns above - Understand [how Workers declare their data](/guides/workers/build-a-worker) via [`mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/mdk-contract.schema.json) — what [`mdkClient`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md#client-methods) reads and [`sendCommand`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md#client-methods) dispatches - See the full [manifest and controller 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](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md). ## Prerequisites - [Node.js](https://nodejs.org/) >=24 (LTS) - npm 11 [(< 12)](/reference/environment) - Commands are run from the repository root - A 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. ```js const { getKernel, startGateway } = require('@tetherto/mdk-core') const kernel = await getKernel() const server = await startGateway({ kernel, port: 3000 }) // HTTP server is up at http://localhost:3000 ``` The Gateway ships no built-in authentication, so every route it serves is unauthenticated. Supply your own identity layer and call it from the controllers that need protecting, as [auth and permissions](/guides/gateway/plugins#auth-and-permissions) describes. The [`@tetherto/mdk-plugin-auth`](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md#the-bundled-auth-plugin) plugin bundled with MDK is not a substitute: the Gateway neither registers it nor provides what its controllers expect. 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 a 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-core') 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-core') const server = await startGateway({ kernelKey: '', port: 3000 }) ``` Pre v1.0, Kernel's allowlist `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](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md) 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](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md) - Choose a [deployment shape](/guides/deployment) # Tear down MDK services (/guides/gateway/teardown) ## Overview MDK registers graceful shutdown handlers automatically when you start services with `getKernel()`, a Worker boot function, 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](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md) - MDK [installed and a working boot sequence](/guides/gateway/run) ### Automatic teardown with `getKernel()` `getKernel()` registers `SIGINT`/`SIGTERM` handlers internally. A Gateway started with `opts.kernel` is chained into the cleanup sequence automatically. Workers are **not** auto-chained: a Worker's boot function has no `opts.kernel`, so push its `stop()` onto `kernel._cleanup` yourself if you want Kernel shutdown to cascade to it: ```js const { getKernel, startGateway } = require('@tetherto/mdk-core') const { startAntminerWorker } = require('@tetherto/mdk-worker-antminer') const kernel = await getKernel() const { runtime, stop } = await startAntminerWorker({ workerId: 'antminer-rack-1', model: 's19xp', storeDir: './data/antminer' }) await kernel.registerWorker(runtime.getPublicKey()) kernel._cleanup.push(stop) // cascade Worker shutdown from Kernel await startGateway({ kernel, port: 3000 }) // Press Ctrl+C: MDK stops Gateway, then the Worker, then Kernel. ``` See [`getKernel` API reference](https://github.com/tetherto/mdk/blob/main/backend/core/mdk/README.md#getkernelopts--promisekernelmanager) and the [Workers discovery model](https://github.com/tetherto/mdk/blob/main/backend/workers/README.md) for the same-process lifecycle rules. ### 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-core') const kernel = await getKernel() await startGateway({ kernel }) // … 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-core') 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()` and `startGateway({ kernel })` wire themselves into `kernel._cleanup` so a single signal stops Kernel and Gateway in order; push a Worker's `stop()` onto `kernel._cleanup` yourself to fold it into the same chain. 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 - [`@tetherto/mdk-core` README](https://github.com/tetherto/mdk/blob/main/backend/core/mdk/README.md): full API reference - [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 an [actions plugin mounted](#create-an-actions-plugin) - Your actions plugin controllers validate tokens and check permissions themselves, since [an unprotected route is reachable by anyone](/guides/gateway/plugins#auth-and-permissions) - Your controllers pass the caller's device-family write permissions (`miner:w`, `container:w`) to Kernel as `authPerms`, which Kernel requires before resolving or approving a write - If present, your 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's voting surface (typically exposed by a plugin at routes like `/auth/actions*`). #### 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 the Gateway's voting endpoint (for example, `PUT /auth/actions/voting/:id/vote` if using that plugin pattern) 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 hook calls the Gateway's cancel endpoint (for example, `DELETE /auth/actions/voting/cancel` if using that plugin pattern). ```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.
## Create an actions plugin To enable approval-gated writes, create a plugin that exposes HTTP routes for the write-action workflow. Each route should call the corresponding method on the plugin's own `mdkClient` (built from `require('@tetherto/mdk-gateway/plugin')`, [as any Gateway plugin does](/guides/gateway/plugins)). The paths shown below (`/auth/actions*`) are illustrative examples. You may use any path structure that fits your plugin's routing pattern. ### Required routes | Method | Example Path | `mdkClient` method | Purpose | |--------|--------------|------------------|---------| | `GET` | `/auth/actions` | `queryActions()` | Query actions by lifecycle state (voting/ready/executing/done) | | `POST` | `/auth/actions/voting` | `pushAction()` | Submit a single action for approval | | `POST` | `/auth/actions/voting/batch` | `pushActionsBatch()` | Submit multiple actions for approval | | `PUT` | `/auth/actions/voting/:id/vote` | `voteAction()` | Cast approval/rejection vote on an action | | `DELETE` | `/auth/actions/voting/cancel` | `cancelActionsBatch()` | Cancel pending actions by IDs | ### Plugin structure ```text backend/plugins/actions/ ├── mdk-plugin.json └── controllers/ ├── query.js ├── push.js ├── push-batch.js ├── vote.js └── cancel.js ``` ### Example controller (push.js) ```javascript 'use strict' const { validateToken } = require('../lib/my-identity-layer') const mdkClient = require('../lib/client') module.exports = async function pushAction (req) { // Identity comes from your own layer: nothing populates req._info const { email: voter, permissions: authPerms } = validateToken(req.headers.authorization) return await mdkClient.pushAction({ query: req.body.query, // Device query/selector action: req.body.action, // Action name from worker contract params: req.body.params, // Action parameters voter, // Current user identifier authPerms // User's permissions (e.g., ['miner:w']) }) } ``` [`lib/client.js`](/guides/gateway/plugins) builds the client once for the whole plugin, per the pattern in the plugin authoring guide. ### Manifest example (mdk-plugin.json) ```json { "name": "@yourorg/mdk-plugin-actions", "version": "1.0.0", "description": "Approval-gated write action APIs", "routes": [ { "id": "actions.push", "handler": "./controllers/push.js", "http": { "method": "POST", "path": "/auth/actions/voting", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["query", "action", "params"], "properties": { "query": { "type": "object" }, "action": { "type": "string" }, "params": { "type": "array" } } } } } } }, "description": "Submit a write action for approval", "safety": "Stage-only: does not execute until approved" } ] } ``` This manifest declares no protection, and none is applied on its behalf. The route accepts any caller until its controller validates the request, which matters more here than on a read route because it stages fleet-changing writes. [Auth and permissions](/guides/gateway/plugins#auth-and-permissions) covers the patterns. ### Mount the plugin ```javascript const { startGateway } = require('@tetherto/mdk-core') const path = require('path') await startGateway({ kernel, extraPluginDirs: [ path.join(__dirname, 'backend/plugins/actions') ] }) ``` For complete mdk-client method signatures and protocol details, see the [mdk-client README](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) and [Kernel actions integration tests](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/tests/integration/actions.test.js). ## Next steps - Understand the [approval-gated write architecture](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#actionmanager) — including how approved actions become normal command requests - Protect your routes with [controller-level auth and permission checks](/guides/gateway/plugins#auth-and-permissions) - Build routes with the [Gateway plugin format](/guides/gateway/plugins), including caching and manifest fields - 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). 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](https://nodejs.org/) >=24 (LTS) - npm 11 [(< 12)](/reference/environment) - Dependencies installed (`npm run setup` from the repo root) - 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 a dashboard: [run a mining site end to end](/tutorials/run-a-site) # 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-core') 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-client') const client = createMdkClient({ kernelKey: 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](/guides/deployment) - 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` - A `password` for each device: the CGMiner wire protocol itself does not authenticate, but the Worker's own config validation requires this field ### 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. The wire protocol itself does not authenticate, but the Avalon Worker Plugin's own config validation rejects a device with no `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 and password with your miner's values: ```js const { getKernel } = require('@tetherto/mdk-core') 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, 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-client') const client = createMdkClient({ kernelKey: 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, 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 `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](/guides/deployment) - 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 Whatsminer support ships as the external [`whatsminer-mdk-worker`](https://github.com/whatsminer/whatsminer-mdk-worker) contract plugin — a plain `mdk-contract.json` plus handler files, with no `startWhatsminerWorker` export, no provisioning store, no alerts/stats templates, and no model validation. You host it yourself on [`WorkerRuntimeV2`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/lib/worker-runtime-v2.js) through a small adapter; this repo ships two working ones to copy from ([`examples/full-site/backend/whatsminer-adapter.js`](https://github.com/tetherto/mdk/blob/main/examples/full-site/backend/whatsminer-adapter.js), [`examples/mvp-site/backend/whatsminer-adapter.js`](https://github.com/tetherto/mdk/blob/main/examples/mvp-site/backend/whatsminer-adapter.js)). ## Prerequisites Review the [common deployment prerequisites](/guides/miners#prerequisites) before you start. Deployment-specific requirements: - The [`whatsminer-mdk-worker`](https://github.com/whatsminer/whatsminer-mdk-worker) package added as a dependency (it's distributed as a git dependency, not on the npm registry — see [its own README](https://github.com/whatsminer/whatsminer-mdk-worker) for the exact version/ref to pin) - A Node.js service or script in your deployment that constructs the adapter below and registers the resulting Worker - A supported Whatsminer device reachable from the machine or container running the Worker - The miner API reachable over encrypted TCP: port `4028` for API v2 (the default) or `4433` for API v3 - The Whatsminer API password — the plugin negotiates a session token from it; there is no separate username ### Development
Run against a mock The plugin ships its own mock at `whatsminer-mdk-worker/mock/api-v3-server`. [`examples/mvp-site`](https://github.com/tetherto/mdk/blob/main/examples/mvp-site/backend/whatsminer-adapter.js) boots one mock per seed device and points the adapter's devices at them — read `startMocks` alongside `startWhatsminerWorker` in that example's `backend/site.js` for the full wiring (mock lifecycle, port assignment, device seeding all come from `config/devices.json`). There is no minimal single-file runnable example for Whatsminer anymore (the old repo-root example shipped against the retired in-repo package); the full-site and mvp-site example stacks are the reference implementation. `cd examples/mvp-site && npm run setup:example && npm start` boots the whole stack — Kernel, mocks, and the Whatsminer Worker included — against seed data in `config/devices.json`.
### Connect a miner #### 2.1 Write the adapter `whatsminer-mdk-worker` has no boot helper of its own — construct a [`WorkerRuntimeV2`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/lib/worker-runtime-v2.js) pointed at the package directory, translating your seed device list into the `{ deviceId, config }` shape it expects: ```js const path = require('path') const { WorkerRuntimeV2 } = require('@tetherto/mdk-worker') const PKG_DIR = path.dirname(require.resolve('whatsminer-mdk-worker/package.json')) async function startWhatsminerWorker (opts) { const devices = (opts.seedDevices || []).map((seed) => ({ deviceId: seed.id || seed.info?.serialNum, config: { ...seed.opts } })) const runtime = new WorkerRuntimeV2(PKG_DIR, { workerId: opts.workerId, kernelTopic: opts.kernelTopic || null, storeDir: opts.storeDir, devices }) await runtime.start() return { runtime, seeded: devices.length, stop: () => runtime.stop() } } module.exports = { startWhatsminerWorker } ``` This is the adapter [`examples/full-site/backend/whatsminer-adapter.js`](https://github.com/tetherto/mdk/blob/main/examples/full-site/backend/whatsminer-adapter.js) ships, trimmed of its input validation and debug logging — copy the real file rather than this excerpt for a production deployment. #### 2.2 Register your miner Add this to the Node.js service or script that runs your Worker. 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-core') const { startWhatsminerWorker } = require('./whatsminer-adapter') const kernel = await getKernel() const worker = await startWhatsminerWorker({ workerId: 'whatsminer-rack-1', storeDir: './store/whatsminer-rack-1', seedDevices: [{ info: { serialNum: 'WM-001' }, opts: { address: '192.168.1.10', port: 4028, 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. The device list is fixed at construction — there is no `registerThing`/`updateThing`/`forgetThings` equivalent. Adding, updating, or removing a device means editing `seedDevices` and restarting the Worker. The [`whatsminer-mdk-worker`](https://github.com/whatsminer/whatsminer-mdk-worker) README documents the plugin's own `mdk-contract.json`, supported models, and connection options in full; the shared [install pattern](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/install-pattern.md) covers the broader deployment mechanics.
## Troubleshooting There is no minimal single-file development example to check readiness output against — the reference deployment is [`examples/mvp-site`](https://github.com/tetherto/mdk/blob/main/examples/mvp-site/backend/whatsminer-adapter.js)'s full stack (`npm start` prints `MDK_READY worker devices=` once the Whatsminer Worker is up). If the Worker does not come up, or a mock port is already in use, follow [miner troubleshooting](/guides/miners/troubleshooting). ## Next steps - Understand the [deployment topologies](/guides/deployment) for running the Worker service - Review [`whatsminer-mdk-worker`](https://github.com/whatsminer/whatsminer-mdk-worker)'s own `mdk-contract.json` for telemetry units, command shapes, and error codes — it isn't vendored into this repo # Troubleshoot miner Workers (/guides/miners/troubleshooting) ## Overview This page covers the mock/development examples used by the [Antminer](/guides/miners/run-antminer-worker), [Whatsminer](/guides/miners/run-whatsminer-worker), and [Avalon](/guides/miners/run-avalon-worker) miner guides. The examples start a bundled mock miner, start a Kernel, register one device, print the identifiers you need, and keep running until you stop them. ## Expected output A working example prints a 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) - 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. Whatsminer follows a different shape — it's the external [`whatsminer-mdk-worker`](https://github.com/whatsminer/whatsminer-mdk-worker) plugin, not an in-repo package with per-model `examples/run-*.js` files. Its mock port comes from whatever deployment config seeds it; [`examples/mvp-site`](/guides/miners/run-whatsminer-worker)'s `config/devices.json` is the reference. 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, port `4028` for API v2 (the default) or `4433` for API v3 (auto-detected from the port), 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. ## Whatsminer example installs an unexpected dependency [`whatsminer-mdk-worker`](https://github.com/whatsminer/whatsminer-mdk-worker) is a git dependency, not a versioned npm package, and its `package.json` entry carries no pinned commit. Only the committed `package-lock.json` resolves it to a specific commit, so a plain `npm install` with a missing or discarded lockfile, or with a lockfile-regenerating command like `npm update`, can silently pull a different commit than the one already tested. If `npm install` changes `package-lock.json` unexpectedly, or `git diff` shows the `whatsminer-mdk-worker` entry's `resolved` commit changed, reinstall from the committed lockfile instead. `examples/mvp-site` is an npm workspace, not a standalone project — it has no lockfile of its own, so run this from the repository root: ```bash npm ci ``` `npm ci` fails on a lockfile/`package.json` mismatch instead of drifting. Separately, `npm ls crypto-js --prefix examples/mvp-site` or an `npm audit` run may show `crypto-js` resolving to `mdk-crypto-lib`, a local file dependency, rather than the real npm package. This is expected: `whatsminer-mdk-worker` still declares a dependency on the deprecated `crypto-js`, and MDK overrides it at install time with a drop-in replacement built on Node's own `node:crypto`. It is not a broken install. ## 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`](/reference/worker). ## 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 # Site security blueprint (/guides/security) MDK ships no user identity at any tier. The Gateway serves every plugin route to any caller, Kernel inspects no human identity, and `WorkerRuntime` applies no caller allowlist. Identity, allowlists, TLS, and network policy are part of the site you build, not defaults the SDK turns on. Treat `mvp-site`, `full-site`, and per-family snippets as boot demos, not a production template. ## Overview This page is a blueprint for securing an enterprise site you assemble from MDK: UI, Gateway, Kernel, and Workers on separate hosts or containers. Walk the steps in order. Each step names what to do, the options MDK actually gives you, and a suggested default. Use it with the [security boundaries](/concepts/security-boundaries) concept (what each layer trusts) and the control plane (how a request travels). MDK leaves these controls to you on purpose. The SDK coordinates devices; your site owns who may talk to it. ## Constraints the SDK does not lift Build around these. They are current `0.y.z` behavior, not optional extras. | Layer | What MDK does | What you own | | ------- | -------------------------------------------------------------------------------------- | ------------------ | | UI | Token attachment if you pass an `AuthProvider`; no route guards ship with the toolkit | Session, HTTPS, `apiBaseUrl`, hiding write controls | | Gateway | Plugin host over HTTP. Manifest `"auth"` / `"permissions"` have no reader. Bundled `@tetherto/mdk-plugin-auth` is not wired. | Identity checks in every controller | | Kernel | Encrypted HRPC. Optional caller-key allowlist (`auth.whitelist`, default empty). `authPerms` only on approval-gated writes, taken from the caller. | Allowlist, mapping verified roles to `authPerms` / `voter` | | Workers | Noise transport and key-addressed HRPC. No caller allowlist. No actor in handler context. | Network isolation, secrets, payload checks, audit upstream | | MCP / agent | `POST /mcp` on `127.0.0.1` with no user auth. Agent plugin binds to `local` without an identity layer. | Bind policy, human approval for writes, real `userId` | | Devices / pools | Vendor protocol APIs and authentication | Device-network isolation | Consumers enter through the Gateway; AI agents enter through the standalone MCP server. The browser never holds a Kernel key. Direct Kernel or Worker reachability is a backend-network concern, not a product feature. ## Blueprint ### Step 1: Choose a network layout Pick a deployment topology before you write auth code. Isolation is the first control; identity sits on top of it. #### Options | Option | When to pick it | Trade-off | | ---------------------- | -------------------------------------- | -------------------------------------------------------- | | Single process | Local demos, tests, smallest footprint | One heap. A compromise of the process is the whole site. | | Local (one OS process per service, shared directory) | One machine, production-like restarts, or a step toward microservices | Stronger isolation than a single heap. Kernel and Workers still share the host. No DHT. | | Microservices over DHT | Enterprise sites: separate hosts or containers, resource limits per service, Workers away from Kernel | Peers that know the discovery topic can find Kernel and Workers. Key distribution and a Kernel allowlist are mandatory. | Suggested default: microservices (one process or container per service, discovery over DHT), with three networks you define: - Operator: UI origin and Gateway HTTP, behind TLS and a reverse proxy you control - Backend: Kernel and Workers on separate hosts, no inbound path from browsers or agents - Devices: miner, container, meter, and pool APIs, reachable only from the Workers that own them Pass the Kernel public key to a remote Gateway as `kernelKey`. Treat the DHT discovery topic as deployment configuration, not a credential: generate a site-specific topic, distribute it out of band, and do not reuse example or well-known values. Pair this shape with a non-empty Kernel allowlist in the next step. Without that allowlist, DHT makes Kernel reachable to any peer that has the topic and the listener key. Do not pass `@tetherto/mdk-client` or Kernel keys into the browser. HTTP to the Gateway is the only operator path. ### Step 2: Admit the Gateway to Kernel Pre v1.0, [`auth.whitelist`](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#api) defaults to `[]` and admits any HRPC caller that knows Kernel's public key. A production Kernel always has a non-empty allowlist. #### Options | Option | When to pick it | Trade-off | | --------------- | ---------------------- | ------------------------------------------------------------------------------------------------ | | Empty allowlist | Local development only | Anyone with the Kernel public key can read telemetry and dispatch commands, skipping the Gateway | | Allowlist the Gateway DHT public key | Every real site | The key is stable across restarts (persisted seed); guard it like a production credential | | Allowlist Gateway plus extra backend clients | A second `mdk-client` service you trust (batch jobs, a second Gateway, the standalone MCP server) | Each extra key is a full Kernel principal. Review it like a production credential. | Suggested default: one persistent Gateway key pair, that hex public key only, on Kernel: ```js const kernel = await getKernel({ hrpc: { whitelist: [''] } }) ``` The [auth allowlist example](https://github.com/tetherto/mdk/blob/main/examples/backend/kernel/auth-whitelist.js) shows the exchange. Generate the Gateway key once, store it with the same care as a TLS key, and put it on the allowlist. The Gateway derives its key from a persisted seed, so it stays stable across restarts — no need to re-check after a bounce. The standalone MCP server (`mdk run mcp`) is a separate Kernel caller with its own persistent key. Add it to `auth.whitelist` alongside the Gateway, or its tools can't reach Kernel on a site with a non-empty allowlist. On separate hosts, pass Kernel's public key to the Gateway as `kernelKey`. Do not rely on the well-known key file (`/mdk/.kernel-key`) across machines. Where that file still exists on the Kernel host, keep it mode `0600` on a directory you own; it is not deleted on shutdown. Do not expose Kernel's HRPC listener off the backend network. ### Step 3: Isolate Workers and devices [`WorkerRuntime`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/README.md) does not enforce a caller allowlist. Any backend peer that can reach a Worker and address its public key may send [`command.request`](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#commanddispatcher). Kernel's allowlist does not protect a Worker that is dialed directly. [Security boundaries](/concepts/security-boundaries) give the trust-boundary rationale behind the controls below. #### Options | Option | When to pick it | Trade-off | | ---------------------------------------------------- | --------------- | --------------------------------------------------------------------------- | | Workers only on the Kernel network, keys unpublished | Every site | Operational discipline: no Worker port or DHT topic on the operator network | | One Worker process per trust zone | Different device families or vendors must not share credentials | More processes to supervise | | Device APIs on a third network (CGMiner `4028`, miner HTTP, container APIs) | Real hardware | Required when the vendor protocol has weak or no auth (Avalon / CGMiner have none; Antminer uses digest auth; Whatsminer uses a device password) | Suggested default: persist each Worker's `storeDir` so its key is stable, treat that key as a machine identity, and register the Worker with Kernel only after the device network is isolated. On DHT, keep Worker public keys and the discovery topic off the operator network and out of git. Inject device passwords and pool API keys from a secret manager or protected environment. Never put secrets in `mdk-contract.json`, git, or a committed seed file. Validate command payloads again in the handler (types, ranges, allowed targets). Kernel checks the command name against the contract; it does not know your hardware's unsafe parameter combinations. Redact credentials and vendor responses in handler errors and logs. For CGMiner-style devices, network isolation is the control: changing the Worker `password` field does not add a wire handshake the protocol lacks. ### Step 4: Put identity in Gateway controllers The Gateway is the only supported user-auth seam. Every [bundled plugin](/reference/supported-plugins#bundled-site-plugins) is served to any caller. Paths under `/auth/metrics/*` are historical names, not a login gate. Manifest `"auth": true` changes nothing. Do not mount `@tetherto/mdk-plugin-auth` expecting it to work. The Gateway does not register it, and its controllers still expect `ctx.authLib`, a `services` argument, and `req._info.user`, which the current host does not provide. #### Options | Option | When to pick it | Trade-off | | --------------------------------------- | --------------- | ------------------------------------- | | Your SSO (OIDC / SAML / existing IdP), validated in each controller | Production sites with an identity provider | You write the plugin: this is the supported path | | Gateway-issued JWT after an OAuth redirect you implement (`/oauth/...` → `?authToken=`, plus `POST /auth/token` and `/auth/userinfo`) | Browser UI using `gatewayRedirectAuth({ oauthBaseUrl })` | You still write those routes. The bundled auth plugin is not a substitute. | | Bearer tokens only (`Authorization: Bearer`), no browser redirect | Service accounts, scripts, or a UI that already has a token | Pair with `bearerTokenAuth()` in the UI | | No identity (`noAuth()`, or nothing in the controller) | Laptop demos bound to loopback | Anyone who can reach the port has the fleet | Suggested default: one identity helper shared by every controller. Map verified roles onto Kernel `authPerms` (`miner:w`, `container:w`, and any others you define). Never accept `authPerms` or `voter` from the HTTP client. Reject missing tokens with `401` and missing perms with `403`, setting `err.statusCode` so the Gateway does not collapse them to `400`. ```js // The controller does the check; the Gateway plugins guide (linked below) owns the full example. if (!token) throw Object.assign(new Error('ERR_UNAUTHORIZED'), { statusCode: 401 }) if (!permissions.includes('miner:w')) throw Object.assign(new Error('ERR_FORBIDDEN'), { statusCode: 403 }) ``` The Gateway returns one error body, `{ statusCode, error, message }`, and sanitizes `message`: only `ERR_*` codes and Fastify's own 4xx errors (validation, content type, body size) reach the client verbatim. Any other message is replaced with the generic status text and the real error is logged, so an internal failure never surfaces its detail to the caller. The [Gateway plugins guide](/guides/gateway/plugins#auth-and-permissions) owns this pattern. Also: - Bind HTTP to the proxy network, not a public interface without that proxy - Rate-limit writes. The Gateway honors `?overwriteCache=true` only on a request that carries an `Authorization` header, but it cannot validate that header: on a route whose controller checks no token, any caller who sends one still bypasses the cache - The HTTP worker sets `trustProxy: true`. Only do that behind a proxy whose hop count you trust, or client IPs can be spoofed - Log actor, route, target, command, outcome, and a correlation id for every write. Redact secrets. Worker handlers cannot see actor identity, so this audit stays in the Gateway ### Step 5: Authorize writes Kernel treats two write paths differently. Choose per action, then enforce the choice in the Gateway. Direct `command.request` has **no** Kernel-side permission check. Approval-gated writes read `authPerms`, but that array is caller-supplied once the HRPC connection is accepted. #### Options | Option | When to pick it | Trade-off | | ------------------------------------- | --------------- | --------------------------------------- | | Direct command (`command.request`) after a Gateway perm check | Low-impact, operator-in-front actions (LED, a single-device reboot you already authorized) | Fast. Kernel does not second-guess the caller. A missing controller check is full control. | | Approval-gated write ([`action.push`](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#actionmanager), then votes) | Fleet-changing actions: power mode, pool assignment, container PDU, bulk reboot | Slower. Still requires the Gateway to set `authPerms` and `voter` from the verified token. The [write-actions guide](/guides/gateway/write-actions) covers the HTTP side. | | Agent-proposed write with a human approval pause | Operator agent (`approvalTimeoutMs` on `@tetherto/mdk-plugin-agent`) | The model cannot execute a write until someone approves. Without identity, every request is the same `local` operator. | Suggested default: approval-gated writes for anything that changes hash, power, or network membership; direct commands only behind a controller that already checked a token. Do not expose a raw `sendCommand` route to the UI without that check. ### Step 6: Wire the UI session [``](https://github.com/tetherto/mdk/blob/main/ui/packages/react-adapter/README.md#authentication) is how the browser attaches a credential. The toolkit does not guard routes or deny writes on its own. #### Options | Option | When to pick it | Trade-off | | --------------------------------------- | --------------- | ------------------------------------- | | `noAuth()` | Fixture-backed demos, open APIs on loopback | No token. A finished-looking dashboard still sends unauthenticated calls. | | `bearerTokenAuth()` | You already issue JWTs from the Gateway identity plugin | Session ends on `401`. You own login UI. | | `gatewayRedirectAuth({ oauthBaseUrl })` | OAuth redirect, `?authToken=` capture, refresh against `POST /auth/token` | Needs *your* token routes. Default `MdkProvider` auth without `oauthBaseUrl` cannot sign in. | | Custom `AuthProvider` | A session model the three presets do not cover | You implement `getToken`, refresh, and sign-out | Suggested default for a production dashboard: - `` or `gatewayRedirectAuth({ oauthBaseUrl })` after those routes exist - HTTPS, a real `apiBaseUrl`, no Vite dev proxy - One fetch client so pages cannot skip `Authorization` - Route guards on any view that can mutate the fleet (the shell template uses ``; copy that idea, not `noAuth()`) - Hide write controls unless the token carries the matching permission. Hiding is not authorization; the Gateway still denies - Scrub `?authToken=` from the URL after capture. Tokens in query strings leak through access logs and Referer - Static bundle behind the same proxy that terminates TLS. Do not run `vite dev` as the production UI The full-site example UI has no sign-in, no session, and a hardcoded profile. Do not copy that into a reachable host. ### Step 7: (Optional) MCP and the operator agent MCP is optional. When you enable it, it has the same authority as the process that hosts it. The standalone `@tetherto/mdk-mcp` process binds `127.0.0.1` and answers `POST /mcp` with no user auth. The Gateway hosts no MCP itself. #### Options | Option | When to pick it | Trade-off | | ---------------- | --------------------------- | ------------------------------------------------- | | No MCP, no agent | Dashboards and scripts only | Smallest surface | | MCP on loopback, operator agent on the Gateway | In-process chat for people who already passed Gateway identity | Keep `approvalTimeoutMs` on. Pass a real `userId` into `createSession` / `resumeSession`. A session id in a URL must not read another operator's chat | | MCP or agent published past localhost | An agent runtime on another host | MDK does not authenticate this path. Put mutual TLS or your SSO proxy in front, and authorize inside the reused HTTP handlers (a Gateway plugin's routes served as MCP tools are those same handlers, with no extra check) | Suggested default: leave MCP on `127.0.0.1`. Do not publish it with compose `ports:`, an SSH tunnel, or a reverse proxy unless that front door is authenticated. Treat the model provider (`qvac` or an OpenAI-compatible URL) as sensitive infrastructure: prompts can include site topology and, if you are careless, secrets. ### Step 8: Handle secrets, stores, and day-two operations #### Options for secrets | Option | When to pick it | Trade-off | | -------------------- | --------------- | -------------------------------------------------------- | | Secret manager (Vault, cloud SM, Kubernetes secrets) into the host process | Production | Extra moving part. This is the suggested default. | | Protected environment on a locked-down host | Small single-host sites | Rotation and audit are your scripts | | Values in git, `mdk-contract.json`, example `site.deploy.json`, or the UI bundle | Never | Credentials leak with the repo or the browser | #### Options for process lifecycle | Option | When to pick it | Trade-off | | ----------------------------------------- | --------------- | ----------------------------------- | | `SIGINT` / supervisor stop so WAL and cleanup run | Always | A `SIGKILL` can leave `.mdk-data` / `storeDir` unusable. The next boot may look healthy with zero Workers. | | Health check HTTP only (`GET /auth/site`) | Insufficient | The Gateway can keep serving HTTP after the Kernel channel closes (`CHANNEL_CLOSED`) and never reconnect | Suggested default: - Mode `0600` (or equivalent) on Kernel db, Worker `storeDir`, and key files. Encrypt at rest if the host requires it - Back up those directories as operational data, with the same access control as the live files - Rotate Kernel, Gateway, and Worker keys when staff leave or a host is rebuilt, and update the allowlist in the same change - Practice recovery from a wiped data directory before an incident - Watch Gateway-to-Kernel errors, not only the HTTP port - Run Node.js >=24. Pin lockfiles. Prefer non-root containers with a read-only root filesystem - Stay on the latest `main` or latest tag you have reviewed. `0.y.z` is initial development; older tags may not receive security fixes. Report product issues through the [security policy](https://github.com/tetherto/mdk/blob/main/SECURITY.md), not public GitHub issues ## Decision cheat sheet | Decision | Suggested production choice | Demo-only choice | | ---------------- | ------------------------------ | --------------------------------------------- | | Topology | Microservices over DHT, three networks (operator / backend / devices) | Single process on loopback | | Kernel admission | Non-empty `auth.whitelist` with persistent Gateway and MCP keys | Empty allowlist | | Worker access | Backend network only, keys unpublished | Same host, unpublished anyway | | User identity | Your SSO or JWTs, checked in every Gateway controller | No check, `noAuth()` | | Writes | Approval-gated for fleet changes; Gateway sets `authPerms` | Direct `sendCommand` with no token | | UI | HTTPS, `bearerTokenAuth` or working `gatewayRedirectAuth`, route guards | Vite proxy, `noAuth()`, HashRouter | | MCP / agent | Loopback, human approval, real `userId` | Disabled, or loopback with `local` operator | | Secrets | Secret manager into the process | Example `admin` passwords | ## Next steps - Read the [security boundaries](/concepts/security-boundaries): what each layer trusts, and what it does not - Follow the control plane: read path, direct command, and approval-gated write - Add identity in [Gateway plugin controllers](/guides/gateway/plugins#auth-and-permissions): the only supported user-auth seam - Restrict Kernel callers with the [HRPC allowlist](https://github.com/tetherto/mdk/blob/main/examples/backend/kernel/auth-whitelist.js): transport admission for the Gateway - Compare deployment topologies: this blueprint assumes microservices; local and single-process are smaller footprints - Report product vulnerabilities through the [security policy](https://github.com/tetherto/mdk/blob/main/SECURITY.md): private advisory, not a public issue # UI guides (/guides/ui) } title="Install and wire the React packages" href="/guides/ui/install" description="Install the three MDK React packages, wrap your app in MdkProvider, and wire headless stores" /> } 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" /> } title="Agent skills" href="/guides/ui/agent-skills" description="Install the MDK Developer Skill suite so your coding agent knows MDK conventions and component props" /> # Agent skills (/guides/ui/agent-skills) ## Overview The **MDK Developer Skill suite** (`@tetherto/mdk-skill`) is how an AI coding agent learns to build with MDK. It ships procedural context in the universal Agent Skills format (`SKILL.md`), so any skills-aware agent (Cursor, Claude Code, and others) becomes fluent in MDK conventions as soon as the suite is installed. Nothing here calls a network or a model. Each skill is a file the agent reads, bundled with the component registry MDK builds from its own source. Install once, then let the agent do the rest. ## Prerequisites - `mdk` on your PATH (see [Install the CLI](/guides/cli/install)) ## Install From your project root: ```bash mdk skill add # Adds both Cursor and Claude Code ```
Optional flags Use `--client` to target a named agent, or `--dir` to install into another directory, for example: - `mdk skill add --client cursor` # .cursor/skills/ - `mdk skill add --client claude` # .claude/skills/ - `mdk skill add --dir path/to/app` # target another directory The [CLI reference](https://github.com/tetherto/mdk/blob/main/packages/cli/README.md#command-surface) lists the options. `mdk onboard` also offers the install as one of its steps.
Skills land flat: one directory per skill, each with its own `SKILL.md`. Agents discover them automatically, so there is no rule file to wire by hand. ## What the suite covers The suite ships a router plus workflows for device workers, Gateway plugins, UI pages, and stack deployment. Each skill's `description` frontmatter is its routing trigger, so you state an intent in plain language and the right skill activates; composite prompts route as an ordered chain through the router. See the [skill suite](https://github.com/tetherto/mdk/blob/main/packages/mdk-skill/README.md) for the current inventory and each skill's triggers, and the [routing prompts](https://github.com/tetherto/mdk/blob/main/packages/mdk-skill/README.md#try-it--routing-prompts) for worked examples. ## What the UI skill knows Use the [`mdk-ui-component`](https://github.com/tetherto/mdk/blob/main/packages/mdk-skill/src/skills/mdk-ui-component/SKILL.md) skill to build dashboards. Alongside its [workflow](https://github.com/tetherto/mdk/blob/main/packages/mdk-skill/src/skills/mdk-ui-component/SKILL.md#workflow) it carries: - The React Devkit's **generated component registry**, listing every [agent-ready component](/reference/ui/components) with its exact prop names and types, so an agent can't invent props that do not exist. It's generated from the devkit's source on every release and never hand-written. - The [page recipe](https://github.com/tetherto/mdk/blob/main/packages/mdk-skill/src/skills/mdk-ui-component/references/page-recipe.md): the layer-by-layer contract a page follows. A page is composition only: data comes from a Gateway route, a hook shapes the payload, the page is thin glue that calls the hook, and the visuals come from the Devkit. The [page recipe](https://github.com/tetherto/mdk/blob/main/packages/mdk-skill/src/skills/mdk-ui-component/references/page-recipe.md) spells out each layer's responsibilities. ## Verify the install Open the project in a skills-aware agent and state one of the intents above. The agent should name the skill it activated. If nothing activates, confirm the skill files landed in your project (for example under `.cursor/skills/` or `.claude/skills/`). Those directories are gitignored by default, so the suite is installed per developer, not committed. ## Next steps - [Build a dashboard](/tutorials/build-a-dashboard): put the skills to work on a real page - See [the end-to-end flow the skill follows](https://github.com/tetherto/mdk/blob/main/packages/mdk-skill/src/skills/mdk-ui-component/SKILL.md#workflow) - Review [each layer's responsibilities](https://github.com/tetherto/mdk/blob/main/packages/mdk-skill/src/skills/mdk-ui-component/references/page-recipe.md) - Browse the [components](/reference/ui/components) the agent composes # Install and wire the React packages (/guides/ui/install) This page walks through the minimum integration of the MDK UI toolkit into a React application. Reference and component pages link here as their shared installation prerequisite. ## Prerequisites - **Node.js** >=24 - **npm** >=11 - **React** 19+ and **react-dom** 19+ ## Install ```bash # Clone the MDK UI monorepo (adjust the URL to your fork if needed) git clone https://github.com/tetherto/mdk.git cd mdk/ui # Install dependencies and build packages (npm workspaces) npm install npm run build ``` Then add to your app's `package.json`: ```json { "dependencies": { "@tetherto/mdk-react-devkit": "*", "@tetherto/mdk-react-adapter": "*", "@tetherto/mdk-ui-foundation": "*" } } ``` [Wrap your app](/guides/ui/install#wrap-your-app-in-mdkprovider) 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( , ) ``` `apiBaseUrl` must point at a Gateway that mounts the routes your pages read. From 0.6.0 the Gateway serves only what its plugins provide, so the endpoints behind these hooks come from your own plugin. [Adding custom plugins to the Gateway HTTP API](/guides/gateway/plugins) covers that side. ## 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; } } ``` ## Next steps - Browse the [state, component, and utility hooks](/reference/ui/hooks): what each hook selects and re-renders on - Browse the [component reference](/reference/ui/components): building blocks and mining-domain components, with props and usage # 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" /> } title="Compose spare parts inventory flows" href="/guides/ui/react/compose-spare-parts-inventory-flows" description="Wire up add, move, bulk-import, and delete flows for spare parts using the dialog components" /> # Compose reporting layouts (/guides/ui/react/compose-reporting-layouts) @tetherto/mdk-react-devkit/foundation The reporting composites — [`Cost`](/reference/ui/components/dashboards#cost), [`Ebitda`](/reference/ui/components/charts#ebitda), [`EnergyBalance`](/reference/ui/components/charts#energybalance), [`HashBalance`](/reference/ui/components/dashboards#hashbalance), and [`Hashrate`](/reference/ui/components/charts#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/components/filters#timeframecontrols), shared across the financial reporting surfaces. | Component | Description | | --- | --- | | [`TimeframeWeekFlatContent`](/guides/ui/react/compose-reporting-layouts/#timeframeweekflatcontent) | Flat week-list for the week selector | | [`TimeframeWeekTreeContent`](/guides/ui/react/compose-reporting-layouts/#timeframeweektreecontent) | Year-month-week tree for the week selector | ### `TimeframeWeekFlatContent` Flat list of selectable week items for the TimeframeControls week selector. Shared building block of the reporting timeframe controls. ```tsx ``` Renders inside the week selector of [`TimeframeControls`](/reference/ui/components/filters#timeframecontrols). ### `TimeframeWeekTreeContent` Hierarchical year to month to week tree for the TimeframeControls week selector. Shared building block of the reporting timeframe controls. ```tsx ``` Renders inside the week selector of [`TimeframeControls`](/reference/ui/components/filters#timeframecontrols). # Compose Cost layouts (/guides/ui/react/compose-reporting-layouts/cost) @tetherto/mdk-react-devkit/foundation The [`Cost`](/reference/ui/components/dashboards#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 | Component | Description | | --- | --- | | [`CostContent`](/guides/ui/react/compose-reporting-layouts/cost/#costcontent) | Data-driven 2x2 grid body of the Cost page | | [`CostMetrics`](/guides/ui/react/compose-reporting-layouts/cost/#costmetrics) | Three \$/MWh cost-summary tiles (all-in, energy, operations) | | [`AvgAllInCostChart`](/guides/ui/react/compose-reporting-layouts/cost/#avgallincostchart) | Revenue vs cost (\$/MWh) bar chart over time | | [`ProductionCostChart`](/guides/ui/react/compose-reporting-layouts/cost/#productioncostchart) | Production cost over time, overlaid with BTC price | | [`OperationsEnergyChart`](/guides/ui/react/compose-reporting-layouts/cost/#operationsenergychart) | Doughnut breakdown of operations vs energy cost | ### `CostContent` Renders the data-driven portion of the Cost page in a 2x2 Mosaic grid (production cost chart, operations energy chart, avg all-in cost chart, and cost metric tiles). Building block of the Cost composite. ```tsx ``` Renders inside the [`Cost`](/reference/ui/components/dashboards#cost) composite. ### `CostMetrics` Three \$/MWh tiles that summarise the cost-summary period. Order mirrors the OSS Cost page: All-in (highlighted), Energy, Operations. Building block of the Cost composite. ```tsx ``` Renders inside the [`Cost`](/reference/ui/components/dashboards#cost) composite. ### `AvgAllInCostChart` Avg All-in Cost - revenue vs cost (\$/MWh) bar chart over time. Building block of the Cost composite. ```tsx ``` Renders inside the [`Cost`](/reference/ui/components/dashboards#cost) composite. ### `ProductionCostChart` Production cost over time, overlaid with BTC price. Building block of the Cost composite. ```tsx ``` Renders inside the [`Cost`](/reference/ui/components/dashboards#cost) composite. ### `OperationsEnergyChart` Doughnut breakdown of Operations vs Energy cost (in USD totals). Building block of the Cost composite. ```tsx ``` Renders inside the [`Cost`](/reference/ui/components/dashboards#cost) composite. # Compose EBITDA layouts (/guides/ui/react/compose-reporting-layouts/ebitda) @tetherto/mdk-react-devkit/foundation The [`Ebitda`](/reference/ui/components/charts#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 | Component | Description | | --- | --- | | [`EbitdaMetrics`](/guides/ui/react/compose-reporting-layouts/ebitda/#ebitdametrics) | Top row of EBITDA summary metric cards | | [`EbitdaCharts`](/guides/ui/react/compose-reporting-layouts/ebitda/#ebitdacharts) | Revenue, cost, and EBITDA chart panel | | [`ActualEbitdaCard`](/guides/ui/react/compose-reporting-layouts/ebitda/#actualebitdacard) | Realised EBITDA stat card vs prior period | | [`EbitdaHodlCard`](/guides/ui/react/compose-reporting-layouts/ebitda/#ebitdahodlcard) | Projected EBITDA if all BTC is held | | [`EbitdaSellingCard`](/guides/ui/react/compose-reporting-layouts/ebitda/#ebitdasellingcard) | Projected EBITDA if all BTC is sold | | [`MonthlyEbitdaChart`](/guides/ui/react/compose-reporting-layouts/ebitda/#monthlyebitdachart) | EBITDA-by-month trend bar chart | | [`BitcoinPriceCard`](/guides/ui/react/compose-reporting-layouts/ebitda/#bitcoinpricecard) | BTC reference-price stat card | | [`BitcoinProducedCard`](/guides/ui/react/compose-reporting-layouts/ebitda/#bitcoinproducedcard) | Bitcoin-produced stat card with prior-period delta | | [`BitcoinProducedChart`](/guides/ui/react/compose-reporting-layouts/ebitda/#bitcoinproducedchart) | Daily bitcoin-produced time-series chart | | [`BitcoinProductionCostCard`](/guides/ui/react/compose-reporting-layouts/ebitda/#bitcoinproductioncostcard) | Avg USD cost to produce one bitcoin | ### `EbitdaMetrics` Row of summary metric cards across the top of the EBITDA section (actual, hodl, selling, cost). Building block of the `Ebitda` composite. ```tsx ``` Renders inside the [`Ebitda`](/reference/ui/components/charts#ebitda) composite. ### `EbitdaCharts` Chart panel inside the EBITDA section visualising revenue, cost, and EBITDA over time. Building block of the `Ebitda` composite. ```tsx ``` Renders inside the [`Ebitda`](/reference/ui/components/charts#ebitda) composite. ### `ActualEbitdaCard` Stat card summarising the realised EBITDA for the selected reporting window vs the prior period. Building block of the `Ebitda` composite. ```tsx ``` Renders inside the [`EbitdaMetrics`](#ebitdametrics) row. ### `EbitdaHodlCard` Stat card projecting EBITDA assuming all produced bitcoin is held instead of sold. Building block of the `Ebitda` composite. ```tsx ``` Renders inside the [`EbitdaMetrics`](#ebitdametrics) row. ### `EbitdaSellingCard` Stat card projecting EBITDA assuming all produced bitcoin is sold at the daily reference price. Building block of the `Ebitda` composite. ```tsx ``` Renders inside the [`EbitdaMetrics`](#ebitdametrics) row. ### `MonthlyEbitdaChart` Bar chart comparing EBITDA across the most recent months for trend visualisation. Building block of the `Ebitda` composite. ```tsx ``` Renders inside the [`EbitdaCharts`](#ebitdacharts) panel. ### `BitcoinPriceCard` Stat card showing the BTC reference price used by the reporting view with currency and timestamp. Building block of the `Ebitda` composite. ```tsx ``` Renders inside the [`Ebitda`](/reference/ui/components/charts#ebitda) composite. ### `BitcoinProducedCard` Stat card summarising the bitcoin produced during the reporting window with delta to prior period. Building block of the `Ebitda` composite. ```tsx ``` Renders inside the [`Ebitda`](/reference/ui/components/charts#ebitda) composite. ### `BitcoinProducedChart` Time-series chart of bitcoin produced per day across the selected reporting window. Building block of the `Ebitda` composite. ```tsx ``` Renders inside the [`Ebitda`](/reference/ui/components/charts#ebitda) composite. ### `BitcoinProductionCostCard` Stat card showing the average cost in USD to produce one bitcoin during the reporting window. Building block of the `Ebitda` composite. ```tsx ``` Renders inside the [`Ebitda`](/reference/ui/components/charts#ebitda) composite. # Compose Energy balance layouts (/guides/ui/react/compose-reporting-layouts/energy-balance) @tetherto/mdk-react-devkit/foundation The [`EnergyBalance`](/reference/ui/components/charts#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 | Component | Description | | --- | --- | | [`EnergyBalanceRevenueCharts`](/guides/ui/react/compose-reporting-layouts/energy-balance/#energybalancerevenuecharts) | Energy revenue tab chart mosaic | | [`EnergyBalanceRevenueMetrics`](/guides/ui/react/compose-reporting-layouts/energy-balance/#energybalancerevenuemetrics) | Energy revenue stat-card grid | | [`EnergyBalanceCostCharts`](/guides/ui/react/compose-reporting-layouts/energy-balance/#energybalancecostcharts) | Energy cost tab chart layout | | [`EnergyBalanceCostMetrics`](/guides/ui/react/compose-reporting-layouts/energy-balance/#energybalancecostmetrics) | Energy cost stat-card grid | | [`EnergyBalancePowerChart`](/guides/ui/react/compose-reporting-layouts/energy-balance/#energybalancepowerchart) | Power-vs-threshold line chart | | [`EnergyRevenueChart`](/guides/ui/react/compose-reporting-layouts/energy-balance/#energyrevenuechart) | Energy revenue per MWh bar chart | | [`EnergyCostChart`](/guides/ui/react/compose-reporting-layouts/energy-balance/#energycostchart) | Revenue vs cost per MWh bar chart | | [`EnergyMetricCard`](/guides/ui/react/compose-reporting-layouts/energy-balance/#energymetriccard) | Single energy-balance metric stat card | ### `EnergyBalanceRevenueCharts` Mosaic layout of revenue, downtime, and power charts for the energy balance revenue tab. Building block of the EnergyBalance composite. ```tsx ``` Renders inside the revenue tab of [`EnergyBalance`](/reference/ui/components/charts#energybalance). ### `EnergyBalanceRevenueMetrics` Grid of stat cards summarising energy revenue metrics for the selected period. Building block of the EnergyBalance composite. ```tsx ``` Renders inside the revenue tab of [`EnergyBalance`](/reference/ui/components/charts#energybalance). ### `EnergyBalanceCostCharts` Layout container for the energy cost tab charts: revenue-vs-cost bar chart and power line chart. Building block of the EnergyBalance composite. ```tsx ``` Renders inside the cost tab of [`EnergyBalance`](/reference/ui/components/charts#energybalance). ### `EnergyBalanceCostMetrics` Grid of stat cards summarising energy cost metrics for the selected period. Building block of the EnergyBalance composite. ```tsx ``` Renders inside the cost tab of [`EnergyBalance`](/reference/ui/components/charts#energybalance). ### `EnergyBalancePowerChart` Line chart visualising power consumption against threshold for the energy balance view. Building block of the EnergyBalance composite. ```tsx ``` Renders inside both tabs of [`EnergyBalance`](/reference/ui/components/charts#energybalance). ### `EnergyRevenueChart` Bar chart showing site energy revenue per MWh, with USD/BTC currency toggle. Building block of the EnergyBalance composite. ```tsx ``` Renders inside [`EnergyBalanceRevenueCharts`](#energybalancerevenuecharts). ### `EnergyCostChart` Bar chart comparing site revenue vs cost per MWh, with USD/BTC currency toggle. Building block of the EnergyBalance composite. ```tsx ``` Renders inside [`EnergyBalanceCostCharts`](#energybalancecostcharts). ### `EnergyMetricCard` Stat card for a single energy balance metric. Building block of the EnergyBalance composite. ```tsx ``` Renders inside the energy-balance metric grids. # Compose Hash balance layouts (/guides/ui/react/compose-reporting-layouts/hash-balance) @tetherto/mdk-react-devkit/foundation The [`HashBalance`](/reference/ui/components/dashboards#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 | Component | Description | | --- | --- | | [`HashBalanceRevenuePanel`](/guides/ui/react/compose-reporting-layouts/hash-balance/#hashbalancerevenuepanel) | Hash balance revenue tab panel | | [`HashBalanceCostPanel`](/guides/ui/react/compose-reporting-layouts/hash-balance/#hashbalancecostpanel) | Hash balance cost tab panel | ### `HashBalanceRevenuePanel` Revenue tab panel for hash balance - site hash revenue, network hashrate, hashprice charts, and currency toggle for per-PH/day units. Building block of the HashBalance composite. ```tsx ``` Renders inside the revenue tab of [`HashBalance`](/reference/ui/components/dashboards#hashbalance). ### `HashBalanceCostPanel` Cost tab panel for hash balance - metric tiles and combined cost / revenue / network hashprice bar chart for the selected period. Building block of the HashBalance composite. ```tsx ``` Renders inside the cost tab of [`HashBalance`](/reference/ui/components/dashboards#hashbalance). # Compose Hashrate layouts (/guides/ui/react/compose-reporting-layouts/hashrate) @tetherto/mdk-react-devkit/foundation The [`Hashrate`](/reference/ui/components/charts#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 | Component | Description | | --- | --- | | [`HashrateSiteView`](/guides/ui/react/compose-reporting-layouts/hashrate/#hashratesiteview) | Site-level hashrate trend view | | [`HashrateMinerTypeView`](/guides/ui/react/compose-reporting-layouts/hashrate/#hashrateminertypeview) | Hashrate grouped by miner model | | [`HashrateMiningUnitView`](/guides/ui/react/compose-reporting-layouts/hashrate/#hashrateminingunitview) | Hashrate grouped by mining unit | ### `HashrateSiteView` Site-level hashrate trend - aggregates hashrate across the whole site for the selected date range, with an optional miner-type filter that scopes the sum to a subset. Building block of the Hashrate composite. ```tsx ``` Renders inside the Site View tab of [`Hashrate`](/reference/ui/components/charts#hashrate). ### `HashrateMinerTypeView` Hashrate drilldown grouped by miner model - bar chart of the latest hashrate per miner type, with an optional multi-select filter. Building block of the Hashrate composite. ```tsx ``` Renders inside the Miner Type View tab of [`Hashrate`](/reference/ui/components/charts#hashrate). ### `HashrateMiningUnitView` Hashrate drilldown grouped by mining unit / container - bar chart of the latest hashrate per container with an optional multi-select filter. Building block of the Hashrate composite. ```tsx ``` Renders inside the Mining Unit View tab of [`Hashrate`](/reference/ui/components/charts#hashrate). # Compose spare parts inventory flows (/guides/ui/react/compose-spare-parts-inventory-flows) ## Overview @tetherto/mdk-react-devkit The spare parts inventory composes from seven [Dialog components](/reference/ui/components/dialogs) that cover registering a part, keeping its subtypes current, moving it (alone or in a batch), reviewing where it has been, and retiring it. Each dialog receives its data and options as props and does no fetching of its own, so you wire the data layer and API calls yourself. This guide walks through composing them into one workflow. ## Prerequisites Complete the [installation](/guides/ui/install) and import styles: `import '@tetherto/mdk-react-devkit/styles.css'`. ## How the pieces fit together ```mermaid flowchart LR subtypes["Manage subtypes"] addOne["Add one part"] bulkAdd["Bulk-add via CSV"] inventory["Spare part in inventory"] moveOne["Move one part"] moveMany["Move many parts"] history["View movement history"] delete["Delete part"] subtypes -.-> addOne addOne --> inventory bulkAdd --> inventory inventory --> moveOne inventory --> moveMany moveOne --> history moveMany --> history inventory --> delete ``` - [`AddSparePartModal`](/reference/ui/components/dialogs#addsparepartmodal): registers a single new spare part - [`SparePartSubTypesModal`](/reference/ui/components/dialogs#sparepartsubtypesmodal): views and adds part-model subtypes for a part type - [`BulkAddSparePartsModal`](/reference/ui/components/dialogs#bulkaddsparepartsmodal): registers many parts at once from a CSV file - [`MoveSparePartModal`](/reference/ui/components/dialogs#movesparepartmodal): moves a single part to a new location or status - [`BatchMoveSparePartsModal`](/reference/ui/components/dialogs#batchmovesparepartsmodal): moves several selected parts to a new location or status in one submit - [`MovementDetailsModal`](/reference/ui/components/dialogs#movementdetailsmodal): shows the origin-to-destination detail of a historical move - [`ConfirmDeleteSparePartModal`](/reference/ui/components/dialogs#confirmdeletesparepartmodal): confirms an irreversible delete Two of these pieces are coupled rather than independent: - [`AddSparePartModal`](/reference/ui/components/dialogs#addsparepartmodal) can embed [`SparePartSubTypesModal`](/reference/ui/components/dialogs#sparepartsubtypesmodal) through its `subTypes*` props. This lets someone add a missing part model without losing the in-progress Add form. `SparePartSubTypesModal` also works standalone, opened directly rather than through Add - [`MoveSparePartModal`](/reference/ui/components/dialogs#movesparepartmodal) and [`BatchMoveSparePartsModal`](/reference/ui/components/dialogs#batchmovesparepartsmodal) both move parts, but for a different cardinality: reach for `MoveSparePartModal` when a single row action moves one part through an edit-then-confirm step, and for `BatchMoveSparePartsModal` when a multi-select table applies one new location or status to every selected part in a single submit, with no confirmation step ## Walk through a typical flow ### Add a part Open [`AddSparePartModal`](/reference/ui/components/dialogs#addsparepartmodal) from your own add-part entry point. Part-type tabs drive which fields validate: a controller part type requires a MAC address, other part types require a serial number instead. Supply `modelOptions` for the active part type and refetch them in `onPartTypeChange` when the tab changes. ### Maintain subtypes If the part model someone needs is not in `modelOptions`, they can open [`SparePartSubTypesModal`](/reference/ui/components/dialogs#sparepartsubtypesmodal) from inside Add without losing their progress, or you can open it standalone from an inventory settings surface. Either way, the parent owns `activePartTypeId` and `subTypes` and re-supplies them when the tab changes. ### Bulk-add many parts instead For registering many parts at once, use [`BulkAddSparePartsModal`](/reference/ui/components/dialogs#bulkaddsparepartsmodal) instead of repeating the one-by-one Add flow. It offers a CSV template download, parses the selected file client-side, and submits the parsed records through your `onSubmit` handler; CSV parsing and validation helpers are exported alongside the component for wiring that handler up. ### Move a part, one or many Move a single part with [`MoveSparePartModal`](/reference/ui/components/dialogs#movesparepartmodal): it previews the before-to-after location and status transition before the user confirms. Move a multi-selected group with [`BatchMoveSparePartsModal`](/reference/ui/components/dialogs#batchmovesparepartsmodal), which applies one new location and status to every part in the selection. ### View its movement history [`MovementDetailsModal`](/reference/ui/components/dialogs#movementdetailsmodal) is read-only: pass it a historical `movement` record and it renders the device summary alongside the origin-to-destination transition. It does not trigger a move itself, it explains one that already happened. ### Delete a part [`ConfirmDeleteSparePartModal`](/reference/ui/components/dialogs#confirmdeletesparepartmodal) gates the destructive path. It surfaces the part code so the user can verify what they are about to remove, and disables its action buttons through `isLoading` while the delete call is in flight. ## Next steps - [Dialog components](/reference/ui/components/dialogs): full props reference for all seven components - [React UI guides](/guides/ui/react): other guides for composing MDK React UI components # Use UI Foundation headlessly (/guides/ui/use-ui-foundation-headlessly) @tetherto/mdk-ui-foundation [`@tetherto/mdk-ui-foundation`](/reference/ui) 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](/guides/ui/install) 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 ``` These are the supported subpath entries — `/store`, `/query`, and `/types`. ## Create a `QueryClient` `createMdkQueryClient` returns a TanStack Query Core client wired to your Gateway. Pass an explicit `apiBaseUrl`, or let the factory resolve one from environment variables: ```ts const queryClient = createMdkQueryClient({ apiBaseUrl: 'https://app-node.example.com', }) ``` Without an explicit `apiBaseUrl`, the factory checks `VITE_MDK_API_URL` then `MDK_API_URL` before falling back to `http://localhost:3000`. ### Bring your own backend `createMdkQueryClient` also accepts `fetcher` and `endpoints`, the seam for pointing the query layer at a backend other than the mining Gateway: - `fetcher` swaps the transport: pass a `Fetcher` that talks to a custom auth scheme, a different protocol, or serves fixtures from memory for a server-less demo. - `endpoints` remaps the `:name` path templates the factories request, pointing the same factories and adapter hooks at a different API's URL space. ```ts const queryClient = createMdkQueryClient({ apiBaseUrl, endpoints: MY_ENDPOINTS, fetcher: myFetcher, }) ``` Both are stashed on the client's query and mutation `meta` and read back by every query and mutation factory, so the same factories and adapter hooks work unchanged regardless of which backend is behind them. > [!TIP] > The catalog app's `bring-your-own-backend` example takes this further: it skips the query layer entirely and drives the same devkit components from plain TanStack `useQuery` against a foreign API shape, useful when a backend doesn't fit the `fetcher`/`endpoints` seam at all. ## 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({ apiBaseUrl: 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 [UI reference](/reference/ui). ## Next steps - [UI reference](/reference/ui): full store list, query helpers, and the `createMdkQueryClient` resolution order. - [What's an app?](/concepts/whats-an-app): where the UI devkit fits into an MDK app's anatomy. - [React adapter](/guides/ui/install): if you decide to layer React on top. # Build a third-party Worker (/guides/workers/build-a-worker) ## TL;DR A Worker plugin package is: - [`mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/mdk-contract.schema.json) at the package root - A file at the path each contract entry's `handler` field names ## Overview 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](https://github.com/tetherto/mdk/blob/main/backend/workers/samples/demo-worker/src/client.js) - The [`mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/mdk-contract.schema.json) - The [handlers](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/lib/worker-runtime-v2.js) - The [mock](https://github.com/tetherto/mdk/blob/main/backend/workers/samples/demo-worker/mock/server.js) - The [tests](https://github.com/tetherto/mdk/blob/main/backend/workers/samples/demo-worker/tests/unit/handlers.test.js) Hosting the finished package (pointing [`WorkerRuntimeV2`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/lib/worker-runtime-v2.js) at it and registering with a live Kernel) is a separate concern, covered in [Test a Worker with MDK](/guides/workers/test-a-worker). [`demo-worker-caller`](https://github.com/tetherto/mdk/blob/main/examples/backend/demo-worker-caller/index.js) shows one host doing exactly that for this guide's own reference implementation. A Worker plugin package is **loaded from its own directory**: [`mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/mdk-contract.schema.json) declares each handler by path, `src/` holds the handler modules, and the host points `WorkerRuntimeV2` at the directory. The package ships only its contract and handler files; `WorkerRuntimeV2` loads them directly rather than requiring an exported module. Handlers are plain `(params)` functions that read their device from the ambient `@tetherto/mdk-worker/device` module. See [`worker-runtime-v2.js`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/lib/worker-runtime-v2.js) for the full shape of that module. This guide generalizes 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/package.json). It proves this pattern works with **zero** dependency on this monorepo's optional worker-infra services (provisioning stores, alert templates, stats aggregation), just [`WorkerRuntimeV2`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/lib/worker-runtime-v2.js) and the directory-loaded Worker plugin shape. This guide 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 [`WorkerRuntimeV2`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/lib/worker-runtime-v2.js), and **device ID** for a runtime device identity. ## What you get ```text your-worker-repo/ package.json mdk-contract.json # the engineering + AI-context contract src/ client.js # plain I/O against your vendor's native API, no MDK concepts 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/handlers.test.js # drives loadContract() + createInstance() against the mock # (no WorkerRuntimeV2 involved) ``` This tree is [`demo-worker`](https://github.com/tetherto/mdk/blob/main/backend/workers/samples/demo-worker/package.json)'s own layout with its vendor name replaced by the placeholder `vendor`: `demo-worker` itself builds and tests with **zero** dependency on `WorkerRuntimeV2`, and your package does too. The `src/telemetry/`, `src/commands/`, and `client.js` naming above is a convention, **not** a requirement. [`WorkerRuntimeV2`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/lib/worker-runtime-v2.js) only requires two things: [`mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/mdk-contract.schema.json) at the package root, and a real file at whatever path each contract entry's `handler` field names. Following the same layout as `demo-worker` just keeps your package legible to anyone who has read another MDK Worker. ## Prerequisites - Node.js `>=24` (all MDK core packages declare this `engines` constraint) - npm 11 [(< 12)](/reference/environment) - 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 - A basic understanding of [how MDK works](/concepts/architecture), the [Worker install pattern](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/install-pattern.md), and the [Worker discovery model](https://github.com/tetherto/mdk/blob/main/backend/workers/README.md) ### 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 publish under your own domain (not `@tetherto`)): ```json { "name": "@your-org/mdk-worker-vendor", "version": "0.1.0", "description": "MDK Worker plugin 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" } } ``` Handler files are loaded with `require()`, so set `"type": "commonjs"` or use `.cjs` files. An ESM-only package (`"type": "module"` with `.js` handlers) is not a supported handler-loading path today. `brittle` and `standard` are the repository's test and lint tools; substitute your own tooling if you prefer. Your own contract-level tests (`loadContract`, `createInstance`, see Step 7) need `@tetherto/mdk-worker` too, but it is **not yet published to the npm registry**. Install it the same way [Test a Worker with MDK](/guides/workers/test-a-worker)'s [Install MDK step](/guides/workers/test-a-worker) does: ```bash npm install github:tetherto/mdk#main (cd node_modules/@tetherto/mdk && npm install) ``` This adds `"@tetherto/mdk": "github:tetherto/mdk#main"` to your `dependencies` and installs the whole monorepo under `node_modules/@tetherto/mdk` (its own root `package.json` name); there is no package literally named `@tetherto/mdk-worker` in `node_modules`. Step 7's test file accounts for this: it requires `@tetherto/mdk/backend/core/mdk-worker`, the same deep path every in-repo Worker already uses. The host process that constructs `WorkerRuntimeV2` and brings its transport dependencies (`@hyperswarm/rpc`, `hyperswarm`, `hyperdht`) is a separate package, not this one. The ambient `@tetherto/mdk-worker/device` import your handler files use (Step 2 onward) is unaffected by any of this: `WorkerRuntimeV2` intercepts that exact string before Node resolves it, so it works whether or not `@tetherto/mdk-worker` exists anywhere in `node_modules`. Only the plain `require("@tetherto/mdk-worker")` calls in your own test/verification scripts need the deep path above. ### 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. `WorkerRuntimeV2` loads every file your handlers require into a private module registry per device (see [`worker-runtime-v2.js`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/lib/worker-runtime-v2.js)), so a client module that binds directly to its device at load time already gets one instance per device, with no factory function and no explicit construction. It reads its device's connection details from the ambient `@tetherto/mdk-worker/device` module: `{ id, opts, env, config, logger }`, where `opts` is this device's own connection config and `env` is the plugin-wide block the host passed when constructing the runtime. [`createModuleContext`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/lib/module-context.js) is the primitive behind that registry: it gives each plugin instance its own `require` cache, so module-level state — a client constructed at load time, say — belongs to that one instance alone. `WorkerRuntimeV2`, the Gateway, and the MCP server each build one per plugin. `WorkerRuntime` v1 has no notion of per-device isolation and does not use it, which is why a v1 plugin needs an explicit `connect()`. `src/client.js`, modeled on [`demo-worker`'s own `client.js`](https://github.com/tetherto/mdk/blob/main/backend/workers/samples/demo-worker/src/client.js): ```js 'use strict' const { opts, env, logger } = require('@tetherto/mdk-worker/device') logger('config received: opts=%o', opts) const TIMEOUT_MS = opts.timeoutMs || 5000 const base = `http://${opts.host || '127.0.0.1'}:${opts.port}` const auth = env.DEVICE_TOKEN ? { authorization: `Bearer ${env.DEVICE_TOKEN}` } : {} const call = async (path, callOpts = {}) => { try { const res = await fetch(base + path, { ...callOpts, headers: { ...auth, ...callOpts.headers }, signal: callOpts.signal || AbortSignal.timeout(TIMEOUT_MS) }) 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 } } module.exports = { 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 }) }) } ``` 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. This module loads once, when `WorkerRuntimeV2` opens this device's context, and stays loaded for the life of the process; nothing probes the device up front. An unreachable device does not fail at load time; the failure surfaces from the first handler call that actually reaches the network (see Step 4). ### 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. `mdk-contract.json`, at your package root: ```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; `WorkerRuntimeV2` 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 your package root) to a function with a fixed signature. `WorkerRuntimeV2` resolves every declared handler path when it loads the contract, and `require()`s it per device the first time that device's context opens. A missing file, a non-function export, or a duplicate name throws before your Worker serves a request (see Troubleshooting). **Every entry in `capabilities.telemetry` and `capabilities.commands` needs a matching file**: declaring `power` / `temperature` / `reboot` in the contract without writing those handlers fails. #### 4.1 Telemetry handler A telemetry handler is `async (params) => value`. The handler reads its own device straight from the ambient `@tetherto/mdk-worker/device` module, the same way `src/client.js` does in Step 2. Devices are isolated by construction: `WorkerRuntimeV2` loads your package's files into a private module registry per device, so `require("../client")` inside one device's handlers always resolves to that device's own client instance, never a sibling's. One file per telemetry field from Step 3, delegating to `src/client.js`: `src/telemetry/hashrate-rt.js`: ```js 'use strict' const client = require('../client') module.exports = async () => (await client.getSummary()).hashrate_ths ``` `src/telemetry/power.js`: ```js 'use strict' const client = require('../client') module.exports = async () => (await client.getSummary()).power_w ``` `src/telemetry/temperature.js`: ```js 'use strict' const client = require('../client') module.exports = async () => (await client.getSummary()).board_temp_c ``` #### 4.2 Command handler A command handler is `async (params) => result`. Return value becomes `payload.result`; a thrown `Error` becomes `{ status: 'FAILED', error: err.message }` in the response, which is how your `errors` map in the contract actually reaches the requesting client. One file per command from Step 3: `src/commands/reboot.js`: ```js 'use strict' const { id } = require('@tetherto/mdk-worker/device') const client = require('../client') const COOLDOWN_MS = 5 * 60 * 1000 // Module-level, not keyed by device: WorkerRuntimeV2 loads this file into a // private registry per device, so this state is already scoped to the one // device this instance was built for. let lastAttemptAt = 0 let running = false function audit (outcome, errorCode) { console.info( JSON.stringify({ event: 'physical_command', command: 'reboot', deviceId: id, 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 () => { const now = Date.now() if (running) { audit('rejected', 'ERR_COMMAND_IN_PROGRESS') throw new Error('ERR_COMMAND_IN_PROGRESS: reboot') } const remaining = COOLDOWN_MS - (now - lastAttemptAt) if (remaining > 0) { audit('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. lastAttemptAt = now running = true audit('started') try { const result = await client.reboot() audit('succeeded') return result } catch (err) { audit('failed', stableErrorCode(err)) throw err } finally { running = false } } ``` `src/commands/set-power-mode.js`: ```js 'use strict' const { id } = require('@tetherto/mdk-worker/device') const client = require('../client') const ALLOWED_MODES = new Set(['eco', 'normal', 'high']) function audit (outcome, errorCode) { console.info( JSON.stringify({ event: 'physical_command', command: 'setPowerMode', deviceId: id, 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 (code) { audit('rejected', code) throw new Error(code) } module.exports = async (params) => { if (!params || params.mode === undefined) reject('ERR_MODE_REQUIRED') if (typeof params.mode !== 'string') reject('ERR_MODE_TYPE') if (!ALLOWED_MODES.has(params.mode)) reject('ERR_BAD_POWER_MODE') audit('started') try { const result = await client.setPowerMode(params.mode) audit('succeeded') return result } catch (err) { audit('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') ``` `lastAttemptAt` and `running` above are deliberately process-local teaching state, scoped to one device by the runtime's per-device module registry rather than by a `Map` keyed on device ID. 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; [`demo-worker`'s own `db.js`](https://github.com/tetherto/mdk/blob/main/backend/workers/samples/demo-worker/src/db.js) shows the same per-device-instance pattern applied to a local SQLite file. 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 arguments. 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. [`WorkerRuntimeV2`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/lib/worker-runtime-v2.js) also auto-registers a builtin `health` channel on every device, with no contract entry required: a plugin that doesn't declare its own `health` telemetry handler still answers `{ query: { type: "health" } }` with `{ status: "OK", id, opts, env, config, workerId }`. Declaring a `health` entry in [`capabilities.telemetry`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/mdk-contract.schema.json) yourself overrides the builtin with your own handler. ```js await mdkClient.pullTelemetry(deviceId, 'health') // → { status: 'OK', id: 'wm-001', opts: {...}, env: {...}, config: {...}, workerId: '...' } ``` ### Verify the plugin loads There is nothing left to assemble: `mdk-contract.json` at your package root, together with the handler files it declares under `src/`, is the complete, loadable Worker plugin. No index file exports it, and nothing turns it into an object for a runtime to consume; a host points `WorkerRuntimeV2` straight at your package directory. That does mean a broken handler wiring has nowhere to surface until something tries to load the directory. Catch it yourself with [`loadContract`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/index.js), the same function `WorkerRuntimeV2` calls internally: ```js 'use strict' const { loadContract } = require('@tetherto/mdk/backend/core/mdk-worker') const loaded = loadContract(__dirname) console.log(loaded.publishedContract) // handler paths stripped, the shape Kernel receives ``` `loadContract` resolves every declared `handler` path on disk but never executes it. A missing file, a missing `handler` field, or a duplicate name throws immediately (see Troubleshooting). It cannot yet catch a handler file that exists but fails to load or does not export a function: that only happens once a device instance is built from it, which is what Step 7's tests exercise per handler. Every declared device reports `online` immediately; an unreachable one surfaces as an error inside the telemetry payload rather than holding the device `offline`. Whatever a handler module opens at load time (a socket, a file handle) lives until the process exits; nothing closes it automatically. ### 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 `src/client.js` already defines that method). ### Test the plugin against the mock Drive [`loadContract`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/index.js) and [`createInstance`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/index.js) directly against the mock. This exercises your whole plugin (telemetry translation, command dispatch, error mapping) with **no** `WorkerRuntimeV2` in the loop, so it needs nothing beyond what you've already written in Steps 1–6. `demo-worker`'s own [`tests/unit/handlers.test.js`](https://github.com/tetherto/mdk/blob/main/backend/workers/samples/demo-worker/tests/unit/handlers.test.js) is the complete worked example of this style; the harness below is the same pattern trimmed to this guide's contract. ```js 'use strict' const path = require('path') const test = require('brittle') const { loadContract, createInstance } = require('@tetherto/mdk/backend/core/mdk-worker') const vendorMock = require('../../mock/server') const PKG_DIR = path.join(__dirname, '..', '..') function buildInstance ({ port, deviceId }) { return createInstance({ dir: PKG_DIR, entries: loadContract(PKG_DIR).entries, device: { id: deviceId, opts: { host: '127.0.0.1', port }, env: {}, config: {} } }) } test('directory-loaded plugin: every contract entry has a working handler module', (t) => { const loaded = loadContract(PKG_DIR) t.is(loaded.entries.telemetry.size, 3) t.is(loaded.entries.commands.size, 2) for (const entry of loaded.publishedContract.capabilities.telemetry) { t.is(entry.handler, undefined, `${entry.name} handler path stripped from published contract`) } // The boot rule proves out per instance: every resolved handler path // loads to a function once bound to a device. const instance = createInstance({ dir: PKG_DIR, entries: loaded.entries, device: { id: 'vendor-boot', opts: { host: '127.0.0.1', port: 1 }, env: {}, config: {} } }) for (const fn of instance.telemetry.values()) t.is(typeof fn, 'function') for (const fn of instance.commands.values()) t.is(typeof fn, 'function') }) 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 instance = buildInstance({ port: 9001, deviceId: 'vendor-0' }) t.is(await instance.telemetry.get('hashrate_rt')(), 200, 'hashrate_rt reads the mock') const result = await instance.commands.get('setPowerMode')({ mode: 'eco' }) t.is(result.power_mode, 'eco', 'command reaches the mock') await t.exception(() => instance.commands.get('setPowerMode')({}), /ERR_MODE_REQUIRED/) await t.exception(() => instance.commands.get('setPowerMode')({ mode: 1 }), /ERR_MODE_TYPE/) await t.exception( () => instance.commands.get('setPowerMode')({ mode: 'turbo' }), /ERR_BAD_POWER_MODE/ ) t.ok( auditEvents.some( (e) => e.command === 'setPowerMode' && e.outcome === 'rejected' ) ) }) test('a telemetry handler rejects when the device is unreachable', async (t) => { // Nothing is listening on this port. With no boot-time connect probe (see // Step 5), the instance itself builds fine; the failure moves to call time. const instance = buildInstance({ port: 9099, deviceId: 'vendor-offline' }) // fetch's connection-refused rejection is a TypeError, which plain // t.exception treats as an uncaught bug rather than an expected rejection. await t.exception.all(instance.telemetry.get('hashrate_rt')()) }) test('reboot enforces concurrency and cooldown after every attempt', async (t) => { const mock = vendorMock.createServer({ port: 9003 }) t.teardown(() => mock.exit()) const instance = buildInstance({ port: 9003, deviceId: 'vendor-concurrent' }) const first = instance.commands.get('reboot')() await t.exception(() => instance.commands.get('reboot')(), /ERR_COMMAND_IN_PROGRESS/) await first await t.exception(() => instance.commands.get('reboot')(), /ERR_COMMAND_COOLDOWN/) }) ``` `createInstance` builds one plugin instance for one device: the same call [`WorkerRuntimeV2`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/lib/worker-runtime-v2.js) makes per configured device at `runtime.start()`. Building two instances against distinct mocks and distinct `deviceId`s (as `demo-worker`'s own test file does) proves device isolation: a command against one instance never reaches the other's client, because each device's `src/client.js` was loaded into its own private module registry. 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 surfacing your contract's `ERR_*` codes, concurrent-command rejection, cooldown after successful and failed attempts, an unreachable device surfacing an error from the handler call rather than failing to build, 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. Run it: ```bash npm install npm test ``` Expected output ends with: ```text # tests = 4/4 pass # asserts = 20/20 pass # ok ``` ### 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](https://github.com/tetherto/mdk/blob/main/backend/core/README.md)) — 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 - [ ] Unreachable-device behavior (an error from the handler call, not a boot-time failure) and the host's 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 Two distinct phases can fail, and telling them apart matters: contract loading validates your `mdk-contract.json` and resolves every handler path once, for the whole package; device instantiation `require()`s those handler files, once per device, the first time that device's context opens. **Contract loading**: `new WorkerRuntimeV2(dir, opts)` runs this synchronously before any device opens, and [`loadContract(dir)`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/index.js) (Step 5) runs the identical check on its own: | Error | Diagnostic and remediation | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------ | | `ERR_WORKER_DIR_REQUIRED` | `WorkerRuntimeV2`'s first argument must be a non-empty directory string | | `ERR_CONTRACT_DIR_REQUIRED` | `loadContract`'s argument must be a non-empty directory string | | `ERR_CONTRACT_NOT_FOUND: : ` | No `mdk-contract.json` at the package root; check the path | | `ERR_CONTRACT_INVALID_JSON: : ` | `mdk-contract.json` does not parse; fix the JSON syntax | | `ERR_PLUGIN_CONTRACT_METADATA_MISSING` | `metadata` is missing or not an object | | `ERR_PLUGIN_CONTRACT_CAPABILITIES_MISSING` | `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:
.: : ` | No file resolves at that path relative to the package root | | `ERR_PLUGIN_DUPLICATE_NAME:
.` | Rename or remove the duplicate entry in that section | **Device instantiation**: `runtime.start()` runs this per configured device (see [`worker-runtime-v2.js`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/lib/worker-runtime-v2.js)), and [`createInstance`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/index.js) (Step 7) runs the identical check for one device at a time in tests: | Error | Diagnostic and remediation | | ---------------------------------------------------------------------------- | ------------------------------------------------------------------ | | `ERR_INSTANCE_HANDLER_NOT_FOUND: :
.: ` | The path that resolved fine at contract-load time no longer resolves inside this device's module context; check for a typo | | `ERR_INSTANCE_HANDLER_LOAD_FAILED: :
.: ` | The handler file exists but throws while loading; the nested error names the real failure (a missing import, a syntax error) | | `ERR_INSTANCE_HANDLER_NOT_FUNCTION: :
.` | The module must assign a function to `module.exports` | For errors from a live Kernel registration or requests once your Worker is actually hosted, 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) - See the end-user experience of controlling and monitoring your device via the Worker in [Test a Worker's next steps](/guides/workers/test-a-worker) # Test a new Worker (/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 package on `WorkerRuntimeV2` in a Node.js host process by pointing it at the Worker plugin package's directory (see [Build a third-party Worker](/guides/workers/build-a-worker)). 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: WorkerRuntimeV2, 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) - npm 11 [(< 12)](/reference/environment) - 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 `WorkerRuntimeV2`) 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). The monorepo is a real root npm workspace, but npm does not auto-install a git dependency's own transitive workspace tree — so run a single install inside the checked-out copy once after adding it: ```bash (cd node_modules/@tetherto/mdk && npm install) ``` 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 path = require("path"); const { WorkerRuntimeV2 } = require("@tetherto/mdk/backend/core/mdk-worker"); const WORKER_DIR = path.resolve(__dirname, "../your-worker-repo"); async function startVendorWorker({ workerId, kernelTopic, seedDevices }) { const runtime = new WorkerRuntimeV2(WORKER_DIR, { 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 }; ``` `WorkerRuntimeV2`'s first argument is the Worker plugin package's own directory, the same one that holds its `mdk-contract.json` (see [Build a third-party Worker](/guides/workers/build-a-worker)); there is no plugin module to `require()`. Required options are `workerId` and a non-empty `devices` array. Each device's `config` object here becomes the ambient `opts` its handlers read from `@tetherto/mdk-worker/device`. `kernelTopic` is needed only for DHT discovery. Without a `store`, `WorkerRuntimeV2` 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. `WorkerRuntimeV2` 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 a frozen `{ deviceId, config, services }` for a device that is currently `online`, or `null` otherwise. There is no `device` key: a directory-loaded plugin has no per-device client object for the host to reach into, since handler modules bind to their device privately through the ambient context (see Step 4 of Build a third-party Worker). A host process that needs to act on a live device drives it the same way a Gateway request would, through `runtime.handleRequest(...)`, rather than through `getDeviceContext(...).device`. ### Register directly with a live Kernel When Kernel and the Worker host share a process, register the runtime's public key directly. The following host script (save it next to your worker as e.g. `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({ kernelKey: 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 local-directory discovery on one machine When the Worker host and Kernel are separate processes on the same machine, neither `registerWorker()` nor a DHT topic is needed: the Worker publishes its RPC key to a shared directory and Kernel picks it up from there. Both sides default that directory to `/.worker-keys`, so passing the same `root` is the whole configuration. ```js "use strict"; const os = require("os"); const path = require("path"); const { getKernel, waitForDiscovery, shutdown, } = require("@tetherto/mdk/backend/core/mdk"); const { keysDir, publishWorkerKey, } = require("@tetherto/mdk/backend/core/mdk/lib/local-discovery"); const { startVendorWorker } = require("./index"); const ROOT = path.join(os.tmpdir(), `vendor-worker-local-${process.pid}`); async function main() { let worker; let kernel; try { worker = await startVendorWorker({ workerId: "vendor-demo", seedDevices: [ { id: "vendor-0", opts: { host: "127.0.0.1", port: 9001 } }, ], }); publishWorkerKey( keysDir(ROOT), "vendor-demo", worker.runtime.getPublicKey().toString("hex"), ); kernel = await getKernel({ root: ROOT, discovery: { mode: "local" } }); const workers = await waitForDiscovery(kernel, { minWorkers: 1, timeoutMs: 30000, }); 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; }); ``` The two halves are shown in one script here so it runs as a single check, but the mode exists for the split case: publish from the Worker process, then read from the Kernel process. Order does not matter. Kernel rescans the directory every four seconds on top of watching it, so a Worker that publishes before Kernel starts is found on the first scan, and one whose RPC server is not yet listening is retried on a later one. Pass a `storeDir` to `WorkerRuntimeV2` if you restart Workers. The published key is only stable across restarts when the runtime has a store to persist its seeds in (see Step 2); without one, every restart looks like a new peer to Kernel and stale `.key` files accumulate. Every process must resolve the same filesystem path, so this mode only covers one machine. Workers on separate hosts need DHT discovery, in the next step. ### 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](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/architecture.md#discovery-model) for how the three modes compare. ## Troubleshooting ### Runtime construction `new WorkerRuntimeV2(dir, opts)` runs two phases synchronously: it loads and validates the Worker plugin package at `dir` first (see [Troubleshooting](/guides/workers/build-a-worker) in Build a third-party Worker for `ERR_WORKER_DIR_REQUIRED` and the contract/handler errors), then validates `opts`: | 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 store services](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/README.md#store-services)) 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 | | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `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` | Every device reports `online` as soon as `runtime.start()` returns; a directory-loaded plugin has no boot-time probe, so an unreachable device is never a startup symptom (see [Step 5](/guides/workers/build-a-worker) of Build a third-party Worker). If a device is unreachable, look for it at request time instead, in the table below. ### Request time | Error | Diagnostic and remediation | | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `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 omitted its target device ID | | `ERR_DEVICE_ID_REQUIRED` | A 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 | An unreachable device does not surface as `ERR_DEVICE_UNAVAILABLE` for a directory-loaded plugin: with no `connect()` probe and no offline state, the failure comes back from inside the handler's own response instead, isolated to the telemetry channel that touched the network (`{ error: '...' }` under that channel's key in `metrics`, or `status: 'FAILED'` for a command); see the [directory-loaded plugin model](/guides/workers/build-a-worker) in Build a third-party Worker. ## Next steps - Understand the [security boundaries](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md#security-model) Understand the end-user experience of controlling and monitoring your device via the Worker: - Build a [minimal dashboard](/tutorials/build-a-dashboard) around one Worker - Run the [Starter site example](https://github.com/tetherto/mdk/blob/main/examples/mvp-site/README.md) with a supervised, multi-Worker fleet - Connect [the operator agent](/guides/agent) to query and command your Workers over MCP # 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. ## Browse by stack area ### App toolkit - **UI Devkit**: [components](/reference/ui/components/), [hooks](/reference/ui/hooks/), [types](/reference/ui/types/), and [utilities](/reference/ui/utilities/) for the React UI Devkit ### 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 ### Workers - **[Workers](/reference/worker/)**: device protocol adapters that wrap vendor hardware APIs and expose them through the MDK Protocol ## Next steps - [Architecture](/concepts/architecture) for narrative explanations - [Try the demo](/tutorials/run-a-site) for step-by-step instructions # Environment requirements (/reference/environment) ## Overview Every guide and tutorial in this repo assumes the same baseline; this page explains the npm ceiling. ## Requirements - [Node.js](https://nodejs.org/) >=24 (LTS) - npm 11 (< 12) - Git (latest stable version) Check the installed npm version with `npm --version`. ## Why npm stays below 12 npm 12 disables fetching git-based dependencies by default and fails with `EALLOWGIT`: ```text npm error code EALLOWGIT npm error Fetching packages of type "git" have been disabled ``` `examples/full-site` and `examples/mvp-site` depend on the Whatsminer Worker directly from its git repository, so `npm install` on npm 12 fails on that dependency before it reaches anything else. If `npm --version` already reports 12 or higher, repin with: ```bash npm install -g npm@11 ``` # 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 pulls telemetry on its own cadence — it performs no aggregation itself | [`backend/core/kernel/`](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md) | | **Gateway** | The developer-owned HTTP entry point between non-Node clients (UI) and Kernel. Required for browser and HTTP consumers; AI agents reach Kernel through the standalone MCP server instead. Not used in the in-process Antminer-rack example below | [`backend/core/gateway/`](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md) | | **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/`](/reference/worker) | | **Worker Plugin** | The executable device integration: [`mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/mdk-contract.schema.json) plus its handler files, loaded from a package directory by `WorkerRuntimeV2` | [`backend/workers/miners/antminer/plugin/index.js`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/plugin/index.js) | | **Worker contract** | The declarative [`mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/mdk-contract.schema.json) alone, as read, validated, published, rendered, or queried | [`backend/workers/miners/antminer/plugin/mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/plugin/mdk-contract.json) | | **Driver class** | The JavaScript class a Worker exports, one per device family (for example `Antminer`, `Whatsminer`), not one per model. Drives every device that Worker registers. | [`backend/workers/miners/antminer/lib/antminer.js`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/lib/antminer.js) | | **Thing** | One registered device instance. Created by sending a `registerThing` command to the Worker's provisioning service, not by calling a driver-class method directly. Identified by a generated `deviceId`. | [`backend/core/mdk/lib/services/provisioning.service.js`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk/lib/services/provisioning.service.js) | | **MCP** (Model Context Protocol) | The protocol AI agents use to discover and call tools. `@tetherto/mdk-mcp` runs as its own standalone process, deriving tools from a plugin's routes; the Gateway hosts no MCP itself | [`backend/core/mcp/`](https://github.com/tetherto/mdk/blob/main/backend/core/mcp/README.md) | ### 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). [Scalability](/concepts/scalability) covers the multi-Worker view, parallel Workers, and multi-site deployments. ## 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({ kernelKey: 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 a mining site end to end](/tutorials/run-a-site) - Learn more about: - Multi-process discovery across machines: [Worker discovery](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/architecture.md#discovery-model) - Gateway implementation details, including HTTP routing and plugin registration: [`backend/core/gateway/README.md`](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md) - Building your own Worker for a new device family: [the build walkthrough](/guides/workers/build-a-worker) - The install and run pattern every shipped Worker package follows: [`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/plugin/mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/plugin/mdk-contract.json) # Kernel reference (/reference/kernel) `@tetherto/mdk-kernel` is the orchestration kernel of the MDK stack. This subsection holds the canonical specs for its internal modules. For the architectural narrative explaining how the Kernel fits into the rest of the stack, see [Architecture](/concepts/architecture). ## 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) ## Overview [Kernel](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/index.js)'s coordination splits across single-purpose modules. Each owns its own state, persistence boundary, and scaling characteristics. It communicates with the others only through its declared interface. The [Kernel's Architecture overview](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#architecture) provides the canonical spec for each module's interfaces, state machine, and recovery behavior. ## Modules - [`WorkerRegistry`](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#workerregistry): holds two flat indexes, `deviceId` to its owning Worker and `workerId` to that Worker's record, and drives each Worker through its registration lifecycle - [`CommandDispatcher`](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#commanddispatcher): validates an incoming command, resolves the target device or devices, and hands off to the Command State Machine - [`CommandStateMachine`](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#commandstatemachine): tracks every command's execution lifecycle in a write-ahead log - [`TelemetryCollector`](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#telemetrycollector): a proxy that routes telemetry queries to the Worker that owns the data, persisting none of it - [`Scheduler`](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#scheduler): the system metronome that fires the recurring telemetry, health, and state jobs - [`HealthMonitor`](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#healthmonitor): pings every registered Worker on a cadence and marks dead ones unroutable - [`ActionManager`](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#actionmanager): handles the write action approval lifecycle at the Kernel layer - [`ActionCaller`](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#actioncaller): resolves an approved action into the per-Worker write calls that carry it out ## Next steps - Review the [Protocol messages](/reference/protocol/messages): the actions these modules route and execute - See the [Kernel architecture](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md): the architectural narrative behind this module split - Understand [approval-gated writes](https://github.com/tetherto/mdk/blob/main/docs/concepts/control-plane.md#approval-gated-writes): the cross-layer flow [`ActionManager`](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#actionmanager) and [`ActionCaller`](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#actioncaller) implement # Protocol reference (/reference/protocol) The MDK Protocol is the contract that crosses every layer of the stack: Workers, `@tetherto/mdk-kernel`, and the Gateway all exchange the same envelope. This subsection holds the canonical specs. For the architectural narrative explaining how the protocol fits together, see [Architecture](/concepts/architecture#why-hrpc). ## 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) ## Overview Every MDK Protocol message uses the same envelope regardless of which layers are talking. This page shows the envelope shape and one worked example. ## Envelope ```json { "id": "uuid-v4", "version": "0.2.0", "type": "request | response | event", "action": "", "sender": "", "target": " | null", "deviceId": "string | null", "timestamp": 1711640000000, "payload": {} } ``` External consumers (UI or AI agents) only provide `deviceId`. [Kernel](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/index.js) resolves the target Worker identity internally. A concrete request and response pair, end to end: ```json // request: Gateway asks Kernel to reboot device wm-001 { "id": "8d1c-e3a4", "version": "0.2.0", "type": "request", "action": "command.request", "sender": "gateway", "target": null, "deviceId": "wm-001", "timestamp": 1711640000000, "payload": { "command": "reboot" } } // response: Kernel relays the Worker's terminal result { "id": "1f9b-77c2", "version": "0.2.0", "type": "response", "action": "command.result", "sender": "kernel:kernel:shard-1", "target": "gateway", "deviceId": "wm-001", "timestamp": 1711640002145, "payload": { "status": "SUCCESS", "elapsedMs": 2145 } } ``` ## Next steps - Learn more about actions and command targeting: - The [Kernel README](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#mdk-protocol) holds the full action catalogue (worker discovery, scheduled polling, command dispatch, kernel queries, and the write action lifecycle) and [command targeting rules](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#command-control) (`payload.scope`'s `device`, `worker`, and `rack` values, and the 1024-target cap) - [Approval-gated writes](https://github.com/tetherto/mdk/blob/main/docs/concepts/control-plane.md#approval-gated-writes) details the write action lifecycle's full cross-layer flow, and use [the write-actions how-to](/guides/gateway/write-actions) to submit and approve actions from a Gateway consumer - [How MDK works](/concepts/architecture): for the architectural narrative explaining when each action fires - See the [Kernel MDK Protocol spec](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#mdk-protocol) for every action, direction, and purpose - [Kernel modules](/reference/kernel/modules): the per-module specs that route and execute these actions - [Build a Worker](/guides/workers/build-a-worker): implement the Worker side of this protocol # Supported hardware (/reference/supported-hardware) ## Manufacturer-maintained workers ### Miners } title="Whatsminer" href="https://github.com/whatsminer/whatsminer-mdk-worker" description="microbt · Not enumerated; see manufacturer documentation" /> ## MDK-maintained workers ### Miners } title="Antminer" href="https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/README.md" description="bitmain · S19XP, S19XPH, S21, S21PRO" /> } title="Avalon" href="https://github.com/tetherto/mdk/blob/main/backend/workers/miners/avalon/README.md" description="canaan · A1346" /> ### Containers } title="Antspace" href="https://github.com/tetherto/mdk/blob/main/backend/workers/containers/antspace/README.md" description="bitmain · HK3, IMM" /> } title="Bitdeer" href="https://github.com/tetherto/mdk/blob/main/backend/workers/containers/bitdeer/README.md" description="bitdeer · D40-A1346, D40-M30, D40-M56, D40-S19XP" /> ### Power meters } title="ABB" href="https://github.com/tetherto/mdk/blob/main/backend/workers/power-meter/abb/README.md" description="abb · B23, B24, M1M20, M4M20, REU615" /> } title="SATEC" href="https://github.com/tetherto/mdk/blob/main/backend/workers/power-meter/satec/README.md" description="satec · PM180" /> } title="Schneider Electric" href="https://github.com/tetherto/mdk/blob/main/backend/workers/power-meter/schneider/README.md" description="schneider · P3U30, PM5340" /> ### Sensors } title="Seneca" href="https://github.com/tetherto/mdk/blob/main/backend/workers/temperature/seneca/README.md" description="seneca · Z-4RTD-2" /> ### Mining pools } title="F2Pool" href="https://github.com/tetherto/mdk/blob/main/backend/workers/minerpools/f2pool/README.md" description="f2pool · F2POOL-BTC" /> } title="Ocean" href="https://github.com/tetherto/mdk/blob/main/backend/workers/minerpools/ocean/README.md" description="ocean · OCEAN-BTC" /> ## Contract conformance All worker contracts validate cleanly against the vendored schema. # Supported plugins (/reference/supported-plugins) ## Overview A Gateway loads exactly the plugins your stack declares in `spec.gateway.plugins[]`, and nothing else. Nothing here auto-registers: a plugin contributes its routes only when a stack names it. MDK ships the following plugins: - Bundled site plugins, ready to use once a stack declares them as `@tetherto/mdk-plugins/` subpaths - Inert plugins, shipped for reference but not wired (declaration does not provide working endpoints) - Optional standalone plugins offered through [`mdk onboard`](/guides/cli/install#command-groups) ## MDK plugins ### Bundled site plugins #### `site-hashrate` | Method | Path | Description | | --- | --- | --- | | `GET` | `/api/site/hashrate-history` | Fans out telemetry.pull to every registered worker and returns site-level hashrate history aggregated by timestamp. Defaults to last 7 days when start/end are omitted | #### `site-monitor` | Method | Path | Description | | --- | --- | --- | | `GET` | `/auth/site` | Returns the site name from the gateway config (common.json `site`) | | `GET` | `/auth/featureConfig` | Returns the `featureConfig` object from the gateway config (common.json `featureConfig`) | | `GET` | `/site-monitor/hashrate` | Pulls metrics telemetry from every READY worker's devices via the MDK protocol and returns per-device hashrate/power plus site totals | #### `telemetry` | Method | Path | Description | | --- | --- | --- | | `GET` | `/auth/metrics/hashrate` | Returns daily hashrate history and summary for the site. Optionally groups by miner type or container | | `GET` | `/auth/metrics/consumption` | Returns daily power consumption (W and MWh) history and summary for the site | | `GET` | `/auth/metrics/efficiency` | Returns daily mining efficiency (W/TH) history and summary for the site | | `GET` | `/auth/metrics/miner-status` | Returns daily online/offline/sleep/maintenance miner counts and averages | | `GET` | `/auth/metrics/power-mode` | Returns miner count by power mode category (low/normal/high/sleep/offline) over time | | `GET` | `/auth/metrics/power-mode/timeline` | Returns per-miner power mode segments over a time range, optionally filtered by container | | `GET` | `/auth/metrics/temperature` | Returns max and average temperature per container over time, with site-level aggregates | | `GET` | `/auth/metrics/containers/{id}` | Returns latest telemetry snapshot and miner list for a specific container | | `GET` | `/auth/metrics/containers/{id}/history` | Returns historical telemetry log for a specific container | ### Inert #### `auth` | Method | Path | Description | | --- | --- | --- | | `GET` | `/auth/userinfo` | Returns the authenticated user's profile from the validated JWT | | `POST` | `/auth/token` | Issues a new JWT from an existing valid token, optionally scoping TTL and roles | | `GET` | `/auth/permissions` | Returns the permission set encoded in the current token | | `GET` | `/auth/ext-data` | Proxies an external data request to the Kernel network by type and optional query filter | ### Optional plugins #### `agent` | Method | Path | Description | | --- | --- | --- | | `POST` | `/agent/sessions` | Opens an agent session bound to the caller's identity | | `POST` | `/agent/sessions/:id/messages` | Streams an agent turn over SSE; write tools pause for an operator's approval | | `POST` | `/agent/sessions/:id/approvals/:approvalId` | Approves or rejects a paused write, resuming or ending the turn | | `DELETE` | `/agent/sessions/:id` | Deletes a session owned by the caller | #### `demo` | Method | Path | Description | | --- | --- | --- | | `GET` | `/api/demo/summary` | Fans out metrics telemetry to every registered demo-worker device (fingerprint: `hashrate_rt` + history) and returns fleet totals plus a per-device breakdown. Aggregation lives here — workers only ever answer for one device | | `GET` | `/api/demo/history` | Pulls the demo-worker `history` telemetry channel (SQLite recent samples: ts, `hashrate_ths`, `power_w`, `board_temp_c`) for every matched device, or a single device when `deviceId` is set | ## Next steps - [Declare a plugin in your stack](/guides/gateway/plugins): the manifest, controllers, and how a stack loads them - [Read the bundled-plugin package notes](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md): the `@tetherto/mdk-plugins` subpath plugins and the inert auth plugin # UI Reference (/reference/ui) Complete API reference for the MDK UI packages: `@tetherto/mdk-react-devkit`, `@tetherto/mdk-react-adapter`, and `@tetherto/mdk-ui-foundation`. ## Quick links | Section | Description | Count | |---------|-------------|-------| | [Components](/reference/ui/components) | React components for building UIs | 286 components | | [Hooks](/reference/ui/hooks) | React hooks for state and data | 107 hooks | | [Query Helpers](/reference/ui/query-helpers) | TanStack Query helpers for data fetching | 18 queryHelpers | | [Stores](/reference/ui/stores) | Zustand stores for state management | 5 stores | | [Types](/reference/ui/types) | TypeScript type definitions | 257 types | | [Utilities](/reference/ui/utilities) | Helper functions and formatters | 189 utilities | ## Package overview ### `@tetherto/mdk-react-devkit` The main UI component library. Provides: - Production-ready React components - Component-specific hooks - TypeScript types for all components ```tsx ``` ### `@tetherto/mdk-react-adapter` React bindings for the foundation layer. Provides: - Zustand store access hooks - Authentication hooks - Permission hooks - Data fetching hooks ```tsx ``` ### `@tetherto/mdk-ui-foundation` Framework-agnostic foundation layer. Provides: - Zustand stores - TanStack Query helpers - Utility functions - TypeScript types ```tsx ``` ## Get started 1. [Install the packages](/guides/ui/install) 2. Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` 3. Browse components by category or search for specific APIs # Components (/reference/ui/components) The `@tetherto/mdk-react-devkit` package provides production-ready React components organized by category. ## Prerequisites - Complete the installation ```bash # Clone the MDK UI monorepo (adjust the URL to your fork if needed) git clone https://github.com/tetherto/mdk.git cd mdk/ui # Install dependencies and build packages (npm workspaces) npm install npm run build ``` - Add the dependency to your app's `package.json` ```json { "dependencies": { "@tetherto/mdk-react-devkit": "*", "@tetherto/mdk-react-adapter": "*", "@tetherto/mdk-ui-foundation": "*" } } ``` > **Coming soon** — npm packages are not yet published. Use the monorepo setup for now. ```bash npm install \ @tetherto/mdk-react-devkit \ @tetherto/mdk-react-adapter \ @tetherto/mdk-ui-foundation ``` Run `npm install` from the `mdk/ui` workspace root after your app is under `apps/` so npm links workspace packages. - Import the core styles in your app's entry point: ```tsx ``` ## Browse by category | Category | Description | |----------|-------------| | [Actions](/reference/ui/components/actions) | Buttons, action triggers, and export controls | | [Auth](/reference/ui/components/auth) | Authentication and sign-in components | | [Branding](/reference/ui/components/branding) | Logos, wordmarks, and brand elements | | [Cards](/reference/ui/components/cards) | Card containers and card-based layouts | | [Charts](/reference/ui/components/charts) | Data visualization and chart components | | [Dashboard](/reference/ui/components/dashboard) | Dashboard layouts and containers | | [Dashboards](/reference/ui/components/dashboards) | Pre-built dashboard compositions | | [Dialogs](/reference/ui/components/dialogs) | Modal dialogs and confirmation prompts | | [Display](/reference/ui/components/display) | Data display and formatting components | | [Features](/reference/ui/components/features) | Feature-specific composite components | | [Feedback](/reference/ui/components/feedback) | Alerts, toasts, and user feedback | | [Filters](/reference/ui/components/filters) | Filter controls and filter bars | | [Forms](/reference/ui/components/forms) | Form inputs, selects, and validation | | [Layout](/reference/ui/components/layout) | Page layouts, grids, and spacing | | [Media](/reference/ui/components/media) | Images, icons, and media display | | [Misc](/reference/ui/components/misc) | Utility and miscellaneous components | | [Monitoring](/reference/ui/components/monitoring) | System monitoring and status displays | | [Navigation](/reference/ui/components/navigation) | Sidebars, tabs, and navigation menus | | [Overlays](/reference/ui/components/overlays) | Popovers, tooltips, and overlay panels | | [Pages](/reference/ui/components/pages) | Full-page layouts and page shells | | [Settings](/reference/ui/components/settings) | Settings panels and preference controls | | [Tables](/reference/ui/components/tables) | Data tables and table utilities | | [Widgets](/reference/ui/components/widgets) | Dashboard widgets and data cards | ## Import pattern Components are imported from the package root: ```tsx ``` ## Styling Components use BEM-style CSS classes (e.g., `.mdk-button`, `.mdk-card__header`) for styling consistency. Every component forwards `className` to its root element. # Action (/reference/ui/components/actions) Components for triggering actions and user interactions. ## 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. - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit ### Button ```tsx ``` Primary action button with variants, sizes, loading state, icon placement, and full-width layout. Forwards refs and all native ` ) ``` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `ActionButton` [**`ActionButton`**](/reference/ui/components/actions/#actionbutton) component with confirmation popover or dialog `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/action-button/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/action-button/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `confirmation` | Required | `ActionButtonConfirmation` | - | Configuration for the confirmation UI | | `className` | Optional | `string` | - | Additional class for the trigger button | | `disabled` | Optional | `boolean` | - | Disables the trigger button | | `label` | Optional | `string` | - | [**`Button`**](/reference/ui/components/actions/#button) label text | | `loading` | Optional | `boolean` | - | Shows a spinner on the trigger button | | `mode` | Optional | `"dialog" \| "popover"` | `"popover"` | Confirmation mode: popover (inline) or dialog (modal) | | `variant` | Optional | `"primary" \| "danger" \| "secondary"` | `"secondary"` | Visual style of the trigger button | ### `Button` Primary action button. Supports loading state with spinner, icon placement, variants, sizes, and full-width layout. Forwards refs and all native button attributes `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/button/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/button/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `contentClassName` | Optional | `string` | - | Class names applied to the inner content wrapper | | `disabled` | Optional | `boolean` | `false` | Disable the button | | `fullWidth` | Optional | `boolean` | `false` | Make the button stretch to fill its container | | `icon` | Optional | `React.ReactNode` | - | Icon node rendered alongside `children` | | `iconPosition` | Optional | `"left" \| "right"` | `"left"` | Icon placement relative to children | | `loading` | Optional | `boolean` | `false` | Show a spinner instead of the content and disable the button | | `size` | Optional | `"sm" \| "md" \| "lg"` | - | Size token (`sm`, `md`, `lg`) | | `type` | Optional | `"button" \| "submit" \| "reset"` | `"button"` | Native button type | | `variant` | Optional | `"icon" \| "link" \| "primary" \| "danger" \| "secondary" \| "tertiary" \| "nav-link" \| "outline" \| "ghost"` | `"secondary"` | Visual variant (e.g. `primary`, `secondary`, `ghost`) | ### `StatsExport` Dropdown button that triggers asynchronous CSV or JSON export. Shows a spinner while the corresponding handler is awaited `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/stats-export/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/stats-export/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `onCsvExport` | Required | `() => Promise` | - | Awaited; spinner shown while pending | | `onJsonExport` | Required | `() => Promise` | - | Awaited; spinner shown while pending | | `disabled` | Optional | `boolean` | `false` | Disable the trigger | | `hideLabel` | Optional | `boolean` | `false` | Hides the textual "Export" label | # Auth (/reference/ui/components/auth) Components for authentication flows and user sign-in. ## 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. - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `RequireAuth` Route guard that reads the session token from the headless `authStore` (via `useAuth`) and renders the children only when a token is present. Otherwise it renders `fallback` — typically `` from `react-router`. Router-agnostic by design `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/auth/require-auth/require-auth.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/auth/require-auth/require-auth.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `children` | Required | `React.ReactNode` | - | Rendered when a token is present | | `fallback` | Required | `React.ReactNode` | - | Rendered when no token is present — typically `` | | `rememberPath` | Optional | `boolean` | `true` | When true (default), the current location is persisted to sessionStorage before rendering the fallback so the sign-in flow can return there | ### `SignInGoogleButton` One-click Google OAuth sign-in trigger. Defaults to a full-page redirect to `${oauthBaseUrl}/oauth/google`, mirroring the reference app's production flow `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/auth/sign-in-google-button/sign-in-google-button.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/auth/sign-in-google-button/sign-in-google-button.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `oauthBaseUrl` | Required | `string` | - | Base URL of the OAuth backend (no trailing slash). Click navigates to `${oauthBaseUrl}/oauth/google` | | `contentClassName` | Optional | `string` | - | Class names applied to the inner content wrapper | | `disabled` | Optional | `boolean` | `false` | Disable the button | | `fullWidth` | Optional | `boolean` | `false` | Make the button stretch to fill its container | | `icon` | Optional | `React.ReactNode` | - | Icon node rendered alongside `children` | | `iconPosition` | Optional | `"left" \| "right"` | `"left"` | Icon placement relative to children | | `label` | Optional | `string` | `"Sign in with Google"` | Override the visible button label | | `loading` | Optional | `boolean` | `false` | Show a spinner instead of the content and disable the button | | `onClick` | Optional | `(() => void)` | `redirect` | Override the click behaviour entirely. When set, `oauthBaseUrl` is ignored | | `size` | Optional | `"sm" \| "md" \| "lg"` | - | Size token (`sm`, `md`, `lg`) | | `type` | Optional | `"button" \| "submit" \| "reset"` | `"button"` | Native button type | | `variant` | Optional | `"icon" \| "link" \| "primary" \| "danger" \| "secondary" \| "tertiary" \| "nav-link" \| "outline" \| "ghost"` | `"secondary"` | Visual variant (e.g. `primary`, `secondary`, `ghost`) | # Branding (/reference/ui/components/branding) Components for brand identity and visual consistency. ## 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. - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `MdkWordmark` MDK wordmark — the canonical brand lockup, rendered as inline SVG so it tints to `currentColor`. Use this in [``](/reference/ui/components/navigation/#appheader) (via the `logo` slot) or anywhere else the brand should appear `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/mdk-wordmark/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/mdk-wordmark/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `className` | Optional | `string` | - | Optional class hook on the outer `` | | `size` | Optional | `"sm" \| "md" \| "lg"` | `"md"` | Visual size of the wordmark. `sm` ≈ 24px tall, `md` ≈ 32px, `lg` ≈ 64px | | `title` | Optional | `string` | `"MDK"` | Accessible label | # Card (/reference/ui/components/cards) Card components for grouping and presenting content. ## 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. - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `ActiveIncidentsCard` Summary card displaying a list of active incidents/alerts with severity indicators, loading skeleton, and empty state. Rows are virtualized via `@tanstack/react-virtual` so the card stays responsive with thousands of incidents `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/active-incidents-card/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/active-incidents-card/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `className` | Optional | `string` | - | Additional class names appended to the root | | `emptyMessage` | Optional | `string` | - | Message rendered when no items | | `isLoading` | Optional | `boolean` | `false` | Show skeleton rows instead of items | | `items` | Optional | `TIncidentRowProps[]` | `[]` | Incident rows to render | | `label` | Optional | `string` | `"Active Alerts"` | Header label shown above the list | | `onItemClick` | Optional | `(id: string) => void` | - | Called with the incident id when a row is clicked | | `skeletonRows` | Optional | `number` | `4` | Number of skeleton rows shown when `isLoading` | ### `CabinetDetailCard` Read-only LV cabinet detail: powermeter readings, the root plus per-position temperature readings (severity-coloured, with an offline marker), and the active-warnings timeline. Presentational — shape the rows with [`useCabinetDetail`](/reference/ui/hooks/cards/#usecabinetdetail) `advanced`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/cabinet-detail-card/cabinet-detail-card.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/cabinet-detail-card/cabinet-detail-card.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `alarmsDataItems` | Required | `TimelineItemData[]` | - | Active-warnings timeline items | | `powerMeters` | Required | `CabinetReadingRow[]` | - | Non-root powermeter reading rows | | `tempSensors` | Required | `CabinetReadingRow[]` | - | Non-root temperature sensor reading rows | | `title` | Required | `string` | - | Cabinet display title (`LV Cabinet 1` / transformer title) | | `isLoading` | Optional | `boolean` | - | Shows a spinner while the cabinet snapshot is loading | | `onNavigate` | Optional | `((path: string) => void)` | - | Router navigate used by warning rows to deep-link into the alert | | `rootTempSensor` | Optional | `CabinetReadingRow` | - | The cabinet-root temperature reading, when present | ### `MetricCard` Compact card displaying a labelled metric value with optional highlight and transparency states `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/composite/metric-card/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/composite/metric-card/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `label` | Required | `string` | - | Text label shown above the value | | `unit` | Required | `string` | - | Unit suffix appended after the value (e.g. `"TH/s"`, `"W"`, `"USD"`) | | `value` | Required | `string \| number \| null` | - | Metric value to display | | `bgColor` | Optional | `string` | `BLACK_ALPHA_05` | Custom background color (CSS color string) | | `className` | Optional | `string` | - | Additional class names appended to the root element | | `isHighlighted` | Optional | `boolean` | `false` | Renders the value in orange to draw attention | | `isTransparentColor` | Optional | `boolean` | `false` | Renders the value in a low-opacity white for de-emphasized display | | `isValueMedium` | Optional | `boolean` | `false` | Applies a medium-weight variant to the value typography | | `noMinWidth` | Optional | `boolean` | `false` | Removes the default minimum width so the card shrinks to content | | `showDashForZero` | Optional | `boolean` | `false` | Displays `—` instead of `0` when value is zero | ### `MiningPoolsPanel` Dashboard card that lists configured mining pools — one row per pool, with revenue, hash rate, and an optional "Show details" action. Pure presentation: the row data + click handler come from props, shaped upstream by `usePoolRows` (or any caller producing `MiningPoolRow`s) `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/mining-pools-panel/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/mining-pools-panel/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `className` | Optional | `string` | - | Extra className for the root | | `emptyMessage` | Optional | `string` | `"No pools configured"` | Message shown when `rows` is empty | | `hideHeader` | Optional | `boolean` | `false` | Hide the title row entirely | | `isLoading` | Optional | `boolean` | `false` | Loading state — renders skeleton rows | | `label` | Optional | `string` | `"Mining Pools"` | Override the card title — defaults to `Mining Pools` | | `onShowDetails` | Optional | `(row: MiningPoolRow) => void` | - | Called when the user clicks the per-row "Show details" button | | `rows` | Optional | `MiningPoolRow[]` | `[]` | Pool rows, in display order | | `skeletonRows` | Optional | `number` | `3` | Number of skeleton rows to show while loading | ### `PoolDetailsCard` Compact key/value card for displaying pool metadata (URL, fee, worker count, etc.). Empty list renders a "No data available" placeholder `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/pool-details-card/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/pool-details-card/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `details` | Required | `PoolDetailItem[]` | - | Detail rows to render | | `className` | Optional | `string` | - | Additional class names | | `label` | Optional | `string` | - | Header label | | `underline` | Optional | `boolean` | `false` | Render an underline under the label | ### `PoolDetailsPopover` [**`Button`**](/reference/ui/components/actions/#button)-triggered popover that displays a pool's key/value details (URL, fee, worker count, status, …) inside a Radix `Dialog`. Wraps [`PoolDetailsCard`](/reference/ui/components/cards/#pooldetailscard) so the read-out matches the embedded card variant `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/pool-details-popover/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/pool-details-popover/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `details` | Required | `PoolDetailItem[]` | - | Detail rows | | `className` | Optional | `string` | - | Additional class names | | `description` | Optional | `string` | - | Dialog body description | | `disabled` | Optional | `boolean` | `false` | Disable the trigger | | `title` | Optional | `string` | - | Dialog title | | `triggerLabel` | Optional | `string` | - | Trigger button label | # Chart (/reference/ui/components/charts) Components for data visualization and charting. ## 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. - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit ### Chart container ```tsx ``` A layout wrapper for charts that provides a title/header row, interactive legend, range selector, highlighted value display, loading/empty states, and a stats footer. #### Prop data shapes > **Props are generated from this component's TypeScript types, the source of truth.** The following are supplementary (object-prop shapes, allowed values, or passthrough props), not the full prop list. ### `LegendItem` | Field | Type | Required | Description | | ----- | ---- | -------- | ----------- | | `label` | `string` | yes | Legend label | | `color` | `string` | yes | Color string (hex, hsl, etc.) | | `hidden` | `boolean` | no | Whether this dataset is currently hidden | ### `HighlightedValueProps` | Field | Type | Required | Description | | ----- | ---- | -------- | ----------- | | `value` | `string \| number` | yes | The primary value to display | | `unit` | `string` | no | Unit suffix | | `className` | `string` | no | Additional class | | `style` | `React.CSSProperties` | no | Inline style | ### `RangeSelectorProps` | Field | Type | Required | Description | | ----- | ---- | -------- | ----------- | | `options` | `RangeSelectorOption[]` | yes | `{ label, value }` items | | `value` | `string` | yes | Currently selected value | | `onChange` | `(value: string) => void` | yes | Fires when user picks a range | #### Notes - `minMaxAvg` and `timeRange` are only rendered when the chart is not loading or empty #### Example [#chart-container-example] ```tsx /** * Runnable example for ChartContainer. */ const RANGE_OPTIONS = [ { label: '1H', value: '1h' }, { label: '24H', value: '24h' }, { label: '7D', value: '7d' }, ] const LEGEND_DATA = [ { label: 'Pool A', color: '#59E8E8' }, { label: 'Pool B', color: '#FF9500' }, ] const MockChart = () => (
Chart content
) const [range, setRange] = useState('24h') return (
) } ``` #### Related API - [**`useChartDataCheck`**](/reference/ui/hooks/charts/#usechartdatacheck) @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `ActualEbitdaCard` Stat card summarising the realised EBITDA for the selected reporting window vs the prior period `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/ebitda/components/actual-ebitda-card.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/ebitda/components/actual-ebitda-card.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `value` | Required | `number` | - | - | ### `AreaChart` Presentational Chart.js area chart (Line with fill). Data must be provided via props; this component does no fetching of its own `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/area-chart/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/area-chart/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Required | `ChartData<"line", (number \| Point \| null)[], unknown>` | - | Chart data - required, provided by parent | | `className` | Optional | `string` | - | Additional class names | | `height` | Optional | `number` | `300` | Chart height in pixels | | `options` | Optional | `object` | - | Chart.js options - merged with defaults | | `tooltip` | Optional | `ChartTooltipConfig` | - | Custom HTML tooltip configuration. When provided, replaces the default Chart.js tooltip | ### `AverageDowntimeChart` Stacked bar chart of average downtime (curtailment vs operational issues). Wraps [`ChartContainer`](/reference/ui/components/charts/#chartcontainer) and [`BarChart`](/reference/ui/components/charts/#barchart); pass period labels and rate arrays via `data` `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/average-downtime-chart/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/average-downtime-chart/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `barWidth` | Optional | `number` | `38` | Max bar thickness | | `className` | Optional | `string` | - | Extra class on the container | | `data` | Optional | `AverageDowntimeChartData` | - | Period labels and rate arrays (fractions 0–1) | | `emptyMessage` | Optional | `string` | - | Message when there are no period labels or rate series | | `height` | Optional | `number` | `280` | Chart height in pixels | | `isLoading` | Optional | `boolean` | `false` | Shows loading overlay | | `showDataLabels` | Optional | `boolean` | `false` | Show values above stacked bars | | `title` | Optional | `string` | `"Monthly Average Downtime"` | Chart title (unit renders on its own line below) | | `unit` | Optional | `string` | `%` | Unit subtitle under the title | | `yTicksFormatter` | Optional | `(value: number) => string` | - | Formats Y-axis ticks, tooltips, and bar data labels (values are 0–1 rates). Defaults to rate × 100 via `formatNumber` | ### `AvgAllInCostChart` Avg All-in [**`Cost`**](/reference/ui/components/dashboards/#cost) - revenue vs cost ($/MWh) bar chart over time. Renders the OSS `SiteEnergyVsCostChart`. The revenue/cost time-series isn't carried by the cost-summary response, so consumers feed it through as a separate prop (the OSS app sources it from `useAvgAllInPowerCostData`) `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/cost/avg-all-in-cost-chart.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/cost/avg-all-in-cost-chart.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `dateRange` | Required | `FinancialDateRange \| null` | - | - | | `data` | Optional | `readonly AvgAllInCostDataPoint[]` | - | - | | `isLoading` | Optional | `boolean` | - | - | ### `BarChart` Presentational Chart.js bar chart. Data must be pre-aggregated; use grouped or stacked categories via `datasets` `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/bar-chart/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/bar-chart/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Required | `any` | - | Chart data - required, provided by parent. Use `as any` for mixed bar+line datasets | | `className` | Optional | `string` | - | Additional class for the wrapper `div` | | `formatDataLabel` | Optional | `((value: number) => string)` | - | Format data label values (default: round to nearest integer) | | `formatYLabel` | Optional | `((value: number) => string)` | - | Format Y-axis tick labels | | `height` | Optional | `number` | `300` | Chart height in pixels | | `isHorizontal` | Optional | `boolean` | `false` | Render bars horizontally (indexAxis: 'y') | | `isStacked` | Optional | `boolean` | `false` | Stack bars on top of each other | | `legendAlign` | Optional | `"center" \| "start" \| "end"` | `"start"` | Alignment of the legend labels within their position | | `legendPosition` | Optional | `"left" \| "right" \| "top" \| "bottom"` | `"top"` | Position of the legend | | `options` | Optional | `object` | - | Chart.js options - merged with defaults | | `showDataLabels` | Optional | `boolean` | `false` | Show values above each bar | | `showLegend` | Optional | `boolean` | `true` | Show built-in Chart.js legend | | `tooltip` | Optional | `ChartTooltipConfig` | - | Custom HTML tooltip configuration. When provided, replaces the default Chart.js tooltip | ### `BitcoinPriceCard` Stat card showing the BTC reference price used by the reporting view with currency and timestamp `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/ebitda/components/bitcoin-price-card.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/ebitda/components/bitcoin-price-card.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `value` | Required | `number` | - | - | ### `BitcoinProducedCard` Stat card summarising the bitcoin produced during the reporting window with delta to prior period `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/ebitda/components/bitcoin-produced-card.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/ebitda/components/bitcoin-produced-card.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `value` | Required | `number` | - | - | ### `BitcoinProducedChart` Time-series chart of bitcoin produced per day across the selected reporting window `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/ebitda/components/bitcoin-produced-chart.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/ebitda/components/bitcoin-produced-chart.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `chartData` | Required | `BarChartDataResult` | - | - | | `hasAllZeros` | Optional | `boolean` | - | - | | `height` | Optional | `number` | - | - | | `isLoading` | Optional | `boolean` | - | - | ### `BitcoinProductionCostCard` Stat card showing the average cost in USD to produce one bitcoin during the reporting window `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/ebitda/components/bitcoin-production-cost-card.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/ebitda/components/bitcoin-production-cost-card.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `value` | Required | `number` | - | - | ### `ChartContainer` Standard chrome for charts: title, optional highlighted value, legend with toggle, range selector (radio cards), loading / empty states, and a footer for min/max/avg stats `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/chart-container/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/chart-container/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `children` | Required | `React.ReactNode` | - | The chart element to render | | `className` | Optional | `string` | - | Additional class for the root element | | `empty` | Optional | `boolean` | - | Hides the chart and shows `emptyMessage` | | `emptyMessage` | Optional | `string` | `"No data available"` | Message shown when `empty` is true | | `footer` | Optional | `React.ReactNode` | - | Custom footer content rendered below the chart | | `footerClassName` | Optional | `string` | - | Additional class for the footer area | | `header` | Optional | `React.ReactNode` | - | Replaces the default `title` heading with a custom element | | `headerAction` | Optional | `React.ReactNode` | - | Optional action rendered on the right side of the header row (e.g. an expand/fullscreen toggle). Sits alongside the range selector when both are present. Purely additive - omit it and the header renders exactly as before | | `highlightedValue` | Optional | `HighlightedValueProps` | - | Large value/unit displayed alongside the legend | | `legendData` | Optional | `LegendItem[]` | - | Color-keyed legend items; each item can be toggled | | `loading` | Optional | `boolean` | - | Shows a centered [``](/reference/ui/components/feedback/#loader) overlay | | `minMaxAvg` | Optional | `Partial<{ min: string; max: string; avg: string; }>` | - | Built-in footer showing Min / Avg / Max values | | `onToggleDataset` | Optional | `((index: number) => void)` | - | Fired when a legend item is clicked | | `rangeSelector` | Optional | `RangeSelectorProps` | - | [**`Radio`**](/reference/ui/components/forms/#radio)-card time-range selector | | `timeRange` | Optional | `string` | - | Time range label shown in the footer | | `title` | Optional | `string` | - | Chart heading (renders as `

` unless `header` is provided) | | `titleExtra` | Optional | `React.ReactNode` | - | Optional node rendered immediately after the title text (e.g. an info tooltip). Only shown when `title` is set and `header` is not. Additive - omit it and the title renders exactly as before | ### `ChartExpandAction` Expand / collapse toggle rendered in a dashboard chart card's header. Swaps between a maximize and a minimize glyph based on `isExpanded` `advanced`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/dashboard/chart-expand-action.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/dashboard/chart-expand-action.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `isExpanded` | Required | `boolean` | - | Whether the parent chart is currently expanded to full width | | `onToggle` | Optional | `VoidFunction` | - | Toggles the expanded state | ### `ChartStatsFooter` [**`ChartStatsFooter`**](/reference/ui/components/charts/#chartstatsfooter) - Displays Min/Max/Avg values and optional stats grid below a chart `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/chart-stats-footer/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/chart-stats-footer/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `className` | Optional | `string` | - | Custom class name | | `minMaxAvg` | Optional | `Partial<{ min: string; max: string; avg: string; }>` | - | Min/Max/Avg values row | | `secondaryLabel` | Optional | `SecondaryLabel` | - | Secondary label displayed below stats | | `stats` | Optional | `ChartStatsFooterItem[]` | - | Additional stats displayed in a columnar grid | | `statsPerColumn` | Optional | `number` | `1` | Number of stat items per column (default: 1) | ### `CostCharts` Convenience wrapper that renders the three cost-page charts in declaration order. Pages that need bespoke layouts (e.g. [`CostContent`](/reference/ui/components/dashboards/#costcontent)'s 2x2 [**`Mosaic`**](/reference/ui/components/layout/#mosaic)) compose the individual chart components directly instead `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/cost/cost-charts.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/cost/cost-charts.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `btcPriceLog` | Required | `readonly BtcPriceTimeSeriesEntry[]` | - | - | | `costLog` | Required | `readonly CostTimeSeriesEntry[]` | - | - | | `dateRange` | Required | `FinancialDateRange \| null` | - | - | | `totals` | Required | `CostSummaryMonetaryTotals \| null` | - | - | | `avgAllInCostData` | Optional | `readonly AvgAllInCostDataPoint[]` | - | - | | `isLoading` | Optional | `boolean` | - | - | ### `DetailLegend` [**`DetailLegend`**](/reference/ui/components/charts/#detaillegend) - Enhanced chart legend with current values and percentage change indicators `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/detail-legend/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/detail-legend/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `items` | Required | `DetailLegendItem[]` | - | Legend items to display | | `className` | Optional | `string` | - | Custom class name | | `onToggle` | Optional | `((label: string, index: number) => void)` | - | Callback when a legend item is toggled | ### `DoughnutChart` [**`DoughnutChart`**](/reference/ui/components/charts/#doughnutchart) – Presentational Chart.js doughnut chart with custom HTML legend matching the MDK design `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/doughnut-chart/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/doughnut-chart/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Required | `DoughnutChartDataset[]` | - | Array of labelled slices | | `borderWidth` | Optional | `number` | `4` | Border width between segments (default: 4) | | `className` | Optional | `string` | - | Additional class for the root element | | `cutout` | Optional | `string` | `"75%"` | Doughnut cutout percentage (default: '75%') | | `formatValue` | Optional | `((value: number) => string)` | - | Formats slice values in the built-in legend and default tooltip (default: raw number) | | `height` | Optional | `number` | `260` | Chart height in pixels | | `legendPosition` | Optional | `"left" \| "right" \| "top" \| "bottom"` | `"top"` | Where to place the legend relative to the chart (default: 'top') | | `options` | Optional | `object` | - | Chart.js options – merged with defaults | | `tooltip` | Optional | `ChartTooltipConfig` | - | Custom HTML tooltip configuration. When provided, replaces the default doughnut tooltip (which shows label, value with unit, and percentage). Use `valueFormatter` to replicate the percentage display if needed | | `unit` | Optional | `string` | `""` | Unit suffix appended to values in tooltips and legends | ### `Ebitda` Top-level EBITDA section of the reporting view — pulls together metric cards, charts, and tables `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/ebitda/ebitda.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/ebitda/ebitda.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `btcProducedChartInput` | Required | `ToBarChartDataInput \| null` | - | Data for the BTC produced chart | | `currentBTCPrice` | Required | `number` | - | Current Bitcoin price in USD | | `datePicker` | Required | `React.ReactElement>` | - | Date picker element | | `ebitdaChartInput` | Required | `ToBarChartDataInput \| null` | - | Data for the EBITDA bar chart | | `hasBtcProducedAllZeros` | Required | `boolean` | - | Whether all BTC produced values are zero | | `hasDateSelection` | Required | `boolean` | - | When false, show the "select a period" hint instead of empty data | | `metrics` | Required | `EbitdaDisplayMetrics \| null` | - | Computed EBITDA metrics | | `showEbitdaBarChart` | Required | `boolean` | - | Show the EBITDA bar chart | | `errors` | Optional | `string[]` | `[]` | Error messages to display | | `isLoading` | Optional | `boolean` | `false` | Loading state | | `setCostHref` | Optional | `string` | - | Optional URL for the "Set Monthly [**`Cost`**](/reference/ui/components/dashboards/#cost)" control (hidden when omitted) | ### `EbitdaCharts` Chart panel inside the EBITDA section visualising revenue, cost, and EBITDA over time `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/ebitda/ebitda-charts.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/ebitda/ebitda-charts.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `btcDisplayData` | Required | `BarChartDataResult` | - | - | | `ebitdaChartData` | Required | `BarChartDataResult` | - | - | | `hasBtcProducedAllZeros` | Required | `boolean` | - | - | | `isLoading` | Required | `boolean` | - | - | | `showEbitdaBarChart` | Required | `boolean` | - | - | ### `EbitdaHodlCard` Stat card projecting EBITDA assuming all produced bitcoin is held instead of sold `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/ebitda/components/ebitda-hodl-card.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/ebitda/components/ebitda-hodl-card.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `currentBTCPrice` | Required | `number` | - | - | | `value` | Required | `number` | - | - | ### `EbitdaMetrics` Row of summary metric cards across the top of the EBITDA section (actual, hodl, selling, cost) `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/ebitda/ebitda-metrics.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/ebitda/ebitda-metrics.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `currentBTCPrice` | Required | `number` | - | - | | `metrics` | Required | `EbitdaDisplayMetrics` | - | - | ### `EbitdaSellingCard` Stat card projecting EBITDA assuming all produced bitcoin is sold at the daily reference price `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/ebitda/components/ebitda-selling-card.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/ebitda/components/ebitda-selling-card.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `value` | Required | `number` | - | - | ### `EnergyBalance` Full energy balance view with tabbed revenue and cost sections, charts, and metric cards `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/energy-balance/energy-balance.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/energy-balance/energy-balance.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `onCostDisplayModeChange` | Required | `(mode: DisplayMode) => void` | - | Called when the user toggles USD / BTC on the cost tab | | `onRevenueDisplayModeChange` | Required | `(mode: DisplayMode) => void` | - | Called when the user toggles USD / BTC on the revenue tab | | `onTabChange` | Required | `(tab: EnergyBalanceTab) => void` | - | Called when the user switches between Revenue and [**`Cost`**](/reference/ui/components/dashboards/#cost) tabs | | `viewModel` | Required | `EnergyBalanceViewModel` | - | All display state: chart inputs, metrics, active tab, display modes, loading/error flags. Returned directly by [`useEnergyBalanceViewModel`](/reference/ui/hooks/charts/#useenergybalanceviewmodel) | | `isDemoMode` | Optional | `boolean` | `false` | Suppresses error banners in demo/mock environments | | `setCostHref` | Optional | `string` | - | Optional URL for the "Set Monthly [**`Cost`**](/reference/ui/components/dashboards/#cost)" control (hidden when omitted) | | `timeframeControls` | Optional | `React.ReactNode` | - | Slot for timeframe / date-range controls rendered by the host app | ### `EnergyBalanceCostCharts` Layout container for the energy cost tab charts: revenue-vs-cost bar chart and power line chart `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/energy-balance/energy-balance-cost-charts.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/energy-balance/energy-balance-cost-charts.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `barLabelFormatter` | Required | `(v: number) => string` | - | - | | `btcUnit` | Required | `string \| null` | - | - | | `costChartData` | Required | `BarChartDataResult` | - | - | | `displayMode` | Required | `"BTC" \| "USD"` | - | - | | `onDisplayModeChange` | Required | `(mode: DisplayMode) => void` | - | - | | `periodType` | Required | `"month" \| "week" \| "day"` | - | - | | `powerChartInput` | Required | `ThresholdLineChartInput` | - | - | | `showCostBarChart` | Required | `boolean` | - | Show the revenue-vs-cost bar chart only for non-daily periods | ### `EnergyBalanceCostMetrics` Grid of stat cards summarising energy cost metrics for the selected period `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/energy-balance/energy-balance-cost-metrics.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/energy-balance/energy-balance-cost-metrics.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `metrics` | Required | `EnergyCostMetrics` | - | - | ### `EnergyBalancePowerChart` Line chart visualising power consumption against threshold for the energy balance view `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/energy-balance/components/energy-balance-power-chart.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/energy-balance/components/energy-balance-power-chart.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `chartInput` | Required | `ThresholdLineChartInput` | - | Series and optional threshold line for power vs availability | | `periodType` | Required | `"month" \| "week" \| "day"` | - | Controls x-axis date formatting (`month` uses `MM-yy`) | | `fillHeight` | Optional | `boolean` | `false` | Stretch the panel and chart to fill a mosaic cell (uses height `320` and `mdk-energy-balance__panel--fill`). Used on the revenue tab power column in [`EnergyBalanceRevenueCharts`](/reference/ui/components/charts/#energybalancerevenuecharts) | | `height` | Optional | `number` | `280` | Chart height when `fillHeight` is false | ### `EnergyBalanceRevenueCharts` [**`Mosaic`**](/reference/ui/components/layout/#mosaic) layout of revenue, downtime, and power charts for the energy balance revenue tab `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/energy-balance/energy-balance-revenue-charts.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/energy-balance/energy-balance-revenue-charts.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `averageDowntimeData` | Required | `AverageDowntimeChartData` | - | - | | `barLabelFormatter` | Required | `(v: number) => string` | - | - | | `displayMode` | Required | `"BTC" \| "USD"` | - | - | | `onDisplayModeChange` | Required | `(mode: DisplayMode) => void` | - | - | | `periodType` | Required | `"month" \| "week" \| "day"` | - | - | | `powerChartInput` | Required | `ThresholdLineChartInput` | - | - | | `revenueChartData` | Required | `BarChartDataResult` | - | - | | `revenueMetrics` | Required | `EnergyRevenueMetrics` | - | - | ### `EnergyBalanceRevenueMetrics` Grid of stat cards summarising energy revenue metrics for the selected period `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/energy-balance/energy-balance-revenue-metrics.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/energy-balance/energy-balance-revenue-metrics.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `metrics` | Required | `EnergyRevenueMetrics` | - | - | ### `EnergyCostChart` Bar chart comparing site revenue vs cost per MWh, with USD/BTC currency toggle `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/energy-balance/components/energy-cost-chart.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/energy-balance/components/energy-cost-chart.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `barLabelFormatter` | Required | `(v: number) => string` | - | - | | `btcUnit` | Required | `string \| null` | - | - | | `chartData` | Required | `BarChartDataResult` | - | - | | `displayMode` | Required | `"BTC" \| "USD"` | - | - | | `onDisplayModeChange` | Required | `(mode: DisplayMode) => void` | - | - | | `height` | Optional | `number` | - | - | ### `EnergyMetricCard` Stat card for a single energy balance metric `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/energy-balance/components/energy-metric-card.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/energy-balance/components/energy-metric-card.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `name` | Required | `string` | - | Metric label shown on the card | | `unit` | Required | `string` | - | Unit suffix shown next to the value | | `value` | Required | `number` | - | Metric value, formatted via `formatNumber` | | `fallback` | Optional | `string` | - | Text shown when `value` can't be formatted | ### `EnergyReportMinerTypeView` Energy report — power consumption grouped by miner model (latest day in range) `advanced`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/composite/reporting-tool/operational/energy-report/miner-type-view/miner-type-view.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/composite/reporting-tool/operational/energy-report/miner-type-view/miner-type-view.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `containers` | Optional | `Container[]` | - | - | | `groupedConsumption` | Optional | `MetricsConsumptionGroupedResponse` | - | - | | `isLoading` | Optional | `boolean` | - | - | | `onTimeFrameChange` | Optional | `((start: Date, end: Date) => void)` | - | - | ### `EnergyReportMinerUnitView` Energy report — power consumption grouped by mining unit / container `advanced`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/composite/reporting-tool/operational/energy-report/miner-unit-view/miner-unit-view.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/composite/reporting-tool/operational/energy-report/miner-unit-view/miner-unit-view.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `containers` | Optional | `Container[]` | - | - | | `groupedConsumption` | Optional | `MetricsConsumptionGroupedResponse` | - | - | | `isLoading` | Optional | `boolean` | - | - | | `onTimeFrameChange` | Optional | `((start: Date, end: Date) => void)` | - | - | ### `EnergyReportSiteView` Energy report site tab — power trend, power-mode table, and per–mining-unit activity cards `advanced`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/composite/reporting-tool/operational/energy-report/site-view/site-view.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/composite/reporting-tool/operational/energy-report/site-view/site-view.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `dateRange` | Required | `EnergyReportDateRange` | - | - | | `consumptionError` | Optional | `unknown` | - | - | | `consumptionFetching` | Optional | `boolean` | - | - | | `consumptionLoading` | Optional | `boolean` | - | - | | `consumptionLog` | Optional | `MetricsConsumptionLogEntry[]` | - | - | | `containers` | Optional | `EnergyReportContainer[]` | - | - | | `containersLoading` | Optional | `boolean` | - | - | | `nominalConfigLoading` | Optional | `boolean` | - | - | | `nominalPowerAvailabilityMw` | Optional | `number \| null` | - | - | | `onDateRangeChange` | Optional | `((range: EnergyReportDateRange) => void)` | - | - | | `onRefetchSnapshot` | Optional | `VoidFunction` | - | - | | `snapshotLoading` | Optional | `boolean` | - | - | | `tailLog` | Optional | `EnergyReportTailLogItem[][]` | - | - | | `tailLogLoading` | Optional | `boolean` | - | - | ### `EnergyRevenueChart` Bar chart showing site energy revenue per MWh, with USD/BTC currency toggle `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/energy-balance/components/energy-revenue-chart.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/energy-balance/components/energy-revenue-chart.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `barLabelFormatter` | Required | `(v: number) => string` | - | - | | `chartData` | Required | `BarChartDataResult` | - | - | | `displayMode` | Required | `"BTC" \| "USD"` | - | - | | `onDisplayModeChange` | Required | `(mode: DisplayMode) => void` | - | - | | `height` | Optional | `number` | - | - | ### `GaugeChart` [**`GaugeChart`**](/reference/ui/components/charts/#gaugechart) - Presentational gauge / speedometer chart. Implementation note: the gauge is drawn by a pure-SVG internal implementation (see `./gauge-svg.tsx`), so the component is bundler- and runtime-agnostic and carries no third-party runtime dependency. The gauge packages on NPM are published CommonJS-only with a `module` field pointing at the same CJS file, which crashes under ESM bundlers that add no `__esModule ? .default : module` interop shim (Webpack 4, raw esbuild, some SSR setups) with React's "Element type is invalid" error `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/gauge-chart/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/gauge-chart/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `percent` | Required | `number` | - | Value between 0 and 1 (e.g. 0.75 = 75%). Values outside the range are clamped | | `arcsLength` | Optional | `number[]` | - | Custom arc-segment proportions (auto-normalised), overriding `nrOfLevels`. e.g. `[0.7, 0.3]` for a progress-style gauge whose first arc is the fill | | `arcWidth` | Optional | `number` | `0.2` | Arc thickness as a fraction of the gauge radius (0–1) | | `className` | Optional | `string` | - | Additional class for the root `div` | | `colors` | Optional | `string[]` | `[COLOR.GREEN, COLOR.RED]` | Arc colours in HEX format | | `formatTextValue` | Optional | `((percent: number) => string)` | - | Format the center label from the clamped fraction (0–1) | | `height` | Optional | `string \| number` | `200` | Chart height in pixels or any CSS length (e.g. `'200px'` or `'50%'`) | | `hideNeedle` | Optional | `boolean` | `false` | Hide the needle + hub (e.g. for a progress-style gauge) | | `hideText` | Optional | `boolean` | `false` | Hide the percentage text rendered inside the gauge | | `id` | Optional | `string` | `"mdk-gauge-chart"` | Stable id used for the gauge's accessibility labels | | `maxWidth` | Optional | `number` | - | Maximum width in pixels | | `needleColor` | Optional | `string` | `COLOR.STEEL_GRAY` | Needle + hub colour | | `nrOfLevels` | Optional | `number` | `3` | Number of arc segments. Ignored when `arcsLength` is provided | ### `Hashrate` Top-level hashrate reporting section - composes the site / miner-type / mining-unit drilldowns into a tabbed shell. Each tab fetches independently because the three views use different `groupBy` axes; the composite stitches them via per-tab prop bags `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/hashrate/hashrate.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/hashrate/hashrate.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `defaultTab` | Optional | `"site-view" \| "miner-type-view" \| "mining-unit-view"` | `"site-view"` | Tab selected on first render. Defaults to the Site View | | `minerTypeView` | Optional | `HashrateMinerTypeViewProps` | - | Props forwarded to the Miner Type View tab | | `miningUnitView` | Optional | `HashrateMiningUnitViewProps` | - | Props forwarded to the Mining Unit View tab | | `siteView` | Optional | `HashrateSiteViewProps` | - | Props forwarded to the Site View tab | ### `HashrateMinerTypeView` [**`Hashrate`**](/reference/ui/components/charts/#hashrate) drilldown grouped by miner model - bar chart of the latest hashrate per miner type, with an optional multi-select filter `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/hashrate/tabs/miner-type-view/miner-type-view.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/hashrate/tabs/miner-type-view/miner-type-view.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `dateRange` | Optional | `HashrateDateRange` | - | Selected date range | | `isLoading` | Optional | `boolean` | `false` | Drives the chart spinner | | `log` | Optional | `HashrateGroupedLog` | `[]` | [**`Hashrate`**](/reference/ui/components/charts/#hashrate) log grouped by miner type (groupBy=miner) | | `onDateRangeChange` | Optional | `((range: HashrateDateRange) => void)` | - | Fires when the user picks a new range | | `onReset` | Optional | `VoidFunction` | - | Optional reset handler | ### `HashrateMiningUnitView` [**`Hashrate`**](/reference/ui/components/charts/#hashrate) drilldown grouped by mining unit / container - bar chart of the latest hashrate per container with an optional multi-select filter. Drops BE-leaked rollup keys (`group-N`, `maintenance`) via the utils layer `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/hashrate/tabs/mining-unit-view/mining-unit-view.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/hashrate/tabs/mining-unit-view/mining-unit-view.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `dateRange` | Optional | `HashrateDateRange` | - | Selected date range | | `isLoading` | Optional | `boolean` | `false` | Drives the chart spinner | | `log` | Optional | `HashrateGroupedLog` | `[]` | [**`Hashrate`**](/reference/ui/components/charts/#hashrate) log grouped by container / mining unit (groupBy=container) | | `onDateRangeChange` | Optional | `((range: HashrateDateRange) => void)` | - | Fires when the user picks a new range | | `onReset` | Optional | `VoidFunction` | - | Optional reset handler | ### `HashrateSiteView` Site-level hashrate trend - aggregates hashrate across the whole site for the selected date range, with an optional miner-type filter that scopes the sum to a subset `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/hashrate/tabs/site-view/site-view.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/hashrate/tabs/site-view/site-view.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `dateRange` | Optional | `HashrateDateRange` | - | Selected date range used by the host to drive the query | | `isLoading` | Optional | `boolean` | `false` | Loading state - drives the chart spinner | | `log` | Optional | `HashrateGroupedLog` | `[]` | [**`Hashrate`**](/reference/ui/components/charts/#hashrate) log grouped by miner type | | `onDateRangeChange` | Optional | `((range: HashrateDateRange) => void)` | - | Fires when the user picks a new range from the [**`DateRangePicker`**](/reference/ui/components/forms/#daterangepicker) | | `onReset` | Optional | `VoidFunction` | - | Optional reset handler shown as a "Reset" button next to the date picker | ### `Heatmap` [**`Heatmap`**](/reference/ui/components/charts/#heatmap) — a generic grid of value-coloured cells on a low→high gradient. Presentational and domain-agnostic: pass a row-major matrix of cells and an optional `[min, max]` range (auto-derived otherwise). Use `renderCell` to overlay domain content (socket borders, selection, tooltips) without forking the primitive — the grid still owns each cell's background colour. Pair with [`HeatmapLegend`](/reference/ui/components/charts/#heatmaplegend) for a gradient scale `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/heatmap/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/heatmap/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Required | `HeatmapCell[][]` | - | Rows of cells (row-major). Rows may be ragged | | `ariaLabel` | Optional | `string` | `"Heatmap"` | Accessible label for the grid | | `className` | Optional | `string` | - | Additional class for the root element | | `colors` | Optional | `readonly string[]` | `HEATMAP_GRADIENT` | Gradient stops low→high. Defaults to the cold→hot `HEATMAP_GRADIENT` | | `emptyColor` | Optional | `string` | `#000000` | Colour used for `null` cells | | `max` | Optional | `number` | `auto` | Range ceiling (maps to the last gradient stop); auto-derived from the finite values when omitted | | `min` | Optional | `number` | `auto` | Range floor (maps to the first gradient stop); auto-derived from the finite values when omitted | | `renderCell` | Optional | `((cell: HeatmapCell, context: HeatmapCellContext) => React.ReactNode)` | - | Override the cell's inner content — e.g. to overlay socket borders, selection, or tooltips for a PDU grid. The primitive still owns the cell's background colour (passed via `context.color`) | | `showValues` | Optional | `boolean` | `false` | Render each cell's value/label as text | ### `HeatmapLegend` [**`HeatmapLegend`**](/reference/ui/components/charts/#heatmaplegend) — a gradient bar with low/high scale labels, matching the gradient used by [`Heatmap`](/reference/ui/components/charts/#heatmap) `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/heatmap/heatmap-legend.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/heatmap/heatmap-legend.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `max` | Required | `string \| number` | - | Value (or pre-formatted label) at the high end of the scale | | `min` | Required | `string \| number` | - | Value (or pre-formatted label) at the low end of the scale | | `className` | Optional | `string` | - | Additional class for the root element | | `colors` | Optional | `readonly string[]` | `HEATMAP_GRADIENT` | Gradient stops low→high | | `label` | Optional | `string` | - | Heading above the gradient bar (e.g. "Temperature") | | `unit` | Optional | `string` | - | Unit suffix appended to `min`/`max` | ### `LineChart` Customisable Chart.js line chart with built-in zoom, tooltip, and legend. Data is passed via props; the component does no fetching `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/line-chart/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/line-chart/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Required | `LineChartData` | - | Data of the chart | | `backgroundColor` | Optional | `string` | - | Background color of the chart | | `beginAtZero` | Optional | `boolean` | - | Starts the value axis at 0 | | `chartRef` | Optional | `React.MutableRefObject` | - | Mutable ref to hold the LightWeightCharts reference | | `customDateFormat` | Optional | `string` | - | Custom date format | | `customLabel` | Optional | `string` | - | TODO: Doc | | `disableAutoRange` | Optional | `boolean` | - | Disable automatically determining range | | `fadedBackground` | Optional | `boolean` | - | Use a faded background | | `fixedTimezone` | Optional | `string` | - | Applies offset if provided, otherwise timestamps are assumed to already be in local time. Otherwise, use browser's current timezone offset for consistent time display | | `height` | Optional | `number` | `240` | Controls the height of the chart | | `horizontalLineLabelVisible` | Optional | `boolean` | - | Show horizontal line at mouse position | | `priceFormatter` | Optional | `((value: number) => string)` | - | Callback to format ticks on y axis | | `roundPrecision` | Optional | `number` | - | The number of decimals to show | | `shouldResetZoom` | Optional | `boolean` | - | Wether to Reset Zoom | | `showDateInTooltip` | Optional | `boolean` | - | Show date line in tooltip | | `showPointMarkers` | Optional | `boolean` | - | Show a marker on the line | | `skipMinWidth` | Optional | `boolean` | - | Do not enforce a min width | | `skipRound` | Optional | `boolean` | - | Prevent rounding of values | | `timeline` | Optional | `string` | - | TODO: DOC | | `uniformDistribution` | Optional | `boolean` | - | Changes horizontal scale marks generation. With this flag equal to true, marks of the same weight are either all drawn or none are drawn at all | | `unit` | Optional | `string` | `""` | The unit to display with values | | `verticalLineLabelVisible` | Optional | `boolean` | - | Show vertical line at mouse position | | `yTicksFormatter` | Optional | `((value: number) => string)` | - | Callback to format ticks on y axis. If `priceFormatter` is given. It would be used instead | ### `LineChartCard` Composable line-chart card with title, timeline range selector, legend (basic or detailed), error boundary, and an optional min/max/avg footer. Accepts either pre-shaped `data` or `rawData` + a `dataAdapter` callback so upstream domain components can keep their data wrangling local `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/line-chart-card/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/line-chart-card/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `chartProps` | Optional | `Partial` | - | Pass-through props to the core [**`LineChart`**](/reference/ui/components/charts/#linechart) | | `chartRef` | Optional | `React.MutableRefObject` | - | Ref to the lightweight-charts IChartApi | | `className` | Optional | `string` | - | Custom class name | | `data` | Optional | `LineChartCardData` | - | Pre-adapted chart data (use this OR rawData+dataAdapter) | | `dataAdapter` | Optional | `(data: unknown) => LineChartCardData` | - | Adapter to transform rawData into LineChartCardData | | `defaultTimeline` | Optional | `string` | `first option` | Default timeline when uncontrolled | | `detailLegends` | Optional | `boolean` | `false` | Show detail legends with current values | | `headerAction` | Optional | `React.ReactNode` | - | Optional action rendered on the right of the card header (e.g. an expand toggle). Passed straight through to [`ChartContainer`](/reference/ui/components/charts/#chartcontainer). Additive - omit it and the card header is unchanged | | `isLoading` | Optional | `boolean` | `false` | Loading state | | `minHeight` | Optional | `string \| number` | `350` | Minimum chart height | | `onTimelineChange` | Optional | `(timeline: string) => void` | - | Callback when timeline changes | | `rawData` | Optional | `unknown` | - | Raw data to be transformed by dataAdapter | | `shouldResetZoom` | Optional | `boolean` | `true` | Whether to reset zoom on timeline change (default: true) | | `timeline` | Optional | `string` | - | Controlled timeline value | | `timelineOptions` | Optional | `TimelineOption[]` | - | Timeline range selector options | | `title` | Optional | `string` | - | Chart title | | `titleExtra` | Optional | `React.ReactNode` | - | Optional node rendered next to the title (e.g. an info tooltip). Passed straight through to [`ChartContainer`](/reference/ui/components/charts/#chartcontainer). Additive | ### `MinMaxAvg` Min / Max / Avg summary row with consistent MDK label and value styling `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/min-max-avg/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/min-max-avg/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `avg` | Optional | `string` | - | Average value (hidden if empty) | | `className` | Optional | `string` | - | Additional root class | | `max` | Optional | `string` | - | Maximum value (hidden if empty) | | `min` | Optional | `string` | - | Minimum value (hidden if empty) | ### `MonthlyEbitdaChart` Bar chart comparing EBITDA across the most recent months for trend visualisation `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/ebitda/components/monthly-ebitda-chart.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/ebitda/components/monthly-ebitda-chart.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `chartData` | Required | `BarChartDataResult` | - | - | | `height` | Optional | `number` | - | - | ### `OperationalHashrateChart` [**`Hashrate`**](/reference/ui/components/charts/#hashrate) trend card for the operational dashboard. Renders the site hashrate over time (TH/s) with an optional nominal reference line, plus an expand toggle. Purely presentational - pass pre-shaped data from [`useOperationsDashboard`](/reference/ui/hooks/misc/#useoperationsdashboard) `advanced`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/dashboard/hashrate-chart.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/dashboard/hashrate-chart.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Optional | `LineChartCardData` | - | - | | `isExpanded` | Optional | `boolean` | - | - | | `isLoading` | Optional | `boolean` | - | - | | `onToggleExpand` | Optional | `VoidFunction` | - | - | ### `OperationalMinersStatusChart` Miners-status card for the operational dashboard. Renders a stacked daily breakdown of miner states (online / error / offline / sleep / maintenance) with an expand toggle. Purely presentational - pass pre-shaped data from [`useOperationsDashboard`](/reference/ui/hooks/misc/#useoperationsdashboard) `advanced`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/dashboard/miners-status-chart.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/dashboard/miners-status-chart.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Optional | `MinersStatusChartData` | - | - | | `isExpanded` | Optional | `boolean` | - | - | | `isLoading` | Optional | `boolean` | - | - | | `onToggleExpand` | Optional | `VoidFunction` | - | - | ### `OperationalPowerConsumptionChart` Power-consumption trend card for the operational dashboard. Renders site power draw over time (MW) with an optional power-availability reference line, plus an expand toggle. Purely presentational - pass pre-shaped data from [`useOperationsDashboard`](/reference/ui/hooks/misc/#useoperationsdashboard) `advanced`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/dashboard/power-consumption-chart.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/dashboard/power-consumption-chart.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Optional | `LineChartCardData` | - | - | | `isExpanded` | Optional | `boolean` | - | - | | `isLoading` | Optional | `boolean` | - | - | | `onToggleExpand` | Optional | `VoidFunction` | - | - | ### `OperationalSiteEfficiencyChart` Site-efficiency trend card for the operational dashboard. Renders measured site efficiency over time (W/TH/s) with an optional nominal reference line, an info tooltip, and an expand toggle. Purely presentational - pass pre-shaped data from [`useOperationsDashboard`](/reference/ui/hooks/misc/#useoperationsdashboard) `advanced`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/dashboard/site-efficiency-chart.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/dashboard/site-efficiency-chart.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Optional | `LineChartCardData` | - | - | | `isExpanded` | Optional | `boolean` | - | - | | `isLoading` | Optional | `boolean` | - | - | | `onToggleExpand` | Optional | `VoidFunction` | - | - | ### `OperationsEnergyChart` Doughnut breakdown of Operations vs Energy cost (in USD totals). Mirrors the OSS [`OperationsEnergyCostChart`](/reference/ui/components/charts/#operationsenergycostchart). Returns the empty-state placeholder when both totals are zero (the OSS chart hides both slices in that case) `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/cost/operations-energy-chart.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/cost/operations-energy-chart.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `totals` | Required | `CostSummaryMonetaryTotals \| null` | - | - | | `isLoading` | Optional | `boolean` | - | - | ### `OperationsEnergyCostChart` Doughnut breakdown of Operations vs Energy cost (USD per MWh). Wraps [`ChartContainer`](/reference/ui/components/charts/#chartcontainer) and [`DoughnutChart`](/reference/ui/components/charts/#doughnutchart); pass `operationalCostsUSD` and `energyCostsUSD` via `data` `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/operations-energy-cost-chart/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/operations-energy-cost-chart/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `className` | Optional | `string` | - | Extra class on the container | | `data` | Optional | `Partial<{ energyCostsUSD: number; operationalCostsUSD: number; }>` | - | `operationalCostsUSD` and `energyCostsUSD` | | `emptyMessage` | Optional | `string` | - | Message when both costs are zero or missing | | `height` | Optional | `number` | `200` | Doughnut height in pixels | | `isLoading` | Optional | `boolean` | `false` | Shows loading overlay on the chart area | | `title` | Optional | `string` | `"Operations vs Energy Cost"` | Chart title | | `unit` | Optional | `string` | `$/MWh` | Subtitle and tooltip unit label | ### `PowerModeTimelineChart` Timeline chart for power-mode state changes over time. Wraps [`TimelineChart`](/reference/ui/components/charts/#timelinechart) with mining-specific data shaping `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/dashboard/power-mode-timeline-chart/power-mode-timeline-chart.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/dashboard/power-mode-timeline-chart/power-mode-timeline-chart.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Optional | `PowerModeTimelineEntry[]` | `[]` | Initial power-mode entries (each with start/end ts + mode) | | `dataUpdates` | Optional | `PowerModeTimelineEntry[]` | `[]` | Streaming updates appended to the initial data | | `isLoading` | Optional | `boolean` | `false` | Show a loading skeleton instead of the chart | | `timezone` | Optional | `string` | `"UTC"` | IANA timezone string for x-axis tick formatting | | `title` | Optional | `string` | `CHART_TITLES.POWER_MODE_TIMELINE` | Chart title | ### `ProductionCostChart` Production cost over time, overlaid with BTC price. Mirrors the OSS `ProductionCostPriceChart` - both series rendered as bars on the same x-axis (bucket labels derived from `dateRange.period`) `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/cost/production-cost-chart.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/cost/production-cost-chart.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `btcPriceLog` | Required | `readonly BtcPriceTimeSeriesEntry[]` | - | - | | `costLog` | Required | `readonly CostTimeSeriesEntry[]` | - | - | | `dateRange` | Required | `FinancialDateRange \| null` | - | - | | `isLoading` | Optional | `boolean` | - | - | ### `RevenueChart` Stacked bar chart displaying monthly revenue per site. Automatically switches between BTC and Sats display based on value scale. Receives pre-fetched data as props — no internal data fetching `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/revenue-chart/revenue-chart.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/revenue-chart/revenue-chart.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Optional | `RevenueDataItem[]` | `[]` | Raw API response, each entry is one time period with site IDs as dynamic keys | | `isLoading` | Optional | `boolean` | `false` | Shows a loading spinner while data is being fetched | | `legendAlign` | Optional | `"center" \| "start" \| "end"` | `"start"` | Chart legend alignment | | `legendPosition` | Optional | `"left" \| "right" \| "top" \| "bottom"` | `"bottom"` | Chart legend position | | `siteList` | Optional | `(string \| SiteItem)[]` | `[]` | List to resolve site IDs to display names | ### `ThresholdLineChart` Line chart with optional horizontal threshold lines. Wraps [`ChartContainer`](/reference/ui/components/charts/#chartcontainer) and [`LineChart`](/reference/ui/components/charts/#linechart); pass `series` and optional `thresholds` via `data` `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/threshold-line-chart/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/threshold-line-chart/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `className` | Optional | `string` | - | Extra class on the container | | `data` | Optional | `ThresholdLineChartData` | - | `series` points plus optional `thresholds` lines | | `emptyMessage` | Optional | `string` | - | Message when data is missing or all zero | | `height` | Optional | `number` | `280` | Chart height in pixels (`360` when `isTall`) | | `isLegendVisible` | Optional | `boolean` | `true` | Legend with click-to-hide series | | `isTall` | Optional | `boolean` | `false` | When true, uses a taller default height (360px) | | `title` | Optional | `string` | - | Chart title (unit appended when `unit` is set) | | `unit` | Optional | `string` | - | Shown in title and axis/tooltip formatting | | `yTicksFormatter` | Optional | `(value: number) => string` | - | Custom Y-axis tick labels | ### `TimelineChart` Discrete-event timeline chart (e.g. miner state over time) with a category legend. Supports streaming updates via `newData` `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/timeline-chart/timeline-chart.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/timeline-chart/timeline-chart.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `initialData` | Required | `TimelineChartData` | - | Initial timeline data | | `axisTitleText` | Optional | `AxisTitleText` | `{ x: "Time", y: "" }` | Axis title strings | | `height` | Optional | `number` | - | Chart pixel height | | `isLoading` | Optional | `boolean` | `false` | Show loader | | `newData` | Optional | `TimelineChartData` | - | Streaming updates appended to the initial data | | `range` | Optional | `ChartRange` | - | Visible time window | | `skipUpdates` | Optional | `boolean` | `false` | Ignore `newData` | | `title` | Optional | `string` | - | Chart title | # Dashboard (/reference/ui/components/dashboard) Components for building dashboard layouts and containers. ## 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. - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `AlarmsBellButton` Top-bar bell button with a three-line severity badge (critical / high / medium). Counts are caller-provided so the button stays domain-agnostic; pair with `useActiveIncidents` or `useSiteMinerCounts` to wire them. `onClick` fires for the bell itself; pass `onSeverityClick` to make each count its own button (severity-filtered deep-link). The two are independent — clicking a severity does not also fire `onClick` `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/header-actions/alarms-bell-button.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/header-actions/alarms-bell-button.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `className` | Optional | `string` | - | Additional class names | | `counts` | Optional | `AlarmsBellButtonCounts` | `{}` | Severity-bucketed alarm counts rendered in the stacked badge | | `label` | Optional | `string` | `"Active alarms"` | Accessible label. Defaults to "Active alarms" | | `onClick` | Optional | `((event: React.MouseEvent) => void)` | - | Click handler — typically opens an alerts panel or routes to /alerts | | `onSeverityClick` | Optional | `((severity: AlarmSeverity, event: React.MouseEvent) => void)` | - | Click handler for an individual severity count. When provided, each badge row becomes its own button so an operator can jump straight to the alerts page filtered by that severity (e.g. `/alerts?severity=critical`). When omitted, the counts render as plain (non-interactive) text | ### `DashboardDateRangePicker` Dashboard-friendly wrapper around the core [`DateRangePicker`](/reference/ui/components/forms/#daterangepicker) that speaks `{ start, end }` epoch-millisecond timestamps instead of `Date` objects, so it drops straight into `useDashboardDateRange` from `@tetherto/mdk-react-adapter` `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/dashboard/dashboard-date-range-picker/dashboard-date-range-picker.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/dashboard/dashboard-date-range-picker/dashboard-date-range-picker.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `onChange` | Required | `(next: DashboardDateRange) => void` | - | Fires with the next `{ start, end }` window when the user applies a range | | `value` | Required | `DashboardDateRange` | - | Current range as `{ start, end }` epoch-millisecond timestamps | | `className` | Optional | `string` | - | Optional class hook | | `dateFormat` | Optional | `string` | `"dd/MM/yyyy"` | Display format. Defaults to `dd/MM/yyyy` | | `disabled` | Optional | `boolean` | `false` | Disable the trigger | ### `ExportButton` Split-button trigger for downloading the current dashboard state. The left half labels the action (`↓ Export`); the right half opens a dropdown with the available formats and invokes `onExport(format)` on selection `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/dashboard/export-button/export-button.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/dashboard/export-button/export-button.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `onExport` | Required | `(format: ExportFormat) => void` | - | Fires with the chosen format when the user picks an item | | `className` | Optional | `string` | - | Optional class hook on the wrapper | | `disabled` | Optional | `boolean` | `false` | Disable the button | | `formats` | Optional | `readonly ExportFormat[]` | `['csv', 'json']` | Formats to offer in the dropdown — defaults to `['csv', 'json']` | | `label` | Optional | `string` | `"Export"` | [**`Button`**](/reference/ui/components/actions/#button) label — defaults to `'Export'` | ### `HeaderConsumptionBox` Single-row consumption cell for the dashboard's header strip. The `1.663` style numeric is rendered in orange (the warning token) to match the reference app's visual treatment `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/header-stats/header-consumption-box.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/header-stats/header-consumption-box.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `className` | Optional | `string` | - | Additional class names | | `icon` | Optional | `React.ReactNode` | - | Icon shown next to the stat | | `unit` | Optional | `string` | `MW` | Unit label — defaults to `MW` | | `valueMw` | Optional | `number` | - | Current site-level power consumption, in megawatts | ### `HeaderEfficiencyBox` Single-row efficiency cell for the dashboard's header strip. Displays the W/TH/s metric derived from `power_w / hashrate_th` `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/header-stats/header-efficiency-box.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/header-stats/header-efficiency-box.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `className` | Optional | `string` | - | Additional class names | | `icon` | Optional | `React.ReactNode` | - | Icon shown next to the stat | | `unit` | Optional | `string` | `W/TH/S` | Unit label — defaults to `W/TH/S` | | `valueWthS` | Optional | `number` | - | Efficiency in watts per TH/s | ### `HeaderHashrateBox` Two-row hashrate cell for the dashboard's header strip. Shows the app-side and pool-side aggregate hashrate side by side. Values fall back to `—` when undefined `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/header-stats/header-hashrate-box.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/header-stats/header-hashrate-box.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `appLabel` | Optional | `string` | `WEBAPP_SHORT_NAME ("APP")` | [**`Label`**](/reference/ui/components/forms/#label) for the app-side row | | `appPhs` | Optional | `number` | - | App-side aggregate hashrate in PH/s | | `className` | Optional | `string` | - | Additional class names | | `fractionDigits` | Optional | `number` | `3` | Decimal places shown for both values — defaults to `3` | | `icon` | Optional | `React.ReactNode` | - | Icon shown next to the stat | | `poolPhs` | Optional | `number` | - | Pool-side aggregate hashrate in PH/s | | `unit` | Optional | `string` | `PH/s` | [**`Hashrate`**](/reference/ui/components/charts/#hashrate) unit label — defaults to `PH/s` | ### `HeaderMinersBox` Two-row miner-count cell for the dashboard's header strip. Top row carries the app-side `online / error / offline` breakdown; the bottom row shows the pool-side equivalent. Numbers fall back to `—` when undefined `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/header-stats/header-miners-box.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/header-stats/header-miners-box.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `appLabel` | Optional | `string` | `WEBAPP_SHORT_NAME ("APP")` | [**`Label`**](/reference/ui/components/forms/#label) for the app-side row | | `appTotal` | Optional | `number` | - | Optional app-side meta line — total miners reporting to the app | | `className` | Optional | `string` | - | Additional class names | | `error` | Optional | `number` | - | Miners flagged in warning (the small amber count) | | `icon` | Optional | `React.ReactNode` | - | Icon shown next to the "Miners" label. Caller-provided so the package stays icon-agnostic | | `offline` | Optional | `number` | - | Miners offline (the small red count) | | `online` | Optional | `number` | - | Online miners (the `158` numerator) | | `poolMismatch` | Optional | `number` | - | Optional pool-side mismatch count (red) | | `poolOnline` | Optional | `number` | - | Optional pool-side online count (green) | | `poolTotal` | Optional | `number` | - | Optional pool-side meta — total miners as reported by upstream pools | | `total` | Optional | `number` | - | Total miners across the site (denominator of the `158 / 2,188` ratio) | ### `HeaderStatsBar` Horizontal flex strip that hosts the dashboard's stat boxes ([**`HeaderMinersBox`**](/reference/ui/components/dashboard/#headerminersbox), [**`HeaderHashrateBox`**](/reference/ui/components/dashboard/#headerhashratebox), [**`HeaderConsumptionBox`**](/reference/ui/components/dashboard/#headerconsumptionbox), [**`HeaderEfficiencyBox`**](/reference/ui/components/dashboard/#headerefficiencybox)). Lives inside [``](/reference/ui/components/navigation/#appheader) as the middle slot. Slot-based: the caller decides which boxes to render and in which order. The bar interleaves an angled chevron divider between adjacent children to match the reference app visual treatment `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/header-stats/header-stats-bar.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/header-stats/header-stats-bar.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `children` | Required | `React.ReactNode` | - | Stat boxes to render in order, left-to-right | | `className` | Optional | `string` | - | Optional class hook | ### `ProfileMenu` Top-bar profile dropdown. Wraps the core DropdownMenu primitive with the user-avatar icon as the trigger. Items are caller-provided so the menu surface stays application-driven `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/header-actions/profile-menu.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/header-actions/profile-menu.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `items` | Required | `ProfileMenuItem[]` | - | Items rendered in the dropdown, top-to-bottom | | `className` | Optional | `string` | - | Additional class names | | `icon` | Optional | `React.ReactNode` | `` | Override the trigger icon — defaults to the user-avatar icon | | `label` | Optional | `string` | `"Profile menu"` | Accessible label for the trigger button | | `user` | Optional | `React.ReactNode` | - | Optional user label rendered at the top of the dropdown (e.g. an email) | ### `SiteStatsBar` Site-level summary strip composed from [`WidgetTopRow`](/reference/ui/components/widgets/#widgettoprow) (title + power) and [`GenericDataBox`](/reference/ui/components/widgets/#genericdatabox) (hashrate / miner-count / container-count). Designed to sit at the top of a dashboard page above the chart cards `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/dashboard/site-stats-bar/site-stats-bar.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/dashboard/site-stats-bar/site-stats-bar.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `title` | Required | `string` | - | Site label rendered in the header row | | `className` | Optional | `string` | - | Optional class hook | | `containerCount` | Optional | `number` | - | Total container count across the site | | `hashrateUnit` | Optional | `string` | `"TH/s"` | [**`Hashrate`**](/reference/ui/components/charts/#hashrate) display unit — defaults to `TH/s` | | `isLoading` | Optional | `boolean` | `false` | Render a skeleton bar while data is loading | | `minerCount` | Optional | `number` | - | Total miner count across the site | | `power` | Optional | `number` | - | Current site-level power consumption, in watts (or whatever `powerUnit` says) | | `powerUnit` | Optional | `string` | `"kW"` | Display unit for `power` — defaults to `kW` | | `totalHashrate` | Optional | `number` | - | Aggregate hashrate, in TH/s | ### `TimelineSelector` Dropdown for picking the dashboard time range. Wraps `core/Select` and the canonical option list from `getTimelineOptions`. Pair with the `useDashboardTimeRange` hook in `@tetherto/mdk-react-adapter` `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/dashboard/timeline-selector/timeline-selector.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/dashboard/timeline-selector/timeline-selector.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `onChange` | Required | `(next: string) => void` | - | Called whenever the user picks a new option | | `value` | Required | `string` | - | Currently selected timeline value (e.g. `'1m'`, `'5m'`) | | `className` | Optional | `string` | - | Tailwind/BEM class hook on the trigger | | `label` | Optional | `string` | `"Time range"` | ARIA label / placeholder for the trigger | | `options` | Optional | `TimelineOption[]` | `getTimelineOptions()` | Available options — defaults to `getTimelineOptions`. Pass a custom list to localise labels or restrict the range | # Dashboard compositions (/reference/ui/components/dashboards) Pre-composed dashboard layouts and templates. ## 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. - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `ActionsSidebar` Full-height side panel for the reference app voting/approval workflow. Three sections (only rendered when non-empty): - **Draft** — locally-staged actions not yet sent to the server. - **In review** — actions this user submitted, awaiting votes. - **Requested** — other users' voting actions this user can approve/reject (only shown when the current token has `actions:w`). Open/close state is driven by `actionsStore.sidebarOpen` so the header [`PendingActionsButton`](/reference/ui/components/dashboards/#pendingactionsbutton) and any in-page code can open it without prop-drilling. Mount this once at the app root — it renders nothing when closed `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/pool-manager/actions-sidebar/actions-sidebar.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/pool-manager/actions-sidebar/actions-sidebar.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `className` | Optional | `string` | - | Extra class names merged onto the sidebar root element | ### `Cost` [**`Cost`**](/reference/ui/components/dashboards/#cost) Summary - composite reporting page (single-site). Reads cost-summary view model fields (from [`useCostSummary`](/reference/ui/hooks/utility/#usecostsummary)) and renders: - page header with "[**`Cost`**](/reference/ui/components/dashboards/#cost) Summary" title and an optional action slot (`setCostAction`) pinned to the opposite edge - period selector slot (`controls`) - pass [``](/reference/ui/components/filters/#timeframecontrols) for the OSS-style Year/Month picker or any other date selector element - shared [`CostContent`](/reference/ui/components/dashboards/#costcontent) 2x2 grid (charts + metric tiles) Multi-site is intentionally out of scope for this extraction wave `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/cost/cost.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/cost/cost.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `btcPriceLog` | Required | `readonly BtcPriceTimeSeriesEntry[]` | - | BTC price time series aligned to `costLog` buckets | | `controls` | Required | `React.ReactElement>` | - | Period selector element. Pass [``](/reference/ui/components/filters/#timeframecontrols) for the OSS-style year/month picker | | `costLog` | Required | `readonly CostTimeSeriesEntry[]` | - | Monthly/weekly production-cost time series for the Production [**`Cost`**](/reference/ui/components/dashboards/#cost) / Price chart | | `dateRange` | Required | `FinancialDateRange \| null` | - | Active date range; drives x-axis labels across all charts | | `metrics` | Required | `CostSummaryDisplayMetrics \| null` | - | Headline $/MWh tiles (all-in, energy, operations). Pass `null` while loading | | `totals` | Required | `CostSummaryMonetaryTotals \| null` | - | Period totals (energy + operations USD) for the Operations vs Energy doughnut | | `avgAllInCostData` | Optional | `readonly AvgAllInCostDataPoint[]` | - | Optional revenue/cost time-series for the Avg All-in [**`Cost`**](/reference/ui/components/dashboards/#cost) panel | | `error` | Optional | `unknown` | - | When truthy, renders an error message in place of the chart grid | | `isLoading` | Optional | `boolean` | `false` | Shows a loading spinner overlay over the chart grid | | `setCostAction` | Optional | `React.ReactElement>` | - | Optional "Set Monthly [**`Cost`**](/reference/ui/components/dashboards/#cost)" header action slot. A `ReactElement` slot (rather than an href string) so consumers can hand in router-aware components like `` or ` setIsOpen(false)} partTypes={PART_TYPES} defaultPartTypeId={activePartType} modelOptions={modelOptions} minerModelOptions={MINER_MODEL_OPTIONS} statusOptions={STATUS_OPTIONS} locationOptions={LOCATION_OPTIONS} isControllerPartTypeSelected={isController} onPartTypeChange={(id) => setActivePartType(id)} onSubmit={async () => { setIsOpen(false); }} /> ); }; ``` #### Related API - [**`SparePartSubTypesModal`**](/reference/ui/components/dialogs/#sparepartsubtypesmodal) @tetherto/mdk-react-devkit ### Manage spare part subtypes ```tsx ``` Modal for viewing and adding spare part subtypes (part models) per part type. Presents a part-type tab strip, a table of existing subtypes for the active type, and an inline add form. #### When to use Use this to manage the list of allowed part models for each part type. It can be opened standalone or embedded from `AddSparePartModal`'s "View Subtypes" button so users can add a missing model without losing their in-progress form. #### Notes - The active part type is controlled by the parent: change `activePartTypeId` and supply the matching `subTypes` in `onPartTypeChange` - The add form validates a non-empty name and clears on successful add #### Example [#spare-part-sub-types-modal-example] ```tsx const PART_TYPES = [ { value: "inventory-miner_part-controller", label: "Controller" }, { value: "inventory-miner_part-psu", label: "PSU" }, { value: "inventory-miner_part-hashboard", label: "Hashboard" }, ]; const INITIAL_SUB_TYPES: Record = { "inventory-miner_part-controller": ["CT-S19", "CT-S19j"], "inventory-miner_part-psu": ["PSU-3000W"], "inventory-miner_part-hashboard": ["HB-S19", "HB-S19j", "HB-S19XP"], }; const [isOpen, setIsOpen] = useState(false); const [activeId, setActiveId] = useState(PART_TYPES[0]?.value ?? ""); const [subTypesMap, setSubTypesMap] = useState(INITIAL_SUB_TYPES); const handleAddSubType = async (name: string): Promise<{ error: string } | void> => { const existing = subTypesMap[activeId] ?? []; if (existing.includes(name)) return { error: "Subtype already exists" }; setSubTypesMap((prev) => ({ ...prev, [activeId]: [...existing, name] })); }; return (
setIsOpen(false)} partTypes={PART_TYPES} activePartTypeId={activeId} onPartTypeChange={setActiveId} subTypes={subTypesMap[activeId] ?? []} onAddSubType={handleAddSubType} />
); }; ``` #### Related API - [**`AddSparePartModal`**](/reference/ui/components/dialogs/#addsparepartmodal) @tetherto/mdk-react-devkit ### Bulk-add spare parts ```tsx ``` Modal for bulk-adding spare parts from a CSV file. Provides a CSV template download, file selection with client-side parsing, and submits the parsed records. CSV parsing and validation helpers (`parseCsvText`, `validateCSVRecords`, `mapRawRowToRecord`, `downloadCsvTemplate`, `CSVRecord`) are exported alongside the component for use in the consuming submit handler. #### When to use Use this when operators need to register many spare parts at once. Validate the parsed records in your `onSubmit` with `validateCSVRecords` (location/status/model checks, duplicate detection) and return an `{ error }` to surface a message in the modal. #### CSV format Template headers (downloadable from the modal): `part, model, miner model, serial num, mac, status, location, comment`. Max `MAX_CSV_ITEMS` (50) rows per upload. `validateCSVRecords` enforces valid part types, miner models, statuses, locations, per-part-type model subtypes, and duplicate serial/MAC detection (the latter only for controllers). #### Example [#bulk-add-spare-parts-modal-example] ```tsx const [isOpen, setIsOpen] = useState(false); return (
setIsOpen(false)} onSubmit={async () => { setIsOpen(false); }} />
); }; ``` @tetherto/mdk-react-devkit ### Move a spare part ```tsx ``` Two-step modal for moving a single spare part. Step one shows the part details (`SparePartDetails`) alongside its current location and status, and lets the user pick a new location, status, and an observation. Step two previews the before → after transition with color-coded badges for confirmation before submitting. #### When to use Use this from a spare-parts inventory row action when an operator moves one part and you want an explicit confirm step showing exactly what changes. For moving many parts at once, use `BatchMoveSparePartsModal` instead. #### Data shape ```ts const sparePart: MoveSparePartModalSparePart = { id: 'sp-001', code: 'CB-AM-CB5_V10-01', // shown via SparePartDetails type: 'CB5_V10', site: 'Site A', serialNum: 'test-miner', macAddress: 'aa:bb:cc:dd:ee:ff', location: 'site.warehouse', // dot-separated location key status: 'ok_repaired', // spare part status key } ``` #### Label & color resolution - Location/status labels are resolved from the passed option lists (`getOptionLabel`) - The current/new badges are colored from `SPARE_PART_LOCATION_BG_COLORS` / `SPARE_PART_STATUS_BG_COLORS`; an unknown location key renders with no background - The footer shows "No Changes made" until the target location or status differs from the current values, at which point "Save Changes" advances to the confirmation step #### Example [#move-spare-part-modal-example] ```tsx Button, MoveSparePartModal, SPARE_PART_LOCATION_LABELS, SPARE_PART_LOCATIONS, SPARE_PART_STATUS_NAMES, SPARE_PART_STATUSES, } from "@tetherto/mdk-react-devkit"; const locationOptions = Object.values(SPARE_PART_LOCATIONS) .filter((value) => value !== SPARE_PART_LOCATIONS.SITE_CONTAINER) .map((value) => ({ value, label: SPARE_PART_LOCATION_LABELS[value] ?? value })); const statusOptions = Object.values(SPARE_PART_STATUSES).map((value) => ({ value, label: SPARE_PART_STATUS_NAMES[value as keyof typeof SPARE_PART_STATUS_NAMES] ?? value, })); const mockSparePart = { id: "sp-001", code: "HB-A001", type: "HB-S19", site: "Site A", serialNum: "SN123456", location: SPARE_PART_LOCATIONS.SITE_WAREHOUSE, status: SPARE_PART_STATUSES.OK_BRAND_NEW, }; const [isOpen, setIsOpen] = useState(false); return (
setIsOpen(false)} sparePart={mockSparePart} locationOptions={locationOptions} statusOptions={statusOptions} onSubmit={async () => { setIsOpen(false); }} />
); }; ``` #### Related API - [**`BatchMoveSparePartsModal`**](/reference/ui/components/dialogs/#batchmovesparepartsmodal) @tetherto/mdk-react-devkit ### Batch-move spare parts ```tsx ``` Modal for moving multiple spare parts at once. Shows the selected parts in a table (code, current location, current status) and lets the user choose a new location and/or status plus an observation, applied to every selected part in one submit. #### When to use Use this when an operator multi-selects spare-part rows and wants to relocate or re-status them together. At least one of location or status must be chosen. For a single part with a confirm step, use `MoveSparePartModal`. #### Data shape ```ts const spareParts: BatchMoveSparePart[] = [ { id: 'sp-001', code: 'HB-A001', location: 'site.warehouse', status: 'ok_brand_new' }, { id: 'sp-002', code: 'HB-A002', location: 'workshop.lab', status: 'faulty' }, ] ``` #### Notes - The form validates that **either** a location or a status is selected before submit - Table location/status cells are resolved to display labels from the passed option lists #### Example [#batch-move-spare-parts-modal-example] ```tsx BatchMoveSparePartsModal, Button, SPARE_PART_LOCATION_LABELS, SPARE_PART_LOCATIONS, SPARE_PART_STATUS_NAMES, SPARE_PART_STATUSES, } from "@tetherto/mdk-react-devkit"; const locationOptions = Object.values(SPARE_PART_LOCATIONS) .filter((value) => value !== SPARE_PART_LOCATIONS.SITE_CONTAINER) .map((value) => ({ value, label: SPARE_PART_LOCATION_LABELS[value] ?? value })); const statusOptions = Object.values(SPARE_PART_STATUSES).map((value) => ({ value, label: SPARE_PART_STATUS_NAMES[value as keyof typeof SPARE_PART_STATUS_NAMES] ?? value, })); const mockSpareParts = [ { id: "sp-001", code: "HB-A001", location: SPARE_PART_LOCATIONS.SITE_WAREHOUSE, status: SPARE_PART_STATUSES.OK_BRAND_NEW }, { id: "sp-002", code: "HB-A002", location: SPARE_PART_LOCATIONS.WORKSHOP_LAB, status: SPARE_PART_STATUSES.FAULTY }, { id: "sp-003", code: "CB-B001", location: SPARE_PART_LOCATIONS.SITE_WAREHOUSE, status: SPARE_PART_STATUSES.OK_RECOVERED }, ]; const [isOpen, setIsOpen] = useState(false); return (
setIsOpen(false)} spareParts={mockSpareParts} locationOptions={locationOptions} statusOptions={statusOptions} onSubmit={async () => { setIsOpen(false); }} />
); }; ``` #### Related API - [**`MoveSparePartModal`**](/reference/ui/components/dialogs/#movesparepartmodal) @tetherto/mdk-react-devkit ### View a device movement ```tsx ``` Modal that shows the details of a single historical device movement: a device summary (code, model, site, container, serial number, MAC) and the origin → destination transition of both location and status, with color-coded badges. #### When to use Use this in an inventory "Historical Movements" view when a user selects a movement row and you want to show the full before/after detail of where a device moved and how its status changed. #### Data shape ```ts const movement: MovementData = { origin: 'site.warehouse', // dot-separated location key destination: 'workshop.lab', previousStatus: 'ok_brand_new', // miner status key newStatus: 'faulty', device: { // raw device record (miner / spare part) code: 'M-123', tags: ['code-M-123'], type: 'antminer', info: { site: 'Site A', container: 'C1', serialNum: 'SN-9', macAddress: 'AA:BB' }, }, comments: 'Moved for repair', // optional ReactNode } ``` #### Label & color resolution The component renders data only — all resolution happens in `buildMovementDetailsViewModel`: - Location labels come from `getLocationLabel` (`'site.warehouse'` → `Site Warehouse`) - Location/status badge colors come from `MINER_LOCATION_*` / `MINER_STATUS_*` constants, falling back to a neutral border when a key is unknown - Status labels come from `MINER_STATUS_NAMES` (`'ok_brand_new'` → `Brand New`) - The device `code` is resolved with `getMinerShortCode`, and `model` falls back from `info.subType` → `type` → `-` #### Example [#movement-details-modal-example] ```tsx /** * Runnable example for MovementDetailsModal. */ const exampleMovement: MovementData = { origin: 'site.warehouse', destination: 'workshop.lab', previousStatus: 'ok_brand_new', newStatus: 'faulty', device: { code: 'M-1042', tags: ['code-M-1042'], type: 'antminer', info: { site: 'Site A', container: 'C-12', serialNum: 'SN-9981', macAddress: 'AA:BB:CC:DD:EE:FF', }, }, comments: 'Moved to workshop lab for diagnostics.', }
{ console.warn('modal closed') }} />
) ``` @tetherto/mdk-react-devkit ### Delete a spare part ```tsx ``` Confirmation modal for deleting a spare part. Warns that the action is irreversible and surfaces the part code so the user can verify before confirming. #### When to use Use this as the confirm step for a destructive "Delete" row action in a spare-parts inventory view. #### Example [#confirm-delete-spare-part-modal-example] ```tsx const [isOpen, setIsOpen] = useState(false); return (
setIsOpen(false)} onConfirm={async () => setIsOpen(false)} sparePart={{ id: "sp-001", code: "HB-A001" }} />
); }; ``` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `AddSparePartModal` Modal for registering a new spare part. Presents a part-type tab strip and a form for miner model, part model, serial number, MAC address (controllers only), status, location, tags, and a comment. Validation is controller-aware: controllers require a MAC address, other parts require a serial number. Receives option lists and the submit handler as props `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/inventory/spare-parts/add-spare-part-modal/add-spare-part-modal.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/inventory/spare-parts/add-spare-part-modal/add-spare-part-modal.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `isOpen` | Required | `boolean` | - | Whether the modal is open | | `locationOptions` | Required | `FormSelectOption[]` | - | Location options | | `minerModelOptions` | Required | `FormSelectOption[]` | - | Parent miner model options | | `modelOptions` | Required | `FormSelectOption[]` | - | Part model options for the active part type | | `onClose` | Required | `VoidFunction` | - | Called when the modal requests to close | | `onPartTypeChange` | Required | `(partTypeId: string) => void` | - | Called when the active part type changes (refetch model options here) | | `onSubmit` | Required | `(values: AddSparePartFormValues) => Promise` | - | Submit handler; return `fieldErrors` to surface server-side validation | | `partTypes` | Required | `SparePartSubTypesModalPartType[]` | - | Part-type tabs | | `statusOptions` | Required | `FormSelectOption[]` | - | Status options | | `defaultPartTypeId` | Optional | `string` | - | Initially selected part type | | `isControllerPartTypeSelected` | Optional | `boolean` | - | When true, the MAC address field is shown and required | | `isLoading` | Optional | `boolean` | - | Renders a loader instead of the form | | `isModelOptionsLoading` | Optional | `boolean` | - | Disables the model select while options load | | `isSubTypesLoading` | Optional | `boolean` | - | Loading state for the embedded subtypes modal | | `onAddSubType` | Optional | `((name: string) => Promise)` | - | Add handler for the embedded subtypes modal; return `{ error }` to surface a field error | | `onSubTypesPartTypeChange` | Optional | `((id: string) => void)` | - | Called when the active tab changes in the embedded subtypes modal | | `subTypes` | Optional | `string[]` | - | Subtype names for the active part type in the embedded modal | | `subTypesActivePartTypeId` | Optional | `string` | - | Active part type for the embedded subtypes modal | | `subTypesPartTypes` | Optional | `SparePartSubTypesModalPartType[]` | - | Part types for the embedded "View Subtypes" modal | ### `AlertDialogAction` Primary confirmation button inside an ``; clicking dismisses the dialog and runs the action handler `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/alert-dialog/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/alert-dialog/index.tsx)
### `AlertDialogCancel` Secondary dismiss button inside an ``; closes the dialog without invoking the destructive action `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/alert-dialog/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/alert-dialog/index.tsx)
### `AlertDialogContent` Modal content surface for an `` — renders the centered panel above the overlay with focus trap `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/alert-dialog/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/alert-dialog/index.tsx)
### `AlertDialogDescription` Supporting body text inside an ``; conveys the consequences of the action being confirmed `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/alert-dialog/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/alert-dialog/index.tsx)
### `AlertDialogFooter` Right-aligned action row inside an `` — hosts the cancel and confirm buttons `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/alert-dialog/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/alert-dialog/index.tsx)
### `AlertDialogHeader` Top section of an `` that groups the title and description above the action row `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/alert-dialog/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/alert-dialog/index.tsx)
### `AlertDialogOverlay` Full-viewport scrim rendered behind an `` to block interaction with the page `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/alert-dialog/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/alert-dialog/index.tsx)
### `AlertDialogTitle` Prominent title text inside an `` summarising the action that requires confirmation `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/alert-dialog/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/alert-dialog/index.tsx)
### `AssignPoolModal` Modal dialog for bulk-assigning a set of selected miners to a pool config. Displays the miner list, a pool selector with metadata (unit/miner counts, last-updated time), an endpoints preview, and an optional credential template preview. Submission is async; the modal stays open with a loading state until the parent resolves `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/pool-manager/assign-pool-modal/assign-pool-modal.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/pool-manager/assign-pool-modal/assign-pool-modal.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `isOpen` | Required | `boolean` | - | Controls modal visibility | | `miners` | Required | `Device[]` | - | Miners to display in the selection table | | `onClose` | Required | `() => void` | - | Called when the modal is dismissed (× button or backdrop) | | `onSubmit` | Required | `(values: { pool: PoolSummary; }) => Promise` | - | Called with the selected pool when the form is submitted | | `poolConfig` | Required | `PoolConfigEntry[]` | - | Available pool configurations to populate the pool selector | ### `BatchMoveSparePartsModal` Modal for moving multiple spare parts at once. Shows the selected parts in a table and lets the user choose a new location and/or status plus an observation, applied to every part. Receives the parts, option lists, and submit handler as props `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/inventory/spare-parts/batch-move-spare-parts-modal/batch-move-spare-parts-modal.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/inventory/spare-parts/batch-move-spare-parts-modal/batch-move-spare-parts-modal.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `isOpen` | Required | `boolean` | - | Whether the modal is open | | `locationOptions` | Required | `FormSelectOption[]` | - | New-location options | | `onClose` | Required | `VoidFunction` | - | Called when the modal requests to close | | `onSubmit` | Required | `(values: { location: string \| null; status: string \| null; observation: string \| null; }) => void \| Promise` | - | Submit handler; unselected fields are `null` | | `spareParts` | Required | `BatchMoveSparePart[]` | - | The parts to move, rendered in the table | | `statusOptions` | Required | `FormSelectOption[]` | - | New-status options | ### `BulkAddSparePartsModal` Modal for bulk-adding spare parts from a CSV file. Provides a CSV template download, file selection with client-side parsing, and submits the parsed records. Receives the submit handler as a prop; CSV parsing and validation helpers are exported alongside the component `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/inventory/spare-parts/bulk-add-spare-parts-modal/bulk-add-spare-parts-modal.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/inventory/spare-parts/bulk-add-spare-parts-modal/bulk-add-spare-parts-modal.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `isOpen` | Required | `boolean` | - | Whether the modal is open | | `onClose` | Required | `VoidFunction` | - | Called when the modal requests to close | | `onSubmit` | Required | `(records: CSVRecord[]) => Promise` | - | Submit handler; return `{ error }` to show an inline error | | `isLoading` | Optional | `boolean` | - | Renders a loader instead of the form | ### `ConfirmDeleteSparePartModal` Confirmation modal for deleting a spare part. Warns that the action is irreversible and surfaces the part code so the user can verify before confirming. Receives the part and confirm handler as props `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/inventory/spare-parts/confirm-delete-spare-part-modal/confirm-delete-spare-part-modal.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/inventory/spare-parts/confirm-delete-spare-part-modal/confirm-delete-spare-part-modal.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `isLoading` | Optional | `boolean` | - | Disables the action buttons while the delete is in flight | | `isOpen` | Optional | `boolean` | - | Whether the modal is open | | `onClose` | Optional | `VoidFunction` | - | Called when the modal requests to close | | `onConfirm` | Optional | `((sparePart: ConfirmDeleteSparePartModalSparePart) => void \| Promise)` | - | Called with the part when the user confirms | | `sparePart` | Optional | `ConfirmDeleteSparePartModalSparePart` | - | The part to delete; when omitted the modal renders nothing | ### `DialogContent` Centered modal surface for a `` — renders above the overlay with focus trap and Escape-to-close `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/dialog/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/dialog/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `bare` | Optional | `boolean` | `false` | Applies `mdk-dialog__header--bare` to the header | | `closable` | Optional | `boolean` | - | Shows an ✕ close button in the header | | `closeOnClickOutside` | Optional | `boolean` | `true` | Whether clicking the overlay closes the dialog | | `closeOnEscape` | Optional | `boolean` | `true` | Whether pressing Escape closes the dialog | | `description` | Optional | `string` | - | Renders [`DialogDescription`](/reference/ui/components/dialogs/#dialogdescription) below the title | | `onClose` | Optional | `VoidFunction` | - | Fired when the ✕ button is clicked | | `title` | Optional | `string` | - | Renders [`DialogTitle`](/reference/ui/components/dialogs/#dialogtitle) (and optional [`DialogDescription`](/reference/ui/components/dialogs/#dialogdescription)) inside the header | ### `DialogDescription` Supporting body copy inside a `` rendered below the title `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/dialog/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/dialog/index.tsx)
### `DialogFooter` Action row at the bottom of a `` — typically primary/secondary buttons `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/dialog/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/dialog/index.tsx)
### `DialogHeader` Top region of a `` that groups the title and description `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/dialog/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/dialog/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `bare` | Optional | `boolean` | `false` | Applies `mdk-dialog__header--bare` to the header | | `closable` | Optional | `boolean` | - | Shows an ✕ close button in the header | | `onClose` | Optional | `VoidFunction` | - | Fired when the ✕ button is clicked | ### `DialogOverlay` Full-viewport scrim rendered behind an open `` to block background interaction `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/dialog/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/dialog/index.tsx)
### `DialogTitle` Prominent title text at the top of a `` summarising the modal's purpose `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/dialog/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/dialog/index.tsx)
### `MovementDetailsModal` Modal showing the details of a historical device movement — the device summary plus the origin → destination transition of location and status. Receives the selected movement as a prop — no internal data fetching `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/inventory/movement-details-modal/movement-details-modal.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/inventory/movement-details-modal/movement-details-modal.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `isOpen` | Optional | `boolean` | `false` | Whether the modal is open | | `movement` | Optional | `MovementData` | - | The selected movement; when omitted the modal renders nothing | | `onClose` | Optional | `() => void` | - | Called when the modal requests to close (overlay click, escape, or close button) | ### `MoveSparePartModal` Two-step modal for moving a single spare part. Step one shows the part details with its current location and status and lets the user pick a new location, status, and observation; step two previews the before → after transition for confirmation. Receives the part, option lists, and submit handler as props `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/inventory/spare-parts/move-spare-part-modal/move-spare-part-modal.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/inventory/spare-parts/move-spare-part-modal/move-spare-part-modal.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `locationOptions` | Required | `FormSelectOption[]` | - | Location options | | `onSubmit` | Required | `(values: { location: string; status: string; observation: string; }, sparePart: MoveSparePartModalSparePart) => void \| Promise` | - | Submit handler with the new `{ location, status, observation }` and the original part | | `statusOptions` | Required | `FormSelectOption[]` | - | Status options | | `isOpen` | Optional | `boolean` | - | Whether the modal is open | | `onClose` | Optional | `VoidFunction` | - | Called when the modal requests to close | | `requestedValues` | Optional | `{ location?: string \| undefined; status?: string \| undefined; }` | - | Pre-seeds the target location/status | | `sparePart` | Optional | `MoveSparePartModalSparePart` | - | The part to move; when omitted the modal renders nothing | ### `SparePartSubTypesModal` Modal for viewing and adding spare part subtypes per part type. Presents a part-type tab strip, a table of existing subtypes for the active type, and an inline add form. Receives the active part type, subtype list, and add handler as props `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/inventory/spare-parts/spare-part-sub-types-modal/spare-part-sub-types-modal.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/inventory/spare-parts/spare-part-sub-types-modal/spare-part-sub-types-modal.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `activePartTypeId` | Required | `string` | - | The selected part type (controlled by the parent) | | `isOpen` | Required | `boolean` | - | Whether the modal is open | | `onAddSubType` | Required | `(name: string) => Promise` | - | Add handler; return `{ error }` to surface a field error | | `onClose` | Required | `VoidFunction` | - | Called when the modal requests to close | | `onPartTypeChange` | Required | `(id: string) => void` | - | Called when the active tab changes (fetch that type's subtypes here) | | `partTypes` | Required | `SparePartSubTypesModalPartType[]` | - | Part-type tabs | | `subTypes` | Required | `string[]` | - | Subtype names for the active part type | | `isLoading` | Optional | `boolean` | - | Renders a loader instead of the body | # Display (/reference/ui/components/display) Components for displaying and formatting data. ## 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. - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `Avatar` Circular avatar surface that shows a profile image with a graceful text fallback when the image fails to load `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/avatar/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/avatar/index.tsx)
### `AvatarFallback` Initials or icon placeholder shown inside [``](/reference/ui/components/display/#avatar) while the image loads or when no image is available `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/avatar/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/avatar/index.tsx)
### `AvatarImage` Profile image slot inside [``](/reference/ui/components/display/#avatar) — renders `src` and triggers the fallback on load failure `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/avatar/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/avatar/index.tsx)
### `Badge` [**`Badge`**](/reference/ui/components/display/#badge) component - Display badge with number or dot `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/badge/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/badge/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `children` | Optional | `React.ReactNode` | - | [**`Badge`**](/reference/ui/components/display/#badge) content (wraps children with badge) | | `className` | Optional | `string` | - | Custom className for badge | | `color` | Optional | `"success" \| "info" \| "warning" \| "error" \| "primary" \| "secondary" \| "default"` | `"primary"` | Color variant | | `count` | Optional | `number` | `0` | Number to display in badge If > overflowCount, will show "overflowCount+" | | `dot` | Optional | `boolean` | `false` | Show badge as a dot | | `offset` | Optional | `[number, number]` | `[0, 0]` | Offset position [x, y] in pixels | | `overflowCount` | Optional | `number` | `99` | Maximum count to display | | `showZero` | Optional | `boolean` | `false` | Whether to show badge when count is 0 | | `size` | Optional | `"sm" \| "md" \| "lg"` | `"md"` | [**`Badge`**](/reference/ui/components/display/#badge) size | | `square` | Optional | `boolean` | `false` | Square badge (no border-radius) | | `status` | Optional | `"success" \| "warning" \| "error" \| "default" \| "processing"` | - | Status badge (small dot badge with text) | | `text` | Optional | `string` | - | Custom badge content (overrides count) | | `title` | Optional | `string` | - | [**`Badge`**](/reference/ui/components/display/#badge) title for accessibility | | `wrapperClassName` | Optional | `string` | - | Custom className for wrapper | ### `BtcAveragePrice` Read-only BTC average price label for reporting toolbars `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/btc-average-price/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/btc-average-price/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `label` | Optional | `string` | `"BTC Average Price"` | [**`Label`**](/reference/ui/components/forms/#label) for the BTC average price | | `price` | Optional | `number \| null` | - | BTC price in USD; formatted with grouping and no decimal places. When `null`, `undefined`, non-finite, or negative, the value shows `-` (`FALLBACK` from format utils) | ### `DataLabel` Read-only period label (`PERIOD: start - end`) with timezone-aware dates `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/data-label/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/data-label/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `endDate` | Optional | `Date \| null` | - | Range end; formatted in the active timezone (`dd/MM/yy`) | | `label` | Optional | `string` | `"PERIOD"` | [**`Label`**](/reference/ui/components/forms/#label) text; defaults to `PERIOD` | | `startDate` | Optional | `Date \| null` | - | Range start; formatted in the active timezone (`dd/MM/yy`) | ### `Indicator` [**`Indicator`**](/reference/ui/components/display/#indicator) component - display status with colored background and label `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/indicator/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/indicator/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `children` | Optional | `React.ReactNode` | - | Children content (can include text, icons, multiple elements) | | `className` | Optional | `string` | - | Custom className for the root element | | `color` | Optional | `"red" \| "gray" \| "blue" \| "yellow" \| "green" \| "purple" \| "amber" \| "slate"` | `"gray"` | Color variant of the indicator | | `onClick` | Optional | `(VoidFunction & React.MouseEventHandler)` | - | Click handler | | `size` | Optional | `"sm" \| "md" \| "lg"` | `"md"` | Size variant of the indicator | | `vertical` | Optional | `boolean` | `false` | When true, adds extra spacing between child elements and stacks them vertically. Useful for displaying multiple pieces of information (e.g. status + count) in a clear way | ### `Tag` [**`Tag`**](/reference/ui/components/display/#tag) component - display labels, categories, or status `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/tag/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/tag/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `children` | Optional | `React.ReactNode` | - | Children content | | `className` | Optional | `string` | - | Custom className for the root element | | `color` | Optional | `"red" \| "blue" \| "green" \| "amber" \| "dark"` | `"dark"` | Color variant of the tag | ### `Typography` [**`Typography`**](/reference/ui/components/display/#typography) component for consistent text styling `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/typography/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/typography/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `align` | Optional | `"center" \| "left" \| "right" \| "justify"` | - | Text alignment | | `className` | Optional | `string` | - | Custom className | | `color` | Optional | `"success" \| "warning" \| "error" \| "primary" \| "default" \| "muted"` | `"default"` | Token-based text color | | `size` | Optional | `"sm" \| "md" \| "lg" \| "xs" \| "xl" \| "2xl" \| "3xl" \| "4xl"` | - | Text size; defaults to the variant's size when unset | | `truncate` | Optional | `boolean` | `false` | Truncate text with ellipsis | | `variant` | Optional | `"body" \| "caption" \| "secondary" \| "heading1" \| "heading2" \| "heading3"` | `"body"` | Determines the rendered HTML element and base style | | `weight` | Optional | `"medium" \| "normal" \| "light" \| "semibold" \| "bold"` | - | Font weight; defaults to the variant's weight when unset | # Feature (/reference/ui/components/features) Composite components for specific application features. ## 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. - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `ContainerDetail` Container detail page shell: a back link, the container name, and a per-model tab strip. Purely presentational — the page resolves the tab list (via the foundation tab matrix), owns the active tab / routing, and supplies the tab body as `children`. This is the frame every container detail tab mounts into `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/features/container-detail/container-detail.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/features/container-detail/container-detail.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `activeTab` | Required | `string` | - | Currently active tab key | | `onBack` | Required | `() => void` | - | Fired when the back link is clicked (the page decides where to go) | | `onTabChange` | Required | `(tab: string) => void` | - | Fired with the next tab key when the operator switches tabs | | `tabs` | Required | `ContainerDetailTab[]` | - | Ordered tabs for this container model (already resolved by the page) | | `backLabel` | Optional | `React.ReactNode` | `"Explorer"` | Back-link label. Defaults to "Explorer" | | `children` | Optional | `React.ReactNode` | - | The active tab's body — supplied by the page (real content or a placeholder) | | `className` | Optional | `string` | - | Additional class for the root element | | `name` | Optional | `React.ReactNode` | - | Container display name shown in the header. Optional — omit it when the host already renders the container name as the page title (e.g. the shell's `PageLayout`), so the name is not shown twice | ### `ContainerWidgets` Site Overview → Container Widgets: the read-only grid of per-container summary cards. Purely presentational — the shell page feeds it the shaped `containers` array (from the container-widgets data hook) and handles navigation via `onContainerClick` `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/features/site-overview/container-widgets/container-widgets.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/features/site-overview/container-widgets/container-widgets.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `containers` | Required | `ContainerWidgetItem[]` | - | Card-ready data for every container, shaped by the data hook | | `className` | Optional | `string` | - | Additional class for the root element | | `errorMessage` | Optional | `string` | - | Error message shown in place of the grid | | `isLoading` | Optional | `boolean` | `false` | Shows a spinner while the first load is in flight | | `onContainerClick` | Optional | `((id: string) => void)` | - | Invoked with the container id when a card is clicked | | `title` | Optional | `string` | - | Section heading | ### `ExplorerLayout` Explorer split-view shell: a header, a scrollable list column, and a sticky detail column that appears when a row is selected (stacking on narrow viewports). Purely presentational — the page supplies the list (tabs + table) and the detail panel, and owns selection/routing state `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/features/explorer/explorer-layout.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/features/explorer/explorer-layout.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `list` | Required | `React.ReactNode` | - | The list column — typically a tab switch plus the device/container table | | `className` | Optional | `string` | - | Additional class for the root element | | `detail` | Optional | `React.ReactNode` | - | The detail column content (shown in the sticky panel when `hasSelection`) | | `hasSelection` | Optional | `boolean` | `false` | When true the layout splits into list (70%) + a sticky detail column (30%); otherwise the list fills the width. Driven by whether a row is selected | | `headerActions` | Optional | `React.ReactNode` | - | Optional header controls (export button, etc.) shown next to the title | | `title` | Optional | `string` | - | Page heading; nothing renders when omitted | # Feedback (/reference/ui/components/feedback) Components for providing feedback and notifications to users. ## 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. - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit ### Core alert ```tsx ``` Contextual feedback banner for success, info, warning, and error messages. Supports icons, close button, an action slot, and an optional full-width banner mode. #### Notes - Once closed via the ✕ button, the component returns `null` and cannot be reopened without unmounting/remounting - Setting `description` automatically adds the `mdk-alert--with-description` modifier for extra spacing #### Example [#core-alert-example] ```tsx /** * Runnable example for Alert. */
) ``` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `AlarmContents` Body region of an alarm card listing the alert details and recommended next actions `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/alarm/alarm-contents/alarm-contents.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/alarm/alarm-contents/alarm-contents.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `alarmsData` | Required | `unknown` | - | Alert entries to render. Falls back to [`EmptyState`](/reference/ui/components/feedback/#emptystate) when empty or falsy | | `onNavigate` | Required | `(path: string) => void` | - | Navigation callback forwarded to each [`AlarmRow`](/reference/ui/components/feedback/#alarmrow) for click-through routing | ### `AlarmRow` Single alarm-feed row with severity dot, timestamp, source device, and the alert message `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/alarm/alarm-row/alarm-row.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/alarm/alarm-row/alarm-row.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Required | `TimelineItemData` | - | The alarm entry to render | | `onNavigate` | Required | `(path: string) => void` | - | Called when the row is clicked; receives the alarm `uuid` as the path segment | ### `CoreAlert` Inline alert banner with type-based icon, optional description, dismissible close button, and trailing action slot `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/alert/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/alert/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `action` | Optional | `React.ReactNode` | - | Action element rendered to the right | | `banner` | Optional | `boolean` | `false` | Display as full-width banner (no border radius, no margin) | | `className` | Optional | `string` | - | Custom className | | `closable` | Optional | `boolean` | `false` | Shows an ✕ button; clicking it hides the alert | | `description` | Optional | `React.ReactNode` | - | Secondary/detail content shown below the title | | `icon` | Optional | `React.ReactNode` | - | Custom icon (used when showIcon is true) | | `onClose` | Optional | `React.MouseEventHandler` | - | Called when close button is clicked | | `showIcon` | Optional | `boolean` | `false` | Renders the type icon (or the `icon` override) before the content | | `style` | Optional | `React.CSSProperties` | - | Custom styles | | `title` | Optional | `React.ReactNode` | - | Main message | | `type` | Optional | `"success" \| "info" \| "warning" \| "error"` | `"info"` | Controls the color scheme and default icon | ### `EmptyState` Empty state component for displaying placeholder content when no data is available `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/empty-state/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/empty-state/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `description` | Required | `React.ReactNode` | - | Description text or ReactNode displayed below the image | | `className` | Optional | `string` | - | Additional CSS class name | | `image` | Optional | `EmptyStateImage` | `"default"` | Image to display. Use "default" for the standard illustration, "simple" for a minimal icon, or pass a custom ReactNode | | `size` | Optional | `"sm" \| "md" \| "lg"` | `"md"` | Size variant controlling spacing and icon dimensions | ### `ErrorCard` Error card component for displaying error messages `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/error-card/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/error-card/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `error` | Required | `string` | - | Error message string. Supports `\\n` for line breaks | | `className` | Optional | `string` | - | Additional CSS class name | | `title` | Optional | `string` | `"Errors"` | Title displayed above the error message | | `variant` | Optional | `"inline" \| "card"` | `"card"` | Display variant. "card" shows a bordered container, "inline" shows flat text | ### `Loader` [**`Loader`**](/reference/ui/components/feedback/#loader) component - display pulsing dots animation `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/loader/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/loader/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `className` | Optional | `string` | - | Custom className for the root element | | `color` | Optional | `"red" \| "gray" \| "blue" \| "amber" \| "orange"` | `"orange"` | Color variant of the loader | | `count` | Optional | `3 \| 5 \| 7` | `5` | Number of dots to display | | `inline` | Optional | `boolean` | `false` | Render as an inline activity indicator instead of a block loading state. The default reserves a fixed 200px so a panel standing in for absent content does not collapse and then jump. Inline, that height is wrong: it strands the dots ~100px below the thing they belong to and centers them against surrounding text, which reads as frozen | | `size` | Optional | `number` | `10` | Size of each dot in pixels | ### `SkeletonBlock` Rectangular shimmer placeholder used to hint at content shape while data is loading `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/skeleton/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/skeleton/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `borderRadius` | Optional | `string \| number` | - | Border radius; ignored when `circle` is true | | `circle` | Optional | `boolean` | `false` | Renders a perfect circle using `height` as the diameter | | `className` | Optional | `string` | - | Additional class for the element | | `height` | Optional | `string \| number` | - | Height in pixels (number) or any CSS value (string) | | `width` | Optional | `string \| number` | - | Width in pixels (number) or any CSS value (string) | ### `Spinner` [**`Spinner`**](/reference/ui/components/feedback/#spinner) component - display loading state with rotating squares `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/spinner/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/spinner/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `className` | Optional | `string` | - | Custom className for the root element | | `color` | Optional | `"primary" \| "secondary"` | `"primary"` | Color variant of the spinner | | `fullScreen` | Optional | `boolean` | `false` | Whether to display in fullscreen mode | | `label` | Optional | `string` | - | Optional label text to display below the spinner | | `size` | Optional | `"sm" \| "md" \| "lg"` | `"md"` | Size variant of the spinner | | `speed` | Optional | `"slow" \| "normal" \| "fast"` | `"normal"` | Speed of the animation | | `type` | Optional | `"circle" \| "square"` | `"square"` | Type of spinner animation | ### `Toast` Single transient notification rendered inside the [``](/reference/ui/components/feedback/#toaster) viewport. Use `variant` to convey intent (`success`, `info`, `warning`, `error`); pair with `` and `` for structured content, or wrap a `` for a single inline call-to-action. Most apps will trigger toasts imperatively via the `useToast` hook rather than mounting [``](/reference/ui/components/feedback/#toast) directly `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/toast/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/toast/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `title` | Required | `string` | - | Title shown at the top of the toast | | `description` | Optional | `string` | - | Body text | | `duration` | Optional | `number` | - | Auto-dismiss after N ms | | `icon` | Optional | `React.JSX.Element` | - | Override the default variant icon | | `onOpenChange` | Optional | `((open: boolean) => void)` | - | Open-state change handler | | `open` | Optional | `boolean` | - | Controlled open state | | `variant` | Optional | `"success" \| "info" \| "warning" \| "error"` | `"info"` | Determines the icon and accent | ### `Toaster` Top-level provider + viewport that hosts every toast triggered via `useToast`. Mount once near the root of your app (typically inside ``). Without a [``](/reference/ui/components/feedback/#toaster) in the tree, `useToast` calls are no-ops. Position and visual stacking are controlled by CSS tokens; no props are required for the default top-left placement `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/toast/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/toast/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `children` | Required | `React.ReactNode` | - | The [``](/reference/ui/components/feedback/#toast) elements to render | | `position` | Optional | `"top-left" \| "top-right" \| "bottom-left" \| "bottom-right" \| "top-center" \| "bottom-center"` | `"top-left"` | Where the viewport is anchored | # Filter (/reference/ui/components/filters) Components for filtering and searching data. ## 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. - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `ListViewFilter` Toolbar of dropdown/checkbox filters that drive a list view; emits the active filter set to the parent `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/list-view-filter/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/list-view-filter/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `onChange` | Required | `(selections: CascaderValue[]) => void` | - | Callback when filters change | | `options` | Required | `CascaderOption[]` | - | [**`Cascader`**](/reference/ui/components/forms/#cascader) options for filtering | | `className` | Optional | `string` | - | Custom className for the filter button | | `filterKey` | Optional | `string` | `"default"` | Optional key to force re-mounting the [**`Cascader`**](/reference/ui/components/forms/#cascader) when filters change Useful if you want to reset the internal state of the [**`Cascader`**](/reference/ui/components/forms/#cascader) when filters change | | `localFilters` | Optional | `LocalFilters` | - | Current filter values as key-value pairs Example: { type: 'Antminer S19XP H', status: ['active', 'pending'] } | ### `ReportTimeFrameSelector` Reporting-period selector with preset windows (1d / 7d / 30d / custom range) `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/report-time-frame-selector/report-time-frame-selector.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/report-time-frame-selector/report-time-frame-selector.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `dateRange` | Required | `[Date, Date]` | - | Custom date range as a `[start, end]` tuple | | `presetTimeFrame` | Required | `number \| null` | - | Selected preset in days (`1`, `7`, `30`); `null` means the custom range is active | | `setDateRange` | Required | `(value: [Date, Date]) => void` | - | Update the custom date range | | `setPresetTimeFrame` | Required | `(value: number \| null) => void` | - | Update the preset selection | ### `TimeframeControls` Date-range picker for financial reporting pages — supports year, month, and week granularity via connected selects `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/timeframe-controls/timeframe-controls.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/timeframe-controls/timeframe-controls.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `dateRange` | Optional | `TimeframeControlsDateRange` | - | Current date range | | `hint` | Optional | `string` | - | Helper text below the controls | | `isMonthSelectVisible` | Optional | `boolean` | `true` | Show month selector | | `isWeekSelectVisible` | Optional | `boolean` | `true` | Show week selector | | `layout` | Optional | `"horizontal" \| "stacked"` | `"horizontal"` | Layout direction | | `onRangeChange` | Optional | `TimeframeControlsOnRangeChange` | - | Called when the range changes | | `onReset` | Optional | `VoidFunction` | - | Called when the Reset button is clicked | | `onTimeframeTypeChange` | Optional | `(type: TimeframeTypeValue) => void` | - | Called when timeframe type changes | | `showResetButton` | Optional | `boolean` | `false` | Shows the Reset button | | `timeframeType` | Optional | `null \| "month" \| "week" \| "year"` | - | Active timeframe type | ### `TimeframeWeekFlatContent` Flat list of selectable week items for the [**`TimeframeControls`**](/reference/ui/components/filters/#timeframecontrols) week selector `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/timeframe-controls/timeframe-controls-week-content.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/timeframe-controls/timeframe-controls-week-content.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `visibleWeeks` | Required | `Week[]` | - | - | ### `TimeframeWeekTreeContent` Hierarchical year → month → week tree for the [**`TimeframeControls`**](/reference/ui/components/filters/#timeframecontrols) week selector `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/timeframe-controls/timeframe-controls-week-content.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/timeframe-controls/timeframe-controls-week-content.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `selectedMonth` | Required | `number` | - | - | | `selectedYear` | Required | `number` | - | - | | `timezone` | Required | `string` | - | - | # Form (/reference/ui/components/forms) Form components for building data entry interfaces. ## 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. - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit ### Core form workflow ```tsx ``` Form primitives built on [`react-hook-form`](https://react-hook-form.com/). Use with a `useForm()` instance. #### Pieces - `Form` — wraps a `
` and provides `FormProvider` context. - `FormField` — wraps [`react-hook-form`](https://react-hook-form.com/)'s `Controller` and provides field context. - `FormItem` — layout wrapper that generates IDs for accessibility linking. - `FormLabel`, `FormControl`, `FormDescription`, `FormMessage` — slots. #### Notes - Pre-built field helpers (`FormInput`, `FormSelect`, `FormCheckbox`, `FormDatePicker`, `FormCascader`, etc.) live in [`form-fields.tsx`](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/form-fields.tsx). - See the directory's [`README.md`](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/README.md) and [`QUICK_REFERENCE.md`](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/QUICK_REFERENCE.md) for the full pattern catalog. #### Example [#core-workflow-example] ```tsx /** * Runnable example for Form (react-hook-form + zod). */ Button, Form, FormControl, FormField, FormItem, FormLabel, FormMessage, Input, } from '@tetherto/mdk-react-devkit' const schema = z.object({ name: z.string().min(2, 'At least 2 characters'), email: z.string().email('Must be a valid email'), }) type FormValues = z.infer const form = useForm({ resolver: zodResolver(schema), defaultValues: { name: '', email: '' }, }) const onSubmit = (values: FormValues) => { // eslint-disable-next-line no-console console.log('submit', values) } return ( ( Name )} /> ( Email )} /> ) } ``` #### Related API - [**`useFormField`**](/reference/ui/hooks/forms/#useformfield) @tetherto/mdk-react-devkit ### Pre-built input field ```tsx ``` #### Related API - [**`FormField`**](/reference/ui/components/forms/#formfield) - [**`useFormField`**](/reference/ui/hooks/forms/#useformfield) @tetherto/mdk-react-devkit ### Date range picker ```tsx ``` Single-date and range-date pickers built on `react-day-picker`. The range picker includes presets and a modal-style popover with Clear / Apply actions. #### Data contracts ```ts type DateRange = { from: Date | undefined; to?: Date | undefined }; type PresetItem = { label: string; value: DateRange }; ``` #### Example [#date-range-picker-example] ```tsx /** * Runnable example for DatePicker and DateRangePicker. */ const [date, setDate] = useState() const [range, setRange] = useState() return (
) } ``` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `Cascader` Two-panel hierarchical selector for picking a leaf value from a nested tree (categories → subcategories → leaf). Features: - Two-column layout: categories on left, options on right - Single or multiple selection modes - Search/filter functionality via [**`TagInput`**](/reference/ui/components/forms/#taginput) - Category-level selection (select/deselect all children) - Indeterminate state for partial selections - [**`Tag`**](/reference/ui/components/display/#tag) display for multiple selections - Keyboard navigation support - Disabled state support `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/cascader/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/cascader/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `options` | Required | `CascaderOption[]` | - | Hierarchical options to display in the cascader Parent options with children appear in the left panel Child options appear in the right panel when parent is selected | | `className` | Optional | `string` | - | Custom className for the root cascader element | | `disabled` | Optional | `boolean` | `false` | Disable the entire cascader (input and all options) | | `dropdownClassName` | Optional | `string` | - | Custom className for the dropdown panels container | | `multiple` | Optional | `boolean` | `false` | Enable multiple selection mode - true: Shows checkboxes, allows multiple selections, displays selected items as tags - false: Shows radio buttons, allows single selection | | `onChange` | Optional | `((value: CascaderValue \| CascaderValue[] \| null) => void)` | - | Callback when selection changes - For single select: receives CascaderValue or null - For multiple select: receives CascaderValue[] or null | | `placeholder` | Optional | `string` | `"Select..."` | Placeholder text shown in the input when no selections are made | | `value` | Optional | `CascaderValue \| CascaderValue[]` | - | Current selected value(s) - For single select: CascaderValue (e.g., ['category', 'option']) - For multiple select: CascaderValue[] (e.g., [['cat1', 'opt1'], ['cat2', 'opt2']]) | ### `Checkbox` [**`Checkbox`**](/reference/ui/components/forms/#checkbox) component with full customization `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/checkbox/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/checkbox/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `checked` | Optional | `CheckedState` | - | Controlled checked state | | `className` | Optional | `string` | - | Custom className for the root element | | `color` | Optional | `"success" \| "warning" \| "error" \| "primary" \| "default"` | `"primary"` | Color variant when checked | | `defaultChecked` | Optional | `CheckedState` | - | Uncontrolled initial checked state | | `disabled` | Optional | `boolean` | `false` | Disable the input | | `indicatorClassName` | Optional | `string` | - | Custom className for the indicator element | | `onCheckedChange` | Optional | `(((checked: CheckedState) => void) & ((checked: CheckedState) => void))` | - | Callback when the checked state changes | | `radius` | Optional | `"small" \| "none" \| "medium" \| "large" \| "full"` | `"none"` | Border radius variant | | `size` | Optional | `"sm" \| "md" \| "lg" \| "xs"` | `"md"` | Size variant of the checkbox | ### `CurrencyToggler` [**`CurrencyToggler`**](/reference/ui/components/forms/#currencytoggler) component for switching between currencies `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/currency-toggler/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/currency-toggler/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `currencies` | Required | `(string \| CurrencyItem)[]` | - | List of currency options | | `onChange` | Required | `(currency: string) => void` | - | Fired with the selected currency value when a button is clicked | | `value` | Required | `string` | - | Currently selected currency value | | `className` | Optional | `string` | - | Additional class for the root element | ### `DatePicker` Single-date selection component built on react-day-picker with the MDK dark theme. Controlled via `selected` / `onSelect` `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/date-picker/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/date-picker/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `calendarClassName` | Optional | `string` | - | Custom className for the calendar | | `dateFormat` | Optional | `string` | `"MM/dd/yyyy"` | Date format for display | | `disabled` | Optional | `(boolean & (Matcher \| Matcher[]))` | `false` | Whether the picker is disabled | | `onSelect` | Optional | `((date: Date \| undefined) => void)` | - | Callback when date changes | | `placeholder` | Optional | `string` | `"Pick a date"` | Placeholder text when no date is selected | | `selected` | Optional | `Date` | - | Currently selected date | | `triggerClassName` | Optional | `string` | - | Custom className for the trigger button | ### `DateRangePicker` Date-range selection component with preset shortcuts (last 7/14/30/90 days) and a modal interface. Controlled via `selected` / `onSelect` `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/date-picker/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/date-picker/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `allowFutureDates` | Optional | `boolean` | `false` | Whether to allow future dates | | `calendarClassName` | Optional | `string` | - | Custom className for the calendar | | `dateFormat` | Optional | `string` | `"MM/dd/yyyy"` | Date format for display | | `disabled` | Optional | `(boolean & (Matcher \| Matcher[]))` | `false` | Whether the picker is disabled | | `modalClassName` | Optional | `string` | - | Custom className for the modal | | `onSelect` | Optional | `((range: DateRange \| undefined) => void)` | - | Callback when date range changes | | `placeholder` | Optional | `string` | `"Pick a date range"` | Placeholder text when no range is selected | | `presets` | Optional | `PresetItem[]` | - | Custom preset items | | `selected` | Optional | `DateRange` | - | Selected date range | | `showPresets` | Optional | `boolean` | `true` | Whether to show preset buttons | | `triggerClassName` | Optional | `string` | - | Custom className for the trigger button | ### `Form` React Hook [**`Form`**](/reference/ui/components/forms/#form) provider wrapper. Pass the result of `useForm()` as `form` and render fields via [`FormField`](/reference/ui/components/forms/#formfield) / [`FormItem`](/reference/ui/components/forms/#formitem) / [`FormLabel`](/reference/ui/components/forms/#formlabel) / [`FormControl`](/reference/ui/components/forms/#formcontrol) / [`FormMessage`](/reference/ui/components/forms/#formmessage) `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `children` | Required | `React.ReactNode` | - | - | | `form` | Required | `UseFormReturn` | - | - | ### `FormCascader` Pre-built [**`Cascader`**](/reference/ui/components/forms/#cascader) field component with integrated form state. Perfect for hierarchical selections like categories and subcategories `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/form-fields.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/form-fields.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `options` | Required | `CascaderOption[]` | - | - | | `cascaderProps` | Optional | `Omit, "onChange" \| "value" \| "options" \| "placeholder">` | - | - | | `description` | Optional | `string` | - | - | | `label` | Optional | `string` | - | - | | `multiple` | Optional | `boolean` | - | - | | `placeholder` | Optional | `string` | - | - | ### `FormCheckbox` Pre-built [**`Checkbox`**](/reference/ui/components/forms/#checkbox) field component with integrated form state `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/form-fields.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/form-fields.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `checkboxProps` | Optional | `({ checked?: CheckedState \| undefined; defaultChecked?: CheckedState \| undefined; disabled?: boolean \| undefined; size?: CheckboxSize \| undefined; color?: ComponentColor \| undefined; radius?: BorderRadius \| undefined; className?: string \| u… /* see source */` | - | - | | `description` | Optional | `string` | - | - | | `label` | Optional | `string` | - | - | | `layout` | Optional | `"row" \| "column"` | - | - | | `placeholder` | Optional | `string` | - | - | ### `FormControl` Slot-based wrapper that injects ARIA attributes onto its child input element without adding an extra DOM wrapper `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/index.tsx)
### `FormDatePicker` Pre-built [**`DatePicker`**](/reference/ui/components/forms/#datepicker) field component with integrated form state `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/form-fields.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/form-fields.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `datePickerProps` | Optional | `object` | - | - | | `description` | Optional | `string` | - | - | | `label` | Optional | `string` | - | - | | `placeholder` | Optional | `string` | - | - | ### `FormDescription` Optional helper text displayed below the input `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/index.tsx)
### `FormField` Wraps react-hook-form's Controller and provides field context to descendants `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/index.tsx)
### `FormInput` Pre-built [**`Input`**](/reference/ui/components/forms/#input) field component with integrated form state. Reduces boilerplate by combining [**`FormField`**](/reference/ui/components/forms/#formfield), [**`FormItem`**](/reference/ui/components/forms/#formitem), [**`FormLabel`**](/reference/ui/components/forms/#formlabel), [**`FormControl`**](/reference/ui/components/forms/#formcontrol), and [**`FormMessage`**](/reference/ui/components/forms/#formmessage) `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/form-fields.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/form-fields.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `description` | Optional | `string` | - | - | | `inputProps` | Optional | `Omit & React.RefAttributes, "type" \| "variant">` | - | - | | `label` | Optional | `string` | - | - | | `placeholder` | Optional | `string` | - | - | | `type` | Optional | `React.HTMLInputTypeAttribute` | - | - | | `variant` | Optional | `"search" \| "default"` | - | - | ### `FormItem` Layout wrapper for a form field. Generates a unique ID for accessibility linking `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/index.tsx)
### `FormLabel` [**`Label`**](/reference/ui/components/forms/#label) that auto-links to the form field input via generated IDs. Applies error styling when the field has a validation error `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `htmlFor` | Optional | `string` | - | Id of the input being labelled | ### `FormMessage` Displays the validation error message from react-hook-form field state. Falls back to children if no error is present. Always renders to prevent layout shift when errors appear `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/index.tsx)
### `FormRadioGroup` Pre-built [**`RadioGroup`**](/reference/ui/components/forms/#radiogroup) field component with integrated form state `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/form-fields.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/form-fields.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `options` | Required | `FormRadioOption[]` | - | - | | `description` | Optional | `string` | - | - | | `label` | Optional | `string` | - | - | | `orientation` | Optional | `"horizontal" \| "vertical"` | - | - | | `placeholder` | Optional | `string` | - | - | | `radioGroupProps` | Optional | `object` | - | - | ### `FormSelect` Pre-built [**`Select`**](/reference/ui/components/forms/#select) field component with integrated form state `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/form-fields.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/form-fields.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `options` | Required | `FormSelectOption[]` | - | - | | `description` | Optional | `string` | - | - | | `label` | Optional | `string` | - | - | | `placeholder` | Optional | `string` | - | - | | `selectProps` | Optional | `object` | - | - | ### `FormSwitch` Pre-built [**`Switch`**](/reference/ui/components/forms/#switch) field component with integrated form state `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/form-fields.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/form-fields.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `description` | Optional | `string` | - | - | | `label` | Optional | `string` | - | - | | `layout` | Optional | `"row" \| "column"` | - | - | | `placeholder` | Optional | `string` | - | - | | `switchProps` | Optional | `object` | - | - | ### `FormTagInput` Pre-built [**`TagInput`**](/reference/ui/components/forms/#taginput) field component with integrated form state. Perfect for multi-select with search and tag display `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/form-fields.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/form-fields.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `allowCustomTags` | Optional | `boolean` | - | - | | `description` | Optional | `string` | - | - | | `label` | Optional | `string` | - | - | | `options` | Optional | `TagInputOption[]` | - | - | | `placeholder` | Optional | `string` | - | - | | `tagInputProps` | Optional | `object` | - | - | | `variant` | Optional | `"search" \| "default"` | - | - | ### `FormTextArea` Pre-built [**`TextArea`**](/reference/ui/components/forms/#textarea) field component with integrated form state `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/form-fields.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/form/form-fields.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `description` | Optional | `string` | - | - | | `label` | Optional | `string` | - | - | | `placeholder` | Optional | `string` | - | - | | `textAreaProps` | Optional | `(Omit & React.RefAttributes)` | - | - | ### `Input` Text input with optional label, prefix/suffix slots, and a `search` variant. Forwards refs and all native `` attributes `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/input/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/input/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `error` | Optional | `string` | - | Validation error message. When provided, displays error styling (red border) and the message below the input | | `id` | Optional | `string` | `auto-generated` | HTML id for the input. Required when using label for accessibility | | `label` | Optional | `string` | - | Optional label displayed above the input | | `prefix` | Optional | `React.ReactNode` | - | Prefix element displayed before the input (left side) | | `size` | Optional | `"default" \| "medium"` | `"default"` | Size of the input - `default`: padding 10px 12px, icon 16px - `medium`: padding 6px 12px, icon 12px | | `suffix` | Optional | `React.ReactNode` | - | Suffix element displayed after the input (right side) | | `variant` | Optional | `"search" \| "default"` | `"default"` | Variant of the input - `default`: Standard text input - `search`: [**`Input`**](/reference/ui/components/forms/#input) with magnifying glass icon on the right | | `wrapperClassName` | Optional | `string` | - | Custom className for the root wrapper | ### `Label` Accessible text label for form controls. Associates with an input via `htmlFor` and supports a required-mark indicator. Built on Radix [**`Label`**](/reference/ui/components/forms/#label) `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/label/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/label/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `htmlFor` | Optional | `string` | - | Id of the input being labelled | ### `MultiLevelSelect` Multi-level select component with collapsible sections `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/multi-level-select/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/multi-level-select/index.tsx)
### `MultiSelect` Multi-select picker built on Radix [**`Popover`**](/reference/ui/components/overlays/#popover) + [**`Checkbox`**](/reference/ui/components/forms/#checkbox). Sibling to [``](/reference/ui/components/forms/#select) sizes | | `value` | Optional | `string[]` | - | Controlled selected values. Omit to use `defaultValue` for uncontrolled mode | | `variant` | Optional | `"default" \| "colored"` | `"default"` | `'colored'` paints the trigger in the primary tint (matches [`` | | `disabled` | Optional | `boolean` | `false` | Disabled state | | `dropdownMaxHeight` | Optional | `string` | `"12rem"` | Maximum height of the dropdown (CSS value, e.g. '300px', '20rem') | | `dropdownMinHeight` | Optional | `string` | - | Minimum height of the dropdown (CSS value, e.g. '100px', '6rem') | | `filterOptions` | Optional | `((options: TagInputOption[], query: string) => TagInputOption[])` | `case-insensitive includes` | Filter options by input value. Receives options and query, returns filtered options. When undefined, filters by case-insensitive includes | | `id` | Optional | `string` | `auto-generated` | HTML id for the input | | `label` | Optional | `string` | - | [**`Label`**](/reference/ui/components/forms/#label) for the input | | `onInputChange` | Optional | `((value: string) => void)` | - | Callback when input value changes (typing). Receives current input value. Useful for async option loading or custom filtering | | `onSubmit` | Optional | `((tags: string[]) => void)` | - | Callback when user presses Enter (submit). Receives current tags. Called after adding a tag from selection or typed text, if applicable | | `onTagsChange` | Optional | `((tags: string[]) => void)` | - | Callback when tags change (add/remove) | | `options` | Optional | `TagInputOption[]` | `[]` | Options to show in the dropdown when input is focused | | `placeholder` | Optional | `string` | `"Search..."` | Placeholder when input is empty | | `renderDropdown` | Optional | `((props: TagInputDropdownProps) => React.ReactNode)` | - | Render custom dropdown content. When provided, replaces the default dropdown. Use this to apply your own styling or structure | | `size` | Optional | `"sm" \| "md" \| "lg"` | `"lg"` | Size of the tag input — matches [**`Select`**](/reference/ui/components/forms/#select) sizes - `sm`: 24px height - `md`: 32px height - `lg`: 40px height | | `value` | Optional | `string[]` | `[]` | Controlled tags (array of tag values) | | `variant` | Optional | `"search" \| "default"` | `"search"` | [**`Input`**](/reference/ui/components/forms/#input) variant; `'search'` shows a magnifying-glass icon that doubles as a clear-all button | | `wrapperClassName` | Optional | `string` | - | Custom className for the wrapper | ### `TextArea` [**`TextArea`**](/reference/ui/components/forms/#textarea) component with label support and error handling `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/textarea/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/textarea/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `error` | Optional | `string` | - | Validation error message. When provided, displays error styling (red border) and the message below the textarea | | `id` | Optional | `string` | `auto-generated` | HTML id for the textarea. Required when using label for accessibility | | `label` | Optional | `string` | - | Optional label displayed above the textarea | | `wrapperClassName` | Optional | `string` | - | Custom className for the root wrapper | # Layout (/reference/ui/components/layout) Components for page structure and layout. ## 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. - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit ### Accordion ```tsx ``` Collapsible panel with a title trigger and animated content area. Exports both a high-level `Accordion` wrapper and composable primitives (`AccordionRoot`, `AccordionItem`, `AccordionTrigger`, `AccordionContent`). #### Notes - `Accordion` wraps the primitives with `type="multiple"` and a single hard-coded item value; use `AccordionRoot` directly when you need multi-item control - `toggleIconPosition="right"` swaps to `PlusIcon`/`MinusIcon` style; `"left"` uses `ChevronDown`/`ChevronRight` #### Example [#accordion-example] ```tsx /** * Runnable example for Accordion. */ Accordion, AccordionContent, AccordionItem, AccordionRoot, AccordionTrigger, } from '@tetherto/mdk-react-devkit'

Collapsed/expanded content goes here. Supports any React node as children.

This variant uses a solid background for the accordion panel.

Custom trigger position

Content for item 1 using low-level Accordion primitives.

Another section

Content for item 2.

) ``` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `Accordion` Vertically stacked, collapsible content panels with optional toggle icon. Renders a styled wrapper around Radix [**`Accordion`**](/reference/ui/components/layout/#accordion): pass `title` for the default header or compose [``](/reference/ui/components/layout/#accordionitem)s as children for full control `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/accordion/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/accordion/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `customLabel` | Optional | `React.ReactNode` | - | Extra content (e.g. a badge) rendered in the trigger header | | `isOpened` | Optional | `boolean` | `false` | Whether the panel starts expanded | | `isRow` | Optional | `boolean` | `false` | Lays out the content area as a flex row instead of column | | `noBorder` | Optional | `boolean` | `false` | Removes the bottom border from the trigger | | `onValueChange` | Optional | `((value: string \| string[]) => void)` | - | Fired when the open/close state changes | | `showToggleIcon` | Optional | `boolean` | `true` | Shows/hides the expand/collapse chevron icon | | `solidBackground` | Optional | `boolean` | `false` | Applies a solid background to the accordion container | | `title` | Optional | `string` | `""` | Header label shown in the trigger button | | `toggleIconPosition` | Optional | `"left" \| "right"` | `"left"` | Side on which the toggle icon appears | | `unpadded` | Optional | `boolean` | `false` | Removes the default inner padding from the content area | ### `AccordionContent` [**`Accordion`**](/reference/ui/components/layout/#accordion) Content component (collapsible content area) `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/accordion/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/accordion/index.tsx)
### `AccordionItem` Single collapsible row within an [``](/reference/ui/components/layout/#accordion). Pass a unique `value` so Radix can track open/closed state; wrap an [``](/reference/ui/components/layout/#accordiontrigger) and an [``](/reference/ui/components/layout/#accordioncontent) as children `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/accordion/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/accordion/index.tsx)
### `AccordionTrigger` [**`Accordion`**](/reference/ui/components/layout/#accordion) Trigger component (header/button) `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/accordion/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/accordion/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `customLabel` | Optional | `React.ReactNode` | - | Slot for extra content in the trigger header | | `showToggleIcon` | Optional | `boolean` | `true` | Shows/hides the toggle icon | | `timestamp` | Optional | `string` | - | Forwarded to the underlying Radix trigger element; not rendered by the component | | `toggleIconPosition` | Optional | `"left" \| "right"` | `"left"` | Side on which the toggle icon appears | ### `CardBody` Main content region of a `` — applies the standard inner padding and vertical rhythm `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/card/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/card/index.tsx)
### `CardFooter` Bottom action/metadata row of a ``, typically used for buttons, timestamps, or secondary info `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/card/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/card/index.tsx)
### `CardHeader` Top region of a `` that groups the title, optional subtitle, and an action slot `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/card/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/card/index.tsx)
### `ChartWrapper` [**`ChartWrapper`**](/reference/ui/components/layout/#chartwrapper) - Wrapper component for charts with loading and empty states Handles three states: - Loading: Shows skeleton loader - No data: Shows empty state placeholder - Has data: Shows chart content `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/chart-wrapper/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/chart-wrapper/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `children` | Optional | `React.ReactNode` | - | Chart content to render | | `className` | Optional | `string` | - | Custom className for the container | | `customLoader` | Optional | `React.ReactNode` | `` | Custom loader component to show when loading (overrides default spinner) | | `customNoDataMessage` | Optional | `React.ReactNode` | - | Custom message or component to show when no data | | `data` | Optional | `unknown[] \| Record` | - | Chart data object (for [**`LineChart`**](/reference/ui/components/charts/#linechart) with datasets) | | `dataset` | Optional | `unknown[] \| Record` | - | Chart dataset (for [**`BarChart`**](/reference/ui/components/charts/#barchart) with direct dataset) | | `isLoading` | Optional | `boolean` | `false` | Loading state | | `loadingMinHeight` | Optional | `number` | `minHeight` | Minimum height for the loading skeleton (in pixels) Falls back to minHeight if not provided | | `minHeight` | Optional | `number` | `400` | Minimum height for the container (in pixels) | | `showNoDataPlaceholder` | Optional | `boolean` | `true` | Whether to show "no data" placeholder when data is empty | ### `Divider` Thin horizontal or vertical rule used to separate logical sections of a layout `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/divider/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/divider/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `align` | Optional | `"center" \| "left" \| "right"` | `"center"` | Horizontal alignment of the label | | `children` | Optional | `React.ReactNode` | - | Text or node rendered in the middle of the divider | | `className` | Optional | `string` | - | Custom className | | `dashed` | Optional | `boolean` | `false` | Line style | | `dotted` | Optional | `boolean` | `false` | Renders a dotted line (takes precedence over `dashed`) | | `orientation` | Optional | `"horizontal" \| "vertical"` | `"horizontal"` | Line orientation | | `plain` | Optional | `boolean` | `false` | Plain text style — no border around label | ### `LabeledCard` Card variant that pairs a tiny label above the body — used for compact stat or metadata blocks `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/labeled-card/labeled-card.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/labeled-card/labeled-card.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `children` | Optional | `React.ReactNode` | - | Card body content | | `className` | Optional | `string` | - | Additional class for the root element | | `getNavigateOptions` | Optional | `(label: string) => Partial<{ href: string; target: string; }>` | - | Returns a link `href`/`target` for the label when provided | | `hasNoBorder` | Optional | `boolean` | `false` | Removes the card border | | `hasNoMargin` | Optional | `boolean` | `false` | Removes default margin | | `hasNoWrap` | Optional | `boolean` | `false` | Prevents content from wrapping | | `isDark` | Optional | `boolean` | `false` | Applies a dark background modifier | | `isFullHeight` | Optional | `boolean` | `false` | Stretches the card to full container height | | `isFullWidth` | Optional | `boolean` | `false` | Stretches the card to full container width | | `isRelative` | Optional | `boolean` | `false` | Sets `position: relative` on the container | | `isScrollable` | Optional | `boolean` | `false` | Enables vertical scroll on the card body | | `label` | Optional | `React.ReactNode` | - | Header content shown above the card body | ### `LazyTabWrapper` [**`LazyTabWrapper`**](/reference/ui/components/layout/#lazytabwrapper) - Wrapper for lazy-loaded tab components with Suspense Handles lazy loading with a fallback spinner while the component loads. Useful for code-splitting large tab components `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/lazy-tab-wrapper/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/lazy-tab-wrapper/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `Component` | Required | `React.ComponentType<{ data?: T \| undefined; }>` | - | Lazy-loaded component to render | | `data` | Optional | `T` | - | Data to pass to the component | | `fallback` | Optional | `React.ReactNode` | `` | Custom fallback component while loading | | `spinnerType` | Optional | `"circle" \| "square"` | `"circle"` | [**`Spinner`**](/reference/ui/components/feedback/#spinner) type when using default fallback | ### `Mosaic` [**`Mosaic`**](/reference/ui/components/layout/#mosaic) - Grid layout component with named areas Features: - Named grid areas for layout - Responsive: stacks on mobile, grid on desktop - Auto-generates column tracks - Validates column count vs template `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/mosaic/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/mosaic/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `children` | Required | `React.ReactNode` | - | `Mosaic.Item` elements | | `template` | Required | `string[] \| string[][]` | - | Grid area layout. 1D: each string is a row of space-separated area names. 2D: each inner array is a row | | `className` | Optional | `string` | - | Additional class for the grid element | | `columns` | Optional | `string \| string[]` | - | Custom `grid-template-columns` value or array of track sizes. Defaults to equal-width fractional tracks | | `gap` | Optional | `string` | `"12px"` | CSS gap between grid cells | | `rowHeight` | Optional | `string` | `"auto"` | CSS height for each row | ### `Tabs` [**`Tabs`**](/reference/ui/components/layout/#tabs) component for organizing content into panels `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/tabs/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/tabs/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `defaultValue` | Optional | `string` | - | Uncontrolled initial active value | | `onValueChange` | Optional | `((value: string) => void)` | - | Fired when the active tab changes | | `orientation` | Optional | `"horizontal" \| "vertical"` | `"horizontal"` | Keyboard navigation orientation | | `value` | Optional | `string` | - | Controlled active tab value | | `variant` | Optional | `"default" \| "side" \| "underline"` | - | - | # Media (/reference/ui/components/media) Components for displaying images, icons, and other media. ## 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. - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `ArrowIcon` Directional arrow glyph (up / down / left / right) used by sortable headers, breadcrumbs, and toggles `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/icons/arrow-icon.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/icons/arrow-icon.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `children` | Optional | `undefined` | - | - | | `color` | Optional | `string` | `"currentColor"` | Only affects single-color icons (default: 'currentColor') | | `isOpen` | Optional | `boolean` | - | - | | `size` | Optional | `string \| number` | - | Sets both width and height | # Miscellaneous (/reference/ui/components/misc) General-purpose and utility components. ## 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. - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `SparePartDetails` Presentational key-value list of a spare part's attributes (code, model, site, serial number, MAC). Renders data only; used inside the move/confirm spare-part dialogs but safe to compose standalone `advanced`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/inventory/spare-parts/move-spare-part-modal/spare-part-details.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/inventory/spare-parts/move-spare-part-modal/spare-part-details.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `sparePart` | Optional | `SparePartDetailsRecord` | - | - | # Monitoring (/reference/ui/components/monitoring) Components for system monitoring and status displays. ## 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. - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `LogActivityIcon` Status icon used inside [``](/reference/ui/components/monitoring/#logrow) to indicate event severity (info / warning / error / success) `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/logs/activity-log-icon.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/logs/activity-log-icon.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `status` | Required | `string` | - | - | ### `LogDot` Coloured dot used to mark a log event's severity or category inline with the row `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/logs/log-dot.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/logs/log-dot.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `status` | Required | `string` | - | Severity string for color mapping | | `type` | Required | `string` | - | `'Incidents'` renders a colored circle; `'Activity'` renders an activity icon | ### `LogItem` Compact log-line element rendering a single event with optional icon, timestamp, and message `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/logs/log-item.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/logs/log-item.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Required | `LogData` | - | - | | `onLogClicked` | Optional | `((uuid: string) => void)` | - | - | ### `LogRow` Single row of a logs feed — composes the dot/icon, timestamp, and message into one entry `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/logs/log-row.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/logs/log-row.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `log` | Required | `LogData` | - | Log entry data | | `type` | Required | `string` | - | Log type (controls dot appearance) | | `onLogClicked` | Optional | `((uuid: string) => void)` | - | Click handler | | `style` | Optional | `React.CSSProperties` | - | Inline style for the row container | ### `LogsCard` Card wrapper for a vertically scrolling logs feed with a sticky header `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/logs/logs-card.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/logs/logs-card.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `emptyMessage` | Optional | `string` | `"No active incidents"` | Message shown when `logsData` is empty | | `isDark` | Optional | `boolean` | `false` | Applies dark card theme | | `isLoading` | Optional | `boolean` | `false` | Shows skeleton rows while loading | | `label` | Optional | `string` | - | Card header label | | `logsData` | Optional | `LogData[]` | `[]` | Array of log entries to display | | `onLogClicked` | Optional | `(uuid: string) => void` | - | Fired with the log UUID when a row is clicked | | `pagination` | Optional | `LogPagination` | - | [**`Pagination`**](/reference/ui/components/navigation/#pagination) config; hides pagination when on page 1 or data is empty | | `skeletonRows` | Optional | `number` | `4` | Number of skeleton rows shown during loading | | `type` | Optional | `string` | - | Log type (`'Incidents'` or `'Activity'`); controls the [`LogDot`](/reference/ui/components/monitoring/#logdot) appearance | # Navigation (/reference/ui/components/navigation) Components for application navigation. ## 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. - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit ### Breadcrumbs ```tsx ``` Horizontal breadcrumb navigation. Renders an ordered trail of links / buttons / plain labels with an optional "Back" button on the left. #### Notes - The last item is rendered as the current page (`aria-current="page"`) - Items without `href` or `onClick` render as plain text #### Example [#breadcrumbs-example] ```tsx /** * Runnable example for Breadcrumbs. */ undefined} items={[ { label: 'Dashboard', href: '/' }, { label: 'Devices', onClick: () => undefined }, { label: 'Miner #42' }, ]} /> ) ``` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `AppHeader` Generic top-bar shell with three slots: `start`, `children` (middle), and `actions` (end). Renders a sticky dark surface; consumers compose any content into the slots. The sidebar collapse toggle, brand logo, stats strip, and action buttons are all caller-provided — this component owns no domain `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/app-header/app-header.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/app-header/app-header.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `actions` | Optional | `React.ReactNode` | - | Right-edge action cluster — e.g. alarms bell, profile menu, or sign-out | | `children` | Optional | `React.ReactNode` | - | Middle slot — e.g. the dashboard stats strip or page title | | `className` | Optional | `string` | - | Optional class hook for the outer `
` element | | `logo` | Optional | `React.ReactNode` | - | Left-most slot — typically the app's brand lockup / logo | | `start` | Optional | `React.ReactNode` | - | Left-edge content — e.g. a sidebar collapse toggle or brand wordmark | | `sticky` | Optional | `boolean` | `true` | Render the header sticky to the top of its scroll container | ### `Breadcrumbs` Hierarchical navigation trail that renders the path of pages leading to the current view. Each item can be a link (`href` or `onClick`) or plain text; the last item is rendered as the current page. Pass `showBack` to prepend a "back" affordance for mobile/touch layouts. The separator between items is customisable via `separator` (defaults to `/`) `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/breadcrumbs/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/breadcrumbs/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `items` | Required | `BreadcrumbItem[]` | - | Ordered trail; the last item is rendered as current | | `backClassName` | Optional | `string` | - | Class names applied to the back button | | `backLabel` | Optional | `string` | `"Back"` | [**`Label`**](/reference/ui/components/forms/#label) for the back button | | `className` | Optional | `string` | - | Root class names | | `itemClassName` | Optional | `string` | - | Class names applied to each item | | `onBackClick` | Optional | `VoidFunction` | - | Callback fired when the back button is clicked | | `separator` | Optional | `React.ReactNode` | `"/"` | Custom separator between items | | `showBack` | Optional | `boolean` | `false` | Show a leading "Back" button | ### `Pagination` [**`Pagination`**](/reference/ui/components/navigation/#pagination) component for navigating through pages `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/pagination/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/pagination/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `className` | Optional | `string` | - | Custom className for the root element | | `current` | Optional | `number` | `1` | Current active page number | | `disabled` | Optional | `boolean` | `false` | Disable pagination | | `onChange` | Optional | `((page: number, pageSize: number) => void)` | - | Callback when page number or page size changes | | `onSizeChange` | Optional | `((current: number, size: number) => void)` | - | Callback when page size changes | | `pageSize` | Optional | `number` | `20` | Number of items per page | | `pageSizeOptions` | Optional | `number[]` | `[10, 20, 50, 100]` | Page size options for the select dropdown | | `showSizeChanger` | Optional | `boolean` | `true` | Show page size changer | | `showTotal` | Optional | `boolean` | `false` | Show total count text | | `size` | Optional | `"sm" \| "md" \| "lg"` | `"sm"` | Size variant | | `total` | Optional | `number` | `0` | Total number of items | ### `Sidebar` Application sidebar with collapsible state, persistent expansion (via `localStorage`), optional overlay mode, and item-click + active-item highlighting `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/sidebar/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/sidebar/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `items` | Required | `SidebarMenuItem[]` | - | Menu items (supports nested `items`) | | `activeId` | Optional | `string` | - | Currently active item id | | `className` | Optional | `string` | - | Additional class names | | `defaultExpanded` | Optional | `boolean` | `false` | Initial expanded state | | `expanded` | Optional | `boolean` | - | Controlled expanded state | | `header` | Optional | `React.ReactNode` | - | Header content (e.g. logo, app name) | | `onClose` | Optional | `VoidFunction` | - | Called when the backdrop or ESC closes | | `onExpandedChange` | Optional | `((expanded: boolean) => void)` | - | Setter for the expanded state | | `onItemClick` | Optional | `((item: SidebarMenuItem) => void)` | - | Item-click handler | | `overlay` | Optional | `boolean` | `false` | Show as fixed overlay with backdrop | | `visible` | Optional | `boolean` | `true` | Hide entirely without unmounting | # Overlay (/reference/ui/components/overlays) Components for overlays and floating UI elements. ## 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. - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `Popover` [**`Popover`**](/reference/ui/components/overlays/#popover) Root - Container for a single popover `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/popover/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/popover/index.tsx)
### `SimplePopover` One-line convenience wrapper that pairs a trigger with floating content — no provider boilerplate `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/popover/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/popover/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `content` | Required | `React.ReactNode` | - | [**`Popover`**](/reference/ui/components/overlays/#popover) content | | `trigger` | Required | `React.ReactNode` | - | Element that triggers the popover | | `align` | Optional | `"center" \| "start" \| "end"` | `"center"` | Alignment of the popover | | `className` | Optional | `string` | - | Additional class for content | | `onOpenChange` | Optional | `((open: boolean) => void)` | - | Callback when open state changes | | `open` | Optional | `boolean` | - | Controlled open state | | `showArrow` | Optional | `boolean` | `false` | Whether to show the arrow | | `showClose` | Optional | `boolean` | `false` | Whether to show a close button | | `side` | Optional | `"left" \| "right" \| "top" \| "bottom"` | `"bottom"` | Position of the popover relative to trigger | | `sideOffset` | Optional | `number` | `8` | Distance from the trigger in pixels | ### `SimpleTooltip` One-line convenience wrapper around the full [``](/reference/ui/components/overlays/#tooltip) primitive set. Pass `content` (the hover text) and any single React element as `children` — [``](/reference/ui/components/overlays/#simpletooltip) handles the provider, trigger, portal and content for you. Prefer this over composing [``](/reference/ui/components/overlays/#tooltip) + `` + `` unless you need custom positioning or async content `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/tooltip/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/tooltip/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `children` | Required | `React.ReactNode` | - | Element that triggers the tooltip | | `content` | Required | `React.ReactNode` | - | [**`Tooltip`**](/reference/ui/components/overlays/#tooltip) content (string or JSX) | | `className` | Optional | `string` | - | Additional class for content | | `delayDuration` | Optional | `number` | `200` | Delay before showing tooltip (ms) | | `showArrow` | Optional | `boolean` | `true` | Whether to show the arrow | | `side` | Optional | `"left" \| "right" \| "top" \| "bottom"` | `"top"` | Position of the tooltip relative to trigger | | `sideOffset` | Optional | `number` | `8` | Distance from the trigger in pixels | ### `Tooltip` [**`Tooltip`**](/reference/ui/components/overlays/#tooltip) Root - Container for a single tooltip `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/tooltip/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/tooltip/index.tsx)
# Page (/reference/ui/components/pages) Components for full-page layouts and page structures. ## 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. - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `NotFoundPage` Full-page 404 view with a heading, supporting copy, and a primary call-to-action back to safety `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/not-found-page/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/not-found-page/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `className` | Optional | `string` | - | Additional CSS class name | | `message` | Optional | `string` | `"The page you are looking for does not exist."` | Message displayed below the title | | `onGoHome` | Optional | `VoidFunction` | - | Callback fired when the "Go Home" button is clicked | | `title` | Optional | `string` | `"404"` | Page title | # Settings (/reference/ui/components/settings) Components for settings and configuration interfaces. ## 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. - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `AddUserModal` Modal form for inviting a new user — captures email, role, and optional team assignment `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/settings/add-user-modal/add-user-modal.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/settings/add-user-modal/add-user-modal.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `onClose` | Required | `VoidFunction` | - | Called when the user dismisses the modal (cancel or backdrop) | | `onSubmit` | Required | `(data: { name: string; email: string; role: string; }) => Promise` | - | Async handler called with validated form values on submission | | `open` | Required | `boolean` | - | Controls whether the dialog is visible | | `roles` | Required | `RoleOption[]` | - | List of assignable roles rendered in the role select drop-down | | `isSubmitting` | Optional | `boolean` | `false` | Disables the submit button and shows a loading label while the parent is saving | ### `ChangeConfirmationModal` Generic confirmation modal that shows a diff or summary of pending changes before applying them `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/settings/change-confirmation-modal/change-confirmation-modal.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/settings/change-confirmation-modal/change-confirmation-modal.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `children` | Required | `React.ReactNode` | - | Body content — use to describe the change being confirmed | | `onClose` | Required | `VoidFunction` | - | Called when the user cancels or dismisses the dialog | | `onConfirm` | Required | `VoidFunction` | - | Called when the user clicks the confirm button | | `open` | Required | `boolean` | - | Controls whether the dialog is visible | | `title` | Required | `string` | - | Dialog header title | | `confirmText` | Optional | `string` | `"Confirm"` | [**`Label`**](/reference/ui/components/forms/#label) for the confirm button | | `destructive` | Optional | `boolean` | `false` | When `true`, the confirm button uses the danger variant | ### `FeatureFlagsSettings` Settings panel listing every feature flag with per-flag enable/disable toggle and description `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/settings/feature-flags/feature-flags-settings.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/settings/feature-flags/feature-flags-settings.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `featureFlags` | Required | `Record` | - | Current flag values keyed by flag name | | `isEditingEnabled` | Required | `boolean` | - | Whether editing is permitted | | `onSave` | Required | `(flags: Record) => void` | - | Called with updated flags when saved | | `className` | Optional | `string` | - | Additional CSS class | | `isLoading` | Optional | `boolean` | `false` | Show loading state | | `isSaving` | Optional | `boolean` | `false` | Show saving spinner on save button | ### `HeaderControlsSettings` Settings panel for the global app header — toggle controls, sticky behaviour, and theme defaults `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/settings/header-controls/header-controls-settings.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/settings/header-controls/header-controls-settings.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `onReset` | Required | `VoidFunction` | - | Reset all preferences to defaults | | `onToggle` | Required | `(key: keyof HeaderPreferences, value: boolean) => void` | - | Called when a preference toggle changes | | `preferences` | Required | `HeaderPreferences` | - | Current header preference values | | `className` | Optional | `string` | - | Additional CSS class | | `isLoading` | Optional | `boolean` | `false` | Show loading state | ### `ImportExportSettings` Settings panel for exporting site configuration as JSON and importing a previously saved snapshot `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/settings/import-export/import-export-settings.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/settings/import-export/import-export-settings.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `onExport` | Required | `VoidFunction` | - | Trigger configuration export | | `onImport` | Required | `(data: SettingsExportData) => void` | - | Apply imported configuration | | `className` | Optional | `string` | - | Additional CSS class | | `isExporting` | Optional | `boolean` | `false` | Show export loading state | | `isImporting` | Optional | `boolean` | `false` | Show import loading state | | `onParseFile` | Optional | `((file: File) => Promise)` | - | Custom file-parsing function | ### `ManageUserModal` Modal for editing an existing user's role, status, or team membership `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/settings/manage-user-modal/manage-user-modal.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/settings/manage-user-modal/manage-user-modal.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `onClose` | Required | `VoidFunction` | - | Called when the dialog closes | | `onSubmit` | Required | `(data: { id: string; name: string; email: string; role: string; }) => Promise` | - | Save handler | | `open` | Required | `boolean` | - | Controls dialog visibility | | `permissionLabels` | Required | `Record` | - | Display labels for permission keys | | `rolePermissions` | Required | `Record>` | - | Permission levels per role | | `roles` | Required | `RoleOption[]` | - | Available role options | | `user` | Required | `SettingsUser` | - | The user being edited | | `isSubmitting` | Optional | `boolean` | `false` | Show loading on submit button | ### `RBACControlSettings` Settings panel listing roles and their permissions; supports editing role-permission grants `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/settings/rbac-control/rbac-control-settings.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/settings/rbac-control/rbac-control-settings.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `canWrite` | Required | `boolean` | - | Whether the current user may edit access settings | | `onCreateUser` | Required | `(data: { name: string; email: string; role: string; }) => Promise` | - | Create a new user | | `onDeleteUser` | Required | `(userId: string) => Promise` | - | Delete a user | | `onUpdateUser` | Required | `(data: { id: string; name: string; email: string; role: string; }) => Promise` | - | Update an existing user's role | | `permissionLabels` | Required | `Record` | - | Display labels for permission keys | | `rolePermissions` | Required | `Record>` | - | Permission levels per role | | `roles` | Required | `RoleOption[]` | - | Available role options | | `users` | Required | `SettingsUser[]` | - | List of current users | | `className` | Optional | `string` | - | Additional CSS class | | `isLoading` | Optional | `boolean` | `false` | Show loading state | ### `SettingsDashboard` Top-level settings landing page — composes the per-section settings cards in a single grid `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/settings/settings-dashboard/settings-dashboard.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/settings/settings-dashboard/settings-dashboard.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `className` | Optional | `string` | - | Additional CSS class | | `dangerActions` | Optional | `ActionButtonProps[]` | - | Danger-zone action buttons (reset, delete) | | `featureFlagsProps` | Optional | `FeatureFlagsSettingsProps` | - | Props forwarded to [`FeatureFlagsSettings`](/reference/ui/components/settings/#featureflagssettings) | | `headerControlsProps` | Optional | `HeaderControlsSettingsProps` | - | Props forwarded to [`HeaderControlsSettings`](/reference/ui/components/settings/#headercontrolssettings) | | `importExportProps` | Optional | `ImportExportSettingsProps` | - | Props forwarded to [`ImportExportSettings`](/reference/ui/components/settings/#importexportsettings) | | `rbacControlProps` | Optional | `RBACControlSettingsProps` | - | Props forwarded to [`RBACControlSettings`](/reference/ui/components/settings/#rbaccontrolsettings) | | `showFeatureFlags` | Optional | `boolean` | `false` | Whether to show the feature-flags section | # Table (/reference/ui/components/tables) Components for displaying and interacting with tabular data. ## 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. - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit ### Data table ```tsx ``` Sortable, paginated, optionally selectable / expandable table built on TanStack React Table. Controlled and uncontrolled modes for each piece of state. #### Props (subset) > **Props are generated from this component's TypeScript types, the source of truth.** The following are supplementary (object-prop shapes, allowed values, or passthrough props), not the full prop list. | Prop | Status | Type | Default | Description | | ------------------------- | -------- | ----------------------------------------- | ------- | --------------------------------------- | | `data` | Required | `I[]` | — | Rows | | `columns` | Required | `DataTableColumnDef[]` | — | TanStack column defs | | `fullWidth` | Optional | `boolean` | `true` | Stretch to container width | | `enableRowSelection` | Optional | `boolean \| ((row) => boolean)` | `false` | Checkbox column | | `enableMultiRowSelection` | Optional | `boolean` | `true` | Allow multi-select | | `selections` | Optional | `DataTableRowSelectionState` | — | Controlled row-selection state | | `onSelectionsChange` | Optional | `(s: DataTableRowSelectionState) => void` | — | Setter | | `enablePagination` | Optional | `boolean` | `true` | Show pagination footer | | `pagination` | Optional | `DataTablePaginationState` | — | Controlled pagination | | `sorting` | Optional | `DataTableSortingState` | — | Controlled sorting | | `bordered` | Optional | `boolean` | `false` | Add cell borders | | `loading` | Optional | `boolean` | `false` | Show loading overlay | | `enableRowExpansion` | Optional | `boolean` | `false` | Show row expansion column | | `renderExpandedContent` | Optional | `(row) => ReactNode` | — | Required when row expansion is enabled | | `getRowId` | Optional | `(row, index, parent?) => string` | index | Stable row ID source | | `onRowClick` | Optional | `(rowData: I) => void` | — | Makes rows interactive (see note below) | See [`data-table.tsx`](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/data-table/data-table.tsx) for the full list (16 props). > [!NOTE] > Setting `onRowClick` makes every body row `role="button"`, focusable, and keyboard-activatable > (Enter/Space). Clicks starting inside a `button`, `a`, `input`, `label`, `[role="checkbox"]`, or > anything marked `data-no-row-click` are ignored, so the selection checkbox and expand toggle keep > working independently of the row click. ### Column `meta` | Field | Applied by `DataTable` | Description | | ------- | ---------------------- | -------------------------------------- | | `align` | Yes | `left` \| `center` \| `right` on cells | #### Data contracts `DataTableColumnDef`, `DataTableRow`, `DataTableSortingState`, `DataTablePaginationState`, `DataTableRowSelectionState`, `DataTableExpandedState` are re-exported from `@tetherto/mdk-react-devkit`. #### Example [#data-table-example] ```tsx /** * Runnable example for DataTable. */ type Miner = { id: string status: 'online' | 'warning' | 'offline' hashrate: number power_w: number } const data: Miner[] = [ { id: 'miner-01', status: 'online', hashrate: 102.4, power_w: 3200 }, { id: 'miner-02', status: 'warning', hashrate: 95.1, power_w: 3450 }, { id: 'miner-03', status: 'offline', hashrate: 0, power_w: 0 }, ] const columns: DataTableColumnDef[] = [ { accessorKey: 'id', header: 'Miner' }, { accessorKey: 'status', header: 'Status' }, { accessorKey: 'hashrate', header: 'Hashrate (TH/s)' }, { accessorKey: 'power_w', header: 'Power (W)' }, ] return data={data} columns={columns} getRowId={(row) => row.id} /> } ``` #### Related API - [**`useExplorerData`**](/reference/ui/hooks/tables/#useexplorerdata) @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `AlertConfirmationModal` Modal that confirms acknowledging or clearing one or more alerts before applying the change `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/alerts/current-alerts/alert-confirmation-modal/alert-confirmation-modal.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/alerts/current-alerts/alert-confirmation-modal/alert-confirmation-modal.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `isOpen` | Required | `boolean` | - | Controls dialog visibility | | `onOk` | Required | `VoidFunction` | - | Called when the user confirms the action | ### `Alerts` Full alerts page — combines the searchable current-alerts table, an optional historical-alerts log section, severity filters, and the sound confirmation modal. Wraps [`CurrentAlerts`](/reference/ui/components/tables/#currentalerts) and [`HistoricalAlerts`](/reference/ui/components/tables/#historicalalerts) and coordinates their shared filter / date-range / selected-id state. Must be rendered inside `` — the embedded tables read tag filters from the devices store and use the timezone formatter hook `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/features/alerts/alerts/alerts.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/features/alerts/alerts/alerts.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `className` | Optional | `string` | - | Extra class on the page wrapper | | `dateRange` | Optional | `HistoricalAlertsRange` | `last 14 days` | Controlled date range for the historical alerts. Defaults to last 14 days | | `devices` | Optional | `Device[]` | - | Devices powering the "Current [**`Alerts`**](/reference/ui/components/tables/#alerts)" table — a flat list of rows that carry `last.alerts`. Any backend that can produce that shape works; there is no response envelope to reproduce | | `header` | Optional | `React.ReactNode` | - | Optional header (e.g. breadcrumbs) rendered above the alerts | | `historicalAlerts` | Optional | `Alert[]` | - | Pre-fetched historical alerts log entries | | `initialSeverity` | Optional | `string` | - | Initial severity selection (typically derived from `?severity=` URL param) | | `isCurrentAlertsLoading` | Optional | `boolean` | `false` | Loading flag for the "Current [**`Alerts`**](/reference/ui/components/tables/#alerts)" table | | `isDemoMode` | Optional | `boolean` | `false` | When true, sound notifications are skipped (e.g. demo / preview) | | `isHistoricalAlertsEnabled` | Optional | `boolean` | `false` | When true, shows the "Historical [**`Alerts`**](/reference/ui/components/tables/#alerts) Log" section. Mirrors the `alertsHistoricalLogEnabled` feature flag in the source app | | `isHistoricalAlertsLoading` | Optional | `boolean` | `false` | Loading flag for the historical log | | `isSoundEnabled` | Optional | `boolean` | `false` | Whether sound notifications are enabled in user preferences | | `onAlertClick` | Optional | `((id?: string \| undefined, uuid?: string \| undefined) => void)` | - | Callback invoked when the operator clicks an alert row. Receives the device id and alert uuid | | `onDateRangeChange` | Optional | `((range: HistoricalAlertsRange) => void)` | - | Called when the operator picks a new historical range | | `selectedAlertId` | Optional | `string` | - | Optional alert id used to focus on a single alert (deep-link from URL) | | `typeFiltersForSite` | Optional | `CascaderOption[]` | - | Optional site-specific overrides for the type filter | ### `AlertsTableTitle` Title strip for an alerts table with the section heading and an optional count badge `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/alerts/alerts-table-title/alerts-table-title.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/alerts/alerts-table-title/alerts-table-title.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `title` | Required | `React.ReactNode` | - | Section heading | | `className` | Optional | `string` | - | Additional CSS class | | `subtitle` | Optional | `React.ReactNode` | - | Optional subtitle or count badge | ### `CurrentAlerts` Sortable, searchable data table of currently active alerts derived from a raw devices payload. Plays an audible beep when a critical alert is present (gated by `isSoundEnabled` + user confirmation modal) `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/alerts/current-alerts/current-alerts.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/alerts/current-alerts/current-alerts.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `filterTags` | Required | `string[]` | - | Search tags (controlled). Mirrors the redux `selectFilterTags` slice in the source app | | `localFilters` | Required | `AlertLocalFilters` | - | Filters controlled outside (typically by URL severity param) | | `onFilterTagsChange` | Required | `(tags: string[]) => void` | - | Setter for the tags above | | `onLocalFiltersChange` | Required | `(filters: AlertLocalFilters) => void` | - | Setter for the filters above | | `className` | Optional | `string` | - | Additional class names | | `devices` | Optional | `Device[]` | - | Devices carrying alerts (`last.alerts`), as a flat list. Unwrapping any backend envelope is the data layer's job, not this component's. Shape mirrors the API response from the source app | | `isDemoMode` | Optional | `boolean` | `false` | Skip sound entirely (e.g. in demo/preview environments) | | `isLoading` | Optional | `boolean` | `false` | Show [**`DataTable`**](/reference/ui/components/tables/#datatable) loading overlay | | `isSoundEnabled` | Optional | `boolean` | `false` | Whether sound notifications are enabled in user preferences (e.g. theme slice) | | `onAlertClick` | Optional | `((id?: string \| undefined, uuid?: string \| undefined) => void)` | - | Click handler when the user opens an alert (right arrow icon in the row) | | `selectedAlertId` | Optional | `string` | - | Optional id used to focus on a single alert (deep-link from URL) | | `typeFiltersForSite` | Optional | `CascaderOption[]` | - | Optional site-specific overrides for the type filter | ### `DataTable` Generic typed data table built on TanStack React Table. Supports pagination, sorting, expansion, row selection and custom column renderers `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/data-table/data-table.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/primitives/components/data-table/data-table.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `columns` | Required | `ColumnDef[]` | - | The column configuration table. See https://tanstack.com/table/v8/docs/guide/column-defs | | `data` | Required | `I[]` | - | The data to be shown in the table. See https://tanstack.com/table/v8/docs/guide/data | | `bordered` | Optional | `boolean` | `false` | Add borders to all cells | | `canRowExpand` | Optional | `((row: Row) => boolean)` | `false` | Callback to check if a row can be expanded | | `contentClassName` | Optional | `string` | - | Classname of the content element | | `defaultSorting` | Optional | `SortingState` | `[]` | Default sorting applied when the table is first mounted (uncontrolled mode). Ignored when `sorting` is provided | | `enableMultiRowSelection` | Optional | `boolean` | `true` | Enables selection of multiple rows | | `enablePagination` | Optional | `boolean` | `true` | Show pagination | | `enableRowExpansion` | Optional | `boolean` | `false` | Show a columns with a button which can expand the row | | `enableRowSelection` | Optional | `boolean \| ((row: Row) => boolean)` | `false` | Show a checkbox column and enables selection | | `expandedRows` | Optional | `ExpandedState` | - | Specify the expanded rows If `undefined`, the expansions are managed internally Object with the key of row ID and a boolean specifying if the row is selected. The default row ID is the index. This can be changed using `getRowId` prop | | `fullWidth` | Optional | `boolean` | `true` | Is table full width | | `getRowId` | Optional | `((row: I, index: number, parent?: Row \| undefined) => string)` | `index` | Get the row ID for a row. If not specified index is the default row ID | | `loading` | Optional | `boolean` | `false` | Show a loading indicator overlay | | `onExpandedRowsChange` | Optional | `((expandedRows: ExpandedState) => void)` | - | Callback to be called when the rows are expanded or collapsed | | `onPaginationChange` | Optional | `((pagination: PaginationState) => void)` | - | Callback to be called when the pagination params change | | `onRowClick` | Optional | `((rowData: I) => void)` | - | Called when a body row is clicked. When set, rows become interactive (pointer cursor, `role="button"`, keyboard-activatable with Enter/Space). Clicks originating from an interactive control in the row (button, link, input, checkbox, or anything marked `data-no-row-click`) are ignored, so the selection checkbox and expand toggle keep working independently | | `onSelectionsChange` | Optional | `((selections: RowSelectionState) => void)` | - | Callback to be called when the row are selected / unselected | | `onSortingChange` | Optional | `((sorting: SortingState) => void)` | - | Callback to be called when the sorting changes | | `pagination` | Optional | `PaginationState` | `undefined` | Specify the pagination params. Object of shape { pageIndex: number, pageSize: number }. If `undefined` then the pagination is managed internally | | `renderExpandedContent` | Optional | `((row: Row) => React.ReactNode)` | - | Render the content of the expanded row. Required when `enableRowExpansion` is `true` | | `selections` | Optional | `RowSelectionState` | `undefined` | Specify the selected rows. If `undefined`, the selections are managed internally Object with the key of row ID and a boolean specifying if the row is selected. The default row ID is the index. This can be changed using `getRowId` prop | | `sorting` | Optional | `SortingState` | `undefined` | Specify the sorting params. If `undefined` then the sorting is managed internally | | `tableClassName` | Optional | `string` | - | Classname of the table element | | `wrapperClassName` | Optional | `string` | - | Classname of the wrapper element | ### `DeviceExplorer` Top-level device explorer: filter toolbar + searchable, sortable table of miners, containers, or cabinets. Designed to be controlled by URL state in the host app `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/device-explorer/device-explorer.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/device-explorer/device-explorer.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Required | `Device[]` | - | Rows | | `deviceType` | Required | `"container" \| "miner" \| "cabinet"` | - | Active device-type tab | | `filterOptions` | Required | `DeviceExplorerFilterOption[]` | - | Filter category definitions | | `getFormattedDate` | Required | `(date: Date) => string` | - | Date formatter from the host's timezone setup | | `onDeviceTypeChange` | Required | `(type: DeviceExplorerDeviceType) => void` | - | Setter for the device type | | `onFiltersChange` | Required | `(value: LocalFilters) => void` | - | Setter for filters | | `onSearchTagsChange` | Required | `(tags: string[]) => void` | - | Setter for search tags | | `renderAction` | Required | `(device: Device) => React.ReactNode` | - | Renderer for the per-row action cell | | `searchOptions` | Required | `DeviceExplorerSearchOption[]` | - | Searchable column definitions | | `searchTags` | Required | `string[]` | - | Active search-tag chips | | `className` | Optional | `string` | - | Additional class names | | `filters` | Optional | `LocalFilters` | - | Controlled filter values | | `onRowClick` | Optional | `((device: Device) => void)` | - | Called when a row is clicked (e.g. to open the device's detail page) | | `onSelectedDevicesChange` | Optional | `((selections: RowSelectionState) => void)` | - | Setter for row selection | | `onSortingChange` | Optional | `((sorting: SortingState) => void)` | - | - | | `selectedDevices` | Optional | `RowSelectionState` | - | Controlled row-selection state | ### `EfficiencyMinerTypeView` Efficiency drilldown grouped by miner model — J/TH and uptime for each model in the fleet `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/efficiency/tabs/miner-type-view/miner-type-view.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/efficiency/tabs/miner-type-view/miner-type-view.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `chartInput` | Optional | `ToBarChartDataInput` | `{ series: [] }` | Bar chart series data | | `isEmpty` | Optional | `boolean` | `false` | Shows the empty state instead of the chart | | `isLoading` | Optional | `boolean` | `false` | Loading state | | `onTimeFrameChange` | Optional | `((start: Date, end: Date) => void)` | - | Fired when the time-frame selector changes | ### `EfficiencyMinerUnitView` Efficiency drilldown by individual miner serial — outliers and worst-performers surface here `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/efficiency/tabs/miner-unit-view/miner-unit-view.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/efficiency/tabs/miner-unit-view/miner-unit-view.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `chartInput` | Optional | `ToBarChartDataInput` | `{ series: [] }` | Bar chart series data | | `isEmpty` | Optional | `boolean` | `false` | Shows the empty state instead of the chart | | `isLoading` | Optional | `boolean` | `false` | Loading state | | `onTimeFrameChange` | Optional | `((start: Date, end: Date) => void)` | - | Fired when the time-frame selector changes | ### `EfficiencySiteView` Site-level efficiency view — site-aggregate J/TH, uptime, and capacity utilisation cards `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/efficiency/tabs/site-view/site-view.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/efficiency/tabs/site-view/site-view.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `avgEfficiency` | Optional | `number \| null` | `null` | Average efficiency value | | `dateRange` | Optional | `EfficiencyDateRange` | - | Selected date range | | `isLoading` | Optional | `boolean` | `false` | Loading state | | `log` | Optional | `MetricsEfficiencyLogEntry[]` | `[]` | Efficiency log entries | | `nominalValue` | Optional | `number \| null` | `null` | Nominal target efficiency | | `onDateRangeChange` | Optional | `((range: EfficiencyDateRange) => void)` | - | Date range change handler | | `onReset` | Optional | `VoidFunction` | - | Reset handler | ### `HistoricalAlerts` Sortable data table of historical alerts within a controlled date range. Renders a header with title + [`DateRangePicker`](/reference/ui/components/forms/#daterangepicker), then the table `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/alerts/historical-alerts/historical-alerts.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/alerts/historical-alerts/historical-alerts.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `dateRange` | Required | `HistoricalAlertsRange` | - | Selected date range for the historical query (controlled) | | `filterTags` | Required | `string[]` | - | Shared with [`CurrentAlerts`](/reference/ui/components/tables/#currentalerts) | | `localFilters` | Required | `AlertLocalFilters` | - | Filters and search tags coming from the parent (typically shared with [`CurrentAlerts`](/reference/ui/components/tables/#currentalerts)) | | `onDateRangeChange` | Required | `(range: HistoricalAlertsRange) => void` | - | Setter for the date range | | `alerts` | Optional | `Alert[]` | `[]` | Pre-fetched historical alerts log entries (each with a `thing` device payload) | | `className` | Optional | `string` | - | Additional class names | | `isLoading` | Optional | `boolean` | `false` | Show [**`DataTable`**](/reference/ui/components/tables/#datatable) loading overlay | | `onAlertClick` | Optional | `((id?: string \| undefined, uuid?: string \| undefined) => void)` | - | Called when the user opens an alert | ### `OperationsEfficiency` Top-level operations-efficiency section of the report — composes the site/type/unit views `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/efficiency/efficiency.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/operational/efficiency/efficiency.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `defaultTab` | Optional | `"site-view" \| "miner-type-view" \| "mining-unit-view"` | `"site-view"` | Initially selected tab | | `minerTypeView` | Optional | `EfficiencyMinerTypeViewProps` | - | Props forwarded to [`EfficiencyMinerTypeView`](/reference/ui/components/tables/#efficiencyminertypeview) | | `minerUnitView` | Optional | `EfficiencyMinerUnitViewProps` | - | Props forwarded to [`EfficiencyMinerUnitView`](/reference/ui/components/tables/#efficiencyminerunitview) | | `siteView` | Optional | `EfficiencySiteViewProps` | - | Props forwarded to [`EfficiencySiteView`](/reference/ui/components/tables/#efficiencysiteview) | ### `PoolManagerMinerExplorer` Pool-manager miner explorer page — searchable / filterable table of miners with multi-select and an "Assign Pool" bulk action. Submits the chosen pool change as a pending action via the adapter actions store, then opens a confirmation modal. Must be rendered inside `` — the page uses `useActions`, `useCheckPerm`, and `useContextualModal` from `@tetherto/mdk-react-adapter` `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/features/pool-manager/miner-explorer/pool-manager-miner-explorer.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/features/pool-manager/miner-explorer/pool-manager-miner-explorer.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `backButtonClick` | Required | `VoidFunction` | - | Called when the operator clicks the "Pool Manager" back link | | `miners` | Required | `ListThingsDevice[]` | - | Miners to render in the explorer table | | `poolConfig` | Required | `PoolConfigEntry[]` | - | Pool configurations powering the "Assign Pool" dropdown | ### `PoolManagerPools` Pool-manager pools page — accordion list of every configured pool with header summary (name, status, priority) and an expandable body (per-pool stats, edit, delete). Optional "Add Pool" CTA is gated by the `ADD_POOL_ENABLED` feature flag. Must be rendered inside `` — the embedded "Add Pool" modal uses the contextual-modal adapter hook `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/features/pool-manager/pools/pool-manager-pools.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/features/pool-manager/pools/pool-manager-pools.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `backButtonClick` | Required | `VoidFunction` | - | Called when the operator clicks the "Pool Manager" back link | | `poolConfig` | Required | `PoolConfigEntry[]` | - | Array of pool configurations to render | ### `RepairLogChangesSubRow` Expandable sub-row that lists the spare-part changes recorded in a repair batch action. Each non-miner repair action is resolved against its device to show the part type, serial number, MAC address, and whether the part was added or removed. Device data is fetched by the parent and passed in via props — the component does no data fetching itself `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/repairs/repair-log-changes-sub-row.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/repairs/repair-log-changes-sub-row.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `batchAction` | Required | `object` | - | The repair batch action whose part changes should be displayed | | `devices` | Required | `Partial<{ id: string; rack: string; info: Partial<{ serialNum: string; macAddress: string; }>; }>[]` | - | Devices referenced by the batch action, pre-fetched by the parent | | `isLoading` | Optional | `boolean` | `false` | Show a spinner while the parent is still fetching `devices` | ### `TagFilterBar` Horizontal strip of removable tag chips that narrow a list view; clicking a chip removes the filter `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/alerts/tag-filter-bar/tag-filter-bar.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/alerts/tag-filter-bar/tag-filter-bar.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `filterTags` | Required | `string[]` | - | Active tag filter values | | `localFilters` | Required | `AlertLocalFilters` | - | Current local filter state | | `onLocalFiltersChange` | Required | `(filters: AlertLocalFilters) => void` | - | Called when any local filter changes | | `onSearchTagsChange` | Required | `(tags: string[]) => void` | - | Called when tag filter changes | | `className` | Optional | `string` | - | Additional CSS class | | `placeholder` | Optional | `string` | - | Search input placeholder | | `typeFiltersForSite` | Optional | `CascaderOption[]` | - | Site-specific overrides for the "type" filter children. If provided, the "Type" filter group will use these instead of the defaults | # Widget (/reference/ui/components/widgets) Widget components for dashboards and data displays. ## 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. - Import styles: `import '@tetherto/mdk-react-devkit/styles.css'` ## Components @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `AddReplaceMinerDialog` Modal for adding a new miner to a slot or swapping the existing one with a replacement unit `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/dialogs/add-replace-miner-dialog/add-replace-miner-dialog.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/dialogs/add-replace-miner-dialog/add-replace-miner-dialog.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `onClose` | Required | `VoidFunction` | - | Called when the dialog should close | | `open` | Required | `boolean` | - | Controls dialog visibility | | `currentDialogFlow` | Optional | `string` | - | Active flow identifier | | `isDirectToMaintenanceMode` | Optional | `boolean` | `false` | Skip add/replace and go directly to maintenance | | `minersType` | Optional | `string` | - | Miner hardware type filter | | `selectedEditSocket` | Optional | `UnknownRecord` | - | [**`Socket`**](/reference/ui/components/widgets/#socket) being edited | | `selectedSocketToReplace` | Optional | `UnknownRecord` | - | [**`Socket`**](/reference/ui/components/widgets/#socket) being replaced | ### `BatchContainerControlsCard` Bulk-controls card that applies start/stop/mode changes to multiple selected containers at once `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/batch-container-controls-card/batch-container-controls-card.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/batch-container-controls-card/batch-container-controls-card.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `alarmsDataItems` | Optional | `TimelineItemData[]` | - | Alarm timeline entries to display | | `connectedMiners` | Optional | `unknown` | - | Array of currently connected miners | | `isBatch` | Optional | `boolean` | `true` | Whether in batch (multi-select) mode | | `isCompact` | Optional | `boolean` | - | Compact layout for tighter spaces | | `onNavigate` | Optional | `(path: string) => void` | - | Navigation callback for alarm deep-links | ### `BitdeerOptions` Options panel for a Bitdeer container — exposes vendor-specific operating modes and thresholds. Main container for Bitdeer cooling system options. Displays dry cooler and pumps components `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitdeer/settings/container-options/bitdeer-options.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitdeer/settings/container-options/bitdeer-options.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Optional | `UnknownRecord` | - | Container settings payload; both components derive state from `cooling_system` fields | ### `BitdeerPumps` Pump telemetry panel for a Bitdeer container showing per-pump RPM, flow, and alert states. Displays exhaust fan status using an indicator `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitdeer/settings/container-options/bitdeer-pumps.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitdeer/settings/container-options/bitdeer-pumps.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Optional | `UnknownRecord` | - | - | ### `BitdeerSettings` Settings tab for a Bitdeer container — vendor-specific configuration controls and limits. Displays container parameter settings and editable threshold forms for Bitdeer containers with oil temperature and tank pressure monitoring. Includes: - Container parameter display (MAC address, serial number, etc.) - Editable threshold forms for oil temperature and tank pressure - Color-coded alerts and flash indicators - Sound alert configuration `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitdeer/settings/bitdeer-settings.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitdeer/settings/bitdeer-settings.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Optional | `UnknownRecord` | `{}` | Container settings payload from the API | ### `BitdeerTankPressureCharts` Stacked time-series of dielectric tank pressure for a Bitdeer immersion container `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitdeer/charts/bitdeer-tank-pressure-charts.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitdeer/charts/bitdeer-tank-pressure-charts.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `chartDataPayload` | Optional | `ChartDataPayload` | - | Declarative chart configuration | | `chartTitle` | Optional | `string` | - | Title shown in the chart header | | `data` | Optional | `UnknownRecord[]` | `[]` | Raw container telemetry entries (with `ts` + nested stats group) | | `dateRange` | Optional | `{ start?: number \| undefined; end?: number \| undefined; }` | - | Custom date range as Unix epoch seconds. Currently unused — accepted by the shared props type but not read by the chart components | | `fixedTimezone` | Optional | `string` | - | IANA timezone for x-axis ticks | | `footer` | Optional | `React.ReactNode` | - | Footer (e.g. min/max/avg stats) | | `height` | Optional | `number` | - | Chart pixel height | | `rangeOptions` | Optional | `{ label: string; value: string; }[]` | `5m/30m/3h/1D` | Override the default range selector options | | `showLegend` | Optional | `boolean` | `true` | Show the toggleable legend | | `showRangeSelector` | Optional | `boolean` | `true` | Show the range selector buttons | | `tag` | Optional | `string` | - | Container tag (any leading prefix is stripped) | | `timeline` | Optional | `string` | `"24h"` | Initial / controlled timeline value | ### `BitdeerTankTempCharts` Tank Temperature Charts for Bitdeer containers Displays oil and water temperature readings (hot and cold) for a specific tank. Features: - Tank Oil TempL (Cold) - Yellow line (4px) - Tank Oil TempH (Hot) - Violet line - Tank Water TempL (Cold) - Blue line - Tank Water TempH (Hot) - Sky Blue line `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitdeer/charts/bitdeer-tank-temp-charts.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitdeer/charts/bitdeer-tank-temp-charts.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `chartDataPayload` | Optional | `ChartDataPayload` | - | Declarative chart configuration | | `chartTitle` | Optional | `string` | - | Title shown in the chart header | | `data` | Optional | `UnknownRecord[]` | `[]` | Raw container telemetry entries (with `ts` + nested stats group) | | `dateRange` | Optional | `{ start?: number \| undefined; end?: number \| undefined; }` | - | Custom date range as Unix epoch seconds. Currently unused — accepted by the shared props type but not read by the chart components | | `fixedTimezone` | Optional | `string` | - | IANA timezone for x-axis ticks | | `footer` | Optional | `React.ReactNode` | - | Footer (e.g. min/max/avg stats) | | `height` | Optional | `number` | - | Chart pixel height | | `rangeOptions` | Optional | `{ label: string; value: string; }[]` | `5m/30m/3h/1D` | Override the default range selector options | | `showLegend` | Optional | `boolean` | `true` | Show the toggleable legend | | `showRangeSelector` | Optional | `boolean` | `true` | Show the range selector buttons | | `tag` | Optional | `string` | - | Container tag (any leading prefix is stripped) | | `tankNumber` | Optional | `string \| number` | `1` | Tank number (1 or 2) | | `timeline` | Optional | `string` | `"24h"` | Initial / controlled timeline value | ### `BitMainBasicSettings` General settings form for a BitMain container — naming, location, and power limits. Main settings view displaying: - Cooling system status (pumps and fans) - Power distribution - GPS positioning `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain/status-item/settings/bitmain-basic-settings.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain/status-item/settings/bitmain-basic-settings.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Optional | `Device` | - | Container data | ### `BitMainControlsTab` Read-only status tab for a BitMain container: fan status, tank levels, and GPS location. Displays container controls including: - Container fan status - Tank levels (A, B, C, D) - GPS location (latitude/longitude) `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain-immersion/controls-tab/bitmain-immersion-controls-tab.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain-immersion/controls-tab/bitmain-immersion-controls-tab.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Required | `Device` | - | Device data | ### `BitMainCoolingSystem` Cooling subsystem panel for a BitMain container — pumps, fans, and dry-cooler status. Displays the status of cooling system components including: - Circulating pump - Fluid infusion pump - Fans (1-2) - Cooling tower fans (1-3) `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain/status-item/settings/cooling-system/bitmain-cooling-system.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain/status-item/settings/cooling-system/bitmain-cooling-system.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Optional | `Device` | - | Container data | ### `BitMainHydroLiquidTemperatureCharts` Time-series charts of dielectric liquid temperature for a BitMain hydro-cooled container. Displays secondary liquid supply temperature readings (Temp1 and Temp2) for Bitmain Hydro containers. Features: - Sec. Liquid supply Temp1 - Sky Blue line - Sec. Liquid supply Temp2 - Violet line - Interactive legend for toggling series - Timeline selector (1h, 6h, 24h, 7d, 30d) - Current temperature display `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain/charts/bitmain-hydro-liquid-temperature-charts.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain/charts/bitmain-hydro-liquid-temperature-charts.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `chartDataPayload` | Optional | `ChartDataPayload` | - | Declarative chart configuration | | `chartTitle` | Optional | `string` | - | Title shown in the chart header | | `data` | Optional | `UnknownRecord[]` | `[]` | Raw container telemetry entries (with `ts` + nested stats group) | | `dateRange` | Optional | `{ start?: number \| undefined; end?: number \| undefined; }` | - | Custom date range as Unix epoch seconds. Currently unused — accepted by the shared props type but not read by the chart components | | `fixedTimezone` | Optional | `string` | - | IANA timezone for x-axis ticks | | `footer` | Optional | `React.ReactNode` | - | Footer (e.g. min/max/avg stats) | | `height` | Optional | `number` | - | Chart pixel height | | `rangeOptions` | Optional | `{ label: string; value: string; }[]` | `5m/30m/3h/1D` | Override the default range selector options | | `showLegend` | Optional | `boolean` | `true` | Show the toggleable legend | | `showRangeSelector` | Optional | `boolean` | `true` | Show the range selector buttons | | `tag` | Optional | `string` | - | Container tag (any leading prefix is stripped) | | `timeline` | Optional | `string` | `"24h"` | Initial / controlled timeline value | ### `BitMainHydroSettings` Settings form for a BitMain hydro-cooled container; flow, temperature, and pump configuration. Main settings view for BitMain Hydro containers displaying: - Basic settings (cooling system, power, positioning) - Threshold configuration forms (water temperature, pressure) `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain/bitmain-hydro-settings.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain/bitmain-hydro-settings.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Optional | `Device` | - | Device data | ### `BitMainImmersionControlBox` Control box for a BitMain immersion container exposing tank, pump, and unit-level actions. A flexible container component with configurable columns and optional bottom content. Used for displaying control information in a structured layout `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain-immersion/control-box/bitmain-immersion-control-box.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain-immersion/control-box/bitmain-immersion-control-box.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `bottomContent` | Optional | `React.ReactNode` | - | Content for bottom row | | `className` | Optional | `string` | - | Custom className | | `leftContent` | Optional | `React.ReactNode` | - | Content for left column | | `rightContent` | Optional | `React.ReactNode` | - | Content for right column | | `secondary` | Optional | `boolean` | `false` | Secondary variant (no border) | | `title` | Optional | `string` | - | Box title | ### `BitMainImmersionPumpStationControlBox` Pump-station control card for a BitMain immersion container with per-pump enable/disable. Displays pump station status including: - Alarm/Normal status - Ready state - Operating state - Started state `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain-immersion/pump-station/bitmain-immersion-pump-station-control-box.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain-immersion/pump-station/bitmain-immersion-pump-station-control-box.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `alarmStatus` | Optional | `boolean` | `false` | Alarm/fault status | | `className` | Optional | `string` | - | Custom className | | `operation` | Optional | `boolean` | - | Operation status | | `ready` | Optional | `boolean` | - | Ready status | | `start` | Optional | `boolean` | - | Start status | | `title` | Optional | `string` | - | Box title | ### `BitMainImmersionSettings` Settings form for a BitMain immersion container — tank thresholds, pump curves, and limits. Settings form for BitMain immersion containers with temperature threshold configuration `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain-immersion/bitmain-immersion-settings.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain-immersion/bitmain-immersion-settings.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `containerSettings` | Optional | `{ thresholds?: Record \| undefined; } \| null` | `null` | Container settings with custom thresholds | | `data` | Optional | `Device` | - | Device data | ### `BitMainImmersionSummaryBox` Summary card for a BitMain immersion-cooled container: temps, pumps, power, and overall status `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/container/bitmain-immersion-summary-box/bitmain-immersion-summary-box.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/container/bitmain-immersion-summary-box/bitmain-immersion-summary-box.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `containerSettings` | Optional | `BitMainImmersionSummaryBoxContainerSettings \| null` | `null` | Optional threshold configuration that drives colour/flash states on temperature stats | | `data` | Optional | `Device` | - | Live device object from the devices store. Returns `null` when omitted | ### `BitMainImmersionSystemStatus` Aggregated system-status card for a BitMain immersion container; rolls up subsystem health. Displays system status information including: - Server start permission - Connection status `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain-immersion/system-status/bitmain-immersion-system-status.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain-immersion/system-status/bitmain-immersion-system-status.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Optional | `Device` | - | Device data | ### `BitMainImmersionUnitControlBox` Per-unit control card inside a BitMain immersion container with start/stop and reset actions `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain-immersion/unit-control-box/bitmain-immersion-unit-control-box.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain-immersion/unit-control-box/bitmain-immersion-unit-control-box.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `alarmStatus` | Optional | `boolean` | `false` | Alarm/fault status | | `className` | Optional | `string` | - | Custom className | | `frequency` | Optional | `number` | - | Frequency value in Hz | | `isDryCooler` | Optional | `boolean` | `false` | Whether this is a dry cooler unit | | `running` | Optional | `boolean` | `false` | Whether the unit is running | | `secondary` | Optional | `boolean` | `false` | Secondary variant (no border) | | `showFrequencyInLeftColumn` | Optional | `boolean` | `false` | Show frequency in left column instead of right | | `title` | Optional | `string` | - | Box title | ### `BitMainLiquidPressureCharts` Time-series charts of dielectric liquid pressure across a BitMain immersion container. Displays supply and return liquid pressure readings for Bitmain containers. Converts pressure values from MPa to bar for display. Features: - Supply Liquid Pressure - Sky Blue line - Return Liquid Pressure - Violet line - Automatic MPa to bar conversion - Interactive legend for toggling series - Timeline selector (1h, 6h, 24h, 7d, 30d) - Current pressure display `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain/charts/bitmain-liquid-pressure-charts.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain/charts/bitmain-liquid-pressure-charts.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `chartDataPayload` | Optional | `ChartDataPayload` | - | Declarative chart configuration | | `chartTitle` | Optional | `string` | - | Title shown in the chart header | | `data` | Optional | `UnknownRecord[]` | `[]` | Raw container telemetry entries (with `ts` + nested stats group) | | `dateRange` | Optional | `{ start?: number \| undefined; end?: number \| undefined; }` | - | Custom date range as Unix epoch seconds. Currently unused — accepted by the shared props type but not read by the chart components | | `fixedTimezone` | Optional | `string` | - | IANA timezone for x-axis ticks | | `footer` | Optional | `React.ReactNode` | - | Footer (e.g. min/max/avg stats) | | `height` | Optional | `number` | - | Chart pixel height | | `rangeOptions` | Optional | `{ label: string; value: string; }[]` | `5m/30m/3h/1D` | Override the default range selector options | | `showLegend` | Optional | `boolean` | `true` | Show the toggleable legend | | `showRangeSelector` | Optional | `boolean` | `true` | Show the range selector buttons | | `tag` | Optional | `string` | - | Container tag (any leading prefix is stripped) | | `timeline` | Optional | `string` | `"24h"` | Initial / controlled timeline value | ### `BitMainLiquidTempCharts` Time-series charts of dielectric liquid temperature across a BitMain immersion container. Displays supply and return liquid temperature readings for Bitmain containers. Features: - Supply Liquid Temp - Sky Blue line - Return Liquid Temp - Violet line - Interactive legend for toggling series - Timeline selector (1h, 6h, 24h, 7d, 30d) - Current temperature display `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain/charts/bitmain-liquid-temp-charts.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain/charts/bitmain-liquid-temp-charts.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `chartDataPayload` | Optional | `ChartDataPayload` | - | Declarative chart configuration | | `chartTitle` | Optional | `string` | - | Title shown in the chart header | | `data` | Optional | `UnknownRecord[]` | `[]` | Raw container telemetry entries (with `ts` + nested stats group) | | `dateRange` | Optional | `{ start?: number \| undefined; end?: number \| undefined; }` | - | Custom date range as Unix epoch seconds. Currently unused — accepted by the shared props type but not read by the chart components | | `fixedTimezone` | Optional | `string` | - | IANA timezone for x-axis ticks | | `footer` | Optional | `React.ReactNode` | - | Footer (e.g. min/max/avg stats) | | `height` | Optional | `number` | - | Chart pixel height | | `rangeOptions` | Optional | `{ label: string; value: string; }[]` | `5m/30m/3h/1D` | Override the default range selector options | | `showLegend` | Optional | `boolean` | `true` | Show the toggleable legend | | `showRangeSelector` | Optional | `boolean` | `true` | Show the range selector buttons | | `tag` | Optional | `string` | - | Container tag (any leading prefix is stripped) | | `timeline` | Optional | `string` | `"24h"` | Initial / controlled timeline value | ### `BitMainPowerAndPositioning` Power and physical-positioning settings for a BitMain container — circuits, phases, and rack slots. Displays power distribution and GPS location information: - Distribution box #1 and #2 power consumption - Latitude and longitude coordinates with direction `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain/status-item/settings/power-and-positioning/bitmain-power-and-positioning.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain/status-item/settings/power-and-positioning/bitmain-power-and-positioning.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Optional | `Device` | - | Container data | ### `BitMainPowerCharts` Time-series charts of per-phase power, voltage, and current draw for a BitMain container. Displays power consumption for Bitmain containers with distribution boxes. Shows total power and individual power for each distribution box. Automatically calculates total power from the sum of both boxes and formats the display units (kW/MW) based on the power magnitude. Features: - Total Power - Sky Blue line (sum of both boxes) - Dist. Box 1 Power - Violet line - Dist. Box 2 Power - Red line - Interactive legend for toggling series - Timeline selector (1h, 6h, 24h, 7d, 30d) - Current power consumption display with auto unit (W/kW/MW) - Automatic kW to W conversion for chart display `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain/charts/bitmain-power-charts.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain/charts/bitmain-power-charts.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `chartDataPayload` | Optional | `ChartDataPayload` | - | Declarative chart configuration | | `chartTitle` | Optional | `string` | - | Title shown in the chart header | | `data` | Optional | `UnknownRecord[]` | `[]` | Raw container telemetry entries (with `ts` + nested stats group) | | `dateRange` | Optional | `{ start?: number \| undefined; end?: number \| undefined; }` | - | Custom date range as Unix epoch seconds. Currently unused — accepted by the shared props type but not read by the chart components | | `fixedTimezone` | Optional | `string` | - | IANA timezone for x-axis ticks | | `footer` | Optional | `React.ReactNode` | - | Footer (e.g. min/max/avg stats) | | `height` | Optional | `number` | - | Chart pixel height | | `rangeOptions` | Optional | `{ label: string; value: string; }[]` | `5m/30m/3h/1D` | Override the default range selector options | | `showLegend` | Optional | `boolean` | `true` | Show the toggleable legend | | `showRangeSelector` | Optional | `boolean` | `true` | Show the range selector buttons | | `tag` | Optional | `string` | - | Container tag (any leading prefix is stripped) | | `timeline` | Optional | `string` | `"24h"` | Initial / controlled timeline value | ### `BitMainSupplyLiquidFlowCharts` Time-series charts of supply-side coolant flow rates for a BitMain immersion container. Displays supply liquid flow rate for Bitmain containers in cubic meters per hour (m³/h). Features: - Supply Liquid Flow - Sky Blue line - Interactive timeline selector (1h, 6h, 24h, 7d, 30d) - Current flow rate display - Optional legend and range selector `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain/charts/bitmain-supply-liquid-flow-charts.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain/charts/bitmain-supply-liquid-flow-charts.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `chartDataPayload` | Optional | `ChartDataPayload` | - | Declarative chart configuration | | `chartTitle` | Optional | `string` | - | Title shown in the chart header | | `data` | Optional | `UnknownRecord[]` | `[]` | Raw container telemetry entries (with `ts` + nested stats group) | | `dateRange` | Optional | `{ start?: number \| undefined; end?: number \| undefined; }` | - | Custom date range as Unix epoch seconds. Currently unused — accepted by the shared props type but not read by the chart components | | `fixedTimezone` | Optional | `string` | - | IANA timezone for x-axis ticks | | `footer` | Optional | `React.ReactNode` | - | Footer (e.g. min/max/avg stats) | | `height` | Optional | `number` | - | Chart pixel height | | `rangeOptions` | Optional | `{ label: string; value: string; }[]` | `5m/30m/3h/1D` | Override the default range selector options | | `showLegend` | Optional | `boolean` | `true` | Show the toggleable legend | | `showRangeSelector` | Optional | `boolean` | `true` | Show the range selector buttons | | `tag` | Optional | `string` | - | Container tag (any leading prefix is stripped) | | `timeline` | Optional | `string` | `"24h"` | Initial / controlled timeline value | ### `ContainerCharts` Tabbed chart panel showing per-container hashrate, power, and temperature time series `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/container/container-charts/container-charts.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/container/container-charts/container-charts.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `combinations` | Required | `ContainerChartCombinationOption[]` | - | Options for the combination selector | | `chartRawData` | Optional | `ChartEntry[] \| null` | `null` | Raw overview stats rows passed to chart adapters | | `defaultSelectedCombination` | Optional | `string \| null` | `null` | Initial selection when uncontrolled | | `disabledMessage` | Optional | `string` | `"Container Charts feature is not enabled"` | Message when `featureEnabled` is false | | `featureEnabled` | Optional | `boolean` | `true` | When false, shows an empty state (feature gate) | | `getDatasetBorderColor` | Optional | `ContainerChartsDatasetBorderColorResolver` | - | Optional per-dataset line colors after adapters run (e.g. demo or host branding) | | `isLoadingCharts` | Optional | `boolean` | `false` | Loading state for the chart panels | | `isLoadingCombinations` | Optional | `boolean` | `false` | Loading state for combination options | | `onSelectedCombinationChange` | Optional | `((value: string \| null) => void)` | - | Called when the selected combination changes | | `selectedCombination` | Optional | `string \| null` | - | Controlled selected combination value | | `title` | Optional | `string` | `"Container Charts"` | Section heading | ### `ContainerControlsBox` Control panel for a single container — start/stop, mode select, and operator actions `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/container/container-controls-box/container-controls-box.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/container/container-controls-box/container-controls-box.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `onNavigate` | Required | `(path: string) => void` | - | Navigation callback used by alarm row click-throughs | | `alarmsDataItems` | Optional | `TimelineItemData[]` | `[]` | Active alarm feed items to display inline | | `data` | Optional | `Device` | - | The container device object | | `isBatch` | Optional | `boolean` | `false` | When `true`, operates on `selectedDevices` instead of a single `data` record | | `isCompact` | Optional | `boolean` | - | - | | `pendingSubmissions` | Optional | `PendingSubmission[]` | `[]` | In-flight command queue; disables conflicting actions | | `powerModesLog` | Optional | `UnknownRecord` | - | - | | `selectedDevices` | Optional | `Device[]` | `[]` | Devices included in a batch operation | | `tailLogData` | Optional | `UnknownRecord[]` | - | Recent log tail entries | ### `ContainerControlsCard` Per-socket controls card for the container detail view: powers the selected miner sockets on/off and shows their aggregate power / current. Reads the selected sockets from the devices store (populated by `deriveSelectedSockets`) and queues `switchSocket` drafts. Container-level "power all sockets" lives on [`BatchContainerControlsCard`](/reference/ui/components/widgets/#batchcontainercontrolscard); this card drives an explicit socket selection, which the flat Explorer list does not yet surface (a container-PDU grid is the follow-on) — hence `advanced` rather than `agent-ready` `advanced`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/container-controls-card/container-controls-card.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/container-controls-card/container-controls-card.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `buttonsStates` | Optional | `ButtonsStates` | - | - | | `isLoading` | Optional | `boolean` | - | - | ### `ContainerFanLegend` Legend strip describing fan states and colours used by the container fans visualisation. Displays a single fan status with number and icon `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitdeer/settings/container-options/dry-cooler/container-fans-card/container-fans-legend.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitdeer/settings/container-options/dry-cooler/container-fans-card/container-fans-legend.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `className` | Optional | `string` | - | Custom className | | `enabled` | Optional | `boolean` | `false` | Running state; controls the icon and colour class | | `index` | Optional | `number \| null` | - | Fan index/number to display | ### `ContainerFansCard` Card displaying the array of cooling fans in a container with live state per fan. Displays a grid of fan status indicators. Shows fan number and on/off state with icon `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitdeer/settings/container-options/dry-cooler/container-fans-card/container-fans-card.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitdeer/settings/container-options/dry-cooler/container-fans-card/container-fans-card.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `fansData` | Optional | `PumpItem[]` | - | Array of fan state objects. Renders an empty card when the array is empty; returns `null` when absent | ### `ContainerSelectionDialog` Modal that lists containers and lets the operator pick one or many for a follow-up action `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/dialogs/position-change-dialog/container-selection-dialog/container-selection-dialog.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/dialogs/position-change-dialog/container-selection-dialog/container-selection-dialog.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `onClose` | Required | `(value?: boolean \| undefined) => void` | - | Called when dialog closes | | `open` | Required | `boolean` | - | Controls visibility | | `containers` | Optional | `Device[]` | `[]` | Available target containers | | `isLoading` | Optional | `boolean` | - | Show loading spinner | | `miner` | Optional | `Device` | - | The miner being moved | ### `ContainerWidgetCard` Presentational summary card for a single container in the Site Overview widgets grid: a header row (title / alarms / power), then either an offline / error banner or the body (optional vendor content, a miners summary, and a miner-activity chart). Fully props-driven — no data fetching, formatting, or alarm math. The owning feature/hook shapes every value (including `flash` and `summary`) `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/container/container-widget-card/container-widget-card.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/container/container-widget-card/container-widget-card.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `summary` | Required | `MinersSummaryParam[]` | - | Pre-formatted miners-summary rows (label + display value incl. units) | | `title` | Required | `string` | - | Container display name shown in the header row | | `activity` | Optional | `ContainerActivityData` | `{}` | Miner-state activity counts for the embedded chart | | `activityError` | Optional | `ContainerActivityError` | `null` | Activity chart error payload | | `alarms` | Optional | `Partial>` | - | Per-category alarm badges for the header row | | `className` | Optional | `string` | - | Additional class for the root element | | `errorMessage` | Optional | `string` | - | Container-level error message; renders an error banner instead of the body | | `flash` | Optional | `boolean` | `false` | Critical-high alarm flash. Computed upstream (by the data hook) so the card stays presentational — never derive alarm state inside this component | | `isActivityError` | Optional | `boolean` | `false` | Activity chart error state | | `isActivityLoading` | Optional | `boolean` | `false` | Activity chart loading state | | `isOffline` | Optional | `boolean` | `false` | Render the offline banner instead of the body | | `onClick` | Optional | `(() => void)` | - | Invoked when the card is clicked (navigation is the caller's concern) | | `power` | Optional | `number` | - | Latest container power draw in watts (rendered in kW by the top row) | | `powerUnit` | Optional | `string` | - | Power unit label shown next to the reading | | `statsErrorMessage` | Optional | `string \| ErrorWithTimestamp[] \| null` | - | Raw stats error surfaced as a tooltip in place of the power reading | | `vendorContent` | Optional | `React.ReactNode` | - | Optional vendor-specific content (supply-liquid / tanks / immersion / MicroBT boxes) rendered above the miners summary. Kept as a slot so the generic card carries no per-model branching | ### `CostMetrics` Three "$/MWh" tiles that summarise the cost-summary period. Order mirrors the OSS [**`Cost`**](/reference/ui/components/dashboards/#cost) page: All-in (highlighted), Energy, Operations `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/cost/cost-metrics.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/reporting-tool/financial/cost/cost-metrics.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `metrics` | Required | `CostSummaryDisplayMetrics` | - | - | ### `DryCooler` Dry-cooler subsystem panel showing inlet/outlet temperatures and fan-stage status. Displays dry cooler status with fans and associated pumps. Shows two cooler groups with fan status indicators and pump controls `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitdeer/settings/container-options/dry-cooler/dry-cooler.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitdeer/settings/container-options/dry-cooler/dry-cooler.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Optional | `UnknownRecord` | - | Container settings payload. `cooling_system.dry_cooler` is read for fan state; `cooling_system.oil_pump` and `cooling_system.water_pump` are read for pump state | ### `EnabledDisableToggle` [**`Switch`**](/reference/ui/components/forms/#switch) with confirmation that enables or disables a container, miner, or feature flag `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/container/enabled-disable-toggle/enabled-disable-toggle.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/container/enabled-disable-toggle/enabled-disable-toggle.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `isButtonDisabled` | Required | `boolean` | - | Disables the Enable/Disable buttons when a command is in-flight | | `isOffline` | Required | `boolean` | - | Disables all controls and shows an offline tooltip | | `onToggle` | Required | `(params: EnabledDisableToggleCbParams) => void` | - | Callback fired when the user confirms a state change | | `tankNumber` | Required | `string \| number` | - | Tank identifier used in the label (`Tank {N} Circulation`). Pass an empty string for the air exhaust label | | `value` | Required | `unknown` | - | Current state. A boolean drives a switch display; non-boolean shows action buttons | ### `ExplorerDetail` Per-type Explorer detail panel. Reads the selection the [`useExplorerSelection`](/reference/ui/hooks/op-center/#useexplorerselection) bridge writes into `devicesStore` and composes the matching cards: - **container:** [`BatchContainerControlsCard`](/reference/ui/components/widgets/#batchcontainercontrolscard) (batch when >1 selected) with the connected-miner power-mode controls and the active-alarms box, plus [`StatsGroupCard`](/reference/ui/components/widgets/#statsgroupcard) for the connected miners and [`ContainerControlsCard`](/reference/ui/components/widgets/#containercontrolscard) when an explicit per-socket selection exists. - **miner:** [`MinerControlsCard`](/reference/ui/components/widgets/#minercontrolscard) write controls, [`MinerInfoCard`](/reference/ui/components/widgets/#minerinfocard) read-only info rows, [`MinerChipsCard`](/reference/ui/components/widgets/#minerchipscard) per-chip stats, [`StatsGroupCard`](/reference/ui/components/widgets/#statsgroupcard) aggregate, and the active-alarms box. - **cabinet:** read-only [`CabinetDetailCard`](/reference/ui/components/cards/#cabinetdetailcard) — powermeter + temperature readings and the LV-cabinet warnings timeline. Writes queue into the actions draft store; submission stays gated behind the [`ActionsSidebar`](/reference/ui/components/dashboards/#actionssidebar) `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/features/explorer/explorer-detail/explorer-detail.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/features/explorer/explorer-detail/explorer-detail.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `deviceType` | Required | `"container" \| "miner" \| "cabinet"` | - | The active Explorer tab — selects which per-type panel renders | | `isCompact` | Optional | `boolean` | `true` | Compact layout for the narrower Explorer detail column | | `onNavigate` | Optional | `((path: string) => void)` | `no-op` | Router navigate used by alarm rows to deep-link into `/alerts/:id` | ### `FireStatusBox` Fire Status Box Component x Displays fire safety and environmental monitoring status `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/micro-bt/fire-status-box/fire-status-box.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/micro-bt/fire-status-box/fire-status-box.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Optional | `{ smokeDetector: string \| number; waterIngressDetector: string \| number; coolingFanStatus: string \| number; }` | - | Device data | ### `GaugeChartComponent` Single-needle gauge chart used inside container settings for thresholded percentage values. Displays a value as a gauge/speedometer chart with label and unit `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/micro-bt/gauge-chart/gauge-chart-component.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/micro-bt/gauge-chart/gauge-chart-component.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `max` | Required | `number` | - | Maximum value for the gauge | | `unit` | Required | `string` | - | Unit of measurement | | `value` | Required | `number` | - | Current value | | `chartStyle` | Optional | `React.CSSProperties` | `{}` | Custom chart style | | `className` | Optional | `string` | - | Custom className | | `colors` | Optional | `string[]` | `[COLOR.EMERALD, COLOR.SOFT_TEAL]` | Arc colors in HEX format | | `height` | Optional | `number` | `200` | Chart height in pixels | | `hideText` | Optional | `boolean` | `true` | Hide the percentage text inside the chart | | `label` | Optional | `string` | `""` | [**`Label`**](/reference/ui/components/forms/#label)/title for the chart | ### `GenericDataBox` Reusable labelled stat box used by container summary panels for one-off numeric values. Displays a table of label-value-unit rows `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/container/generic-data-box/generic-data-box.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/container/generic-data-box/generic-data-box.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Optional | `DataItem[]` | `[]` | Array of data items to display | | `fallbackValue` | Optional | `unknown` | - | Fallback value when value is undefined | ### `MaintenanceDialogContent` Body of the maintenance dialog — captures the work-order details before applying the maintenance flag `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/dialogs/position-change-dialog/maintenance-dialog-content/maintenance-dialog-content.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/dialogs/position-change-dialog/maintenance-dialog-content/maintenance-dialog-content.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `onCancel` | Optional | `VoidFunction` | - | Called when the user cancels | | `selectedEditSocket` | Optional | `Partial` | - | The socket/slot being flagged for maintenance | ### `MicroBTCooling` Cooling subsystem panel for a MicroBT container — pumps, fans, and coolant flow telemetry. Displays cooling system status for MicroBT containers including: - Cycle pump status - Main circulation pump (status, switch, speed) - Cooling fan (status, switch, speed) - Make-up water pump (status, switch, fault) `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/micro-bt/cooling/micro-bt-cooling.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/micro-bt/cooling/micro-bt-cooling.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Optional | `Device` | - | Device data | ### `MicroBTSettings` Settings form for a MicroBT container with vendor-specific operating limits. Settings form for MicroBT containers with temperature threshold configuration `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/micro-bt/settings/micro-bt-settings.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/micro-bt/settings/micro-bt-settings.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `containerSettings` | Optional | `{ thresholds?: Record \| undefined; } \| null` | `null` | Container settings with custom thresholds | | `data` | Optional | `Device` | - | Device data | ### `MicroBTWidgetBox` Summary card for a MicroBT-equipped container showing pumps, fans, and operating mode `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/container/micro-bt-widget-box/micro-bt-widget-box.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/container/micro-bt-widget-box/micro-bt-widget-box.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Optional | `Device` | - | Live device object. Returns `null` when omitted | ### `MinerChip` Chip representing a single miner; surfaces slot index, temperature, and frequency `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/miner-chips-card/miner-chip/miner-chip.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/miner-chips-card/miner-chip/miner-chip.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `frequency` | Required | `{ current: number; }` | - | Current frequency in MHz | | `index` | Required | `number` | - | Chip slot index | | `temperature` | Required | `{ avg: number; min: number; max: number; }` | - | Temperature readings in °C | ### `MinerChipsCard` Card listing every miner in a container as [``](/reference/ui/components/widgets/#minerchip)s for at-a-glance selection `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/miner-chips-card/miner-chips-card.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/miner-chips-card/miner-chips-card.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Required | `ContainerStats` | - | Container stats including chip frequency and temperature arrays | ### `MinerControlsCard` Action card for a single miner: power, reboot, mode select, and maintenance entry points `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/miner-controls-card/miner-controls-card.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/miner-controls-card/miner-controls-card.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `buttonsStates` | Required | `Record` | - | Map of action name → loading/disabled state | | `isLoading` | Required | `boolean` | - | Whether the card itself is in a loading state | | `showPowerModeSelector` | Optional | `boolean` | `true` | Show the power-mode selection button | ### `MinerInfoCard` Info card for one miner — serial, model, firmware, location, and recent activity summary `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/miner-info-card/miner-info-card.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/miner-info-card/miner-info-card.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Optional | `InfoItem[]` | - | Array of label/value pairs to display | | `label` | Optional | `string` | `"Miner info"` | Card heading label | ### `MinerMetricCard` Single-metric card (hashrate, temperature, or power) for one miner with sparkline and delta. Displays key metrics for a miner including: - Efficiency (fixed position top-right) - Hash rate, Temperature - Frequency, Consumption - Optional secondary stats in grid `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/miner-metric-card/miner-metric-card.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/miner-metric-card/miner-metric-card.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `primaryStats` | Optional | `StatItem[]` | - | Primary statistics (efficiency, hashrate, temperature, frequency, consumption) | | `secondaryStats` | Optional | `StatItem[]` | - | Secondary statistics to display in grid | | `showSecondaryStats` | Optional | `boolean` | `true` | Whether to show secondary stats section | ### `MinerPowerModeSelectionButtons` Segmented control letting an operator switch a miner between low / normal / turbo power modes `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/miner-power-mode-selection-buttons/miner-power-mode-selection-buttons.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/miner-power-mode-selection-buttons/miner-power-mode-selection-buttons.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `connectedMiners` | Optional | `Device[]` | - | Currently connected miners | | `disabled` | Optional | `boolean` | `false` | Disable all buttons | | `hasMargin` | Optional | `boolean` | `false` | Add margin around the button group | | `powerModesLog` | Optional | `UnknownRecord` | - | Log of previous power mode selections | | `selectedDevices` | Optional | `Device[]` | `[]` | Devices to apply the power mode to | | `setPowerMode` | Optional | `((devices: Device[], mode: string) => void)` | - | Callback to apply the selected mode | ### `MinersActivityChart` Per-status miner counts (online / offline / faulted / power-mode). Renders as coloured indicator dots (`indicators`) or tinted status tiles (`tiles`) `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/miners-activity-chart/miners-activity-chart.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/miners-activity-chart/miners-activity-chart.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Optional | `MinersActivityData` | `{}` | Time-series data for online/offline/faulted counts | | `error` | Optional | `MinerActivityChartErrorProp \| null` | `null` | Error details to display | | `isDemoMode` | Optional | `boolean` | `false` | Use demo/mock data | | `isError` | Optional | `boolean` | `false` | Show error state | | `isLoading` | Optional | `boolean` | `false` | Show loading state | | `large` | Optional | `boolean` | `false` | Use tall variant | | `showLabel` | Optional | `boolean` | `true` | Show axis labels | | `variant` | Optional | `"indicators" \| "tiles"` | `"indicators"` | `indicators` (default) renders coloured dots; `tiles` renders tinted status tiles | ### `MinersSummaryBox` Headline card summarising miner counts by state (online, offline, faulted) for one container. Displays mining summary parameters in a 2-column grid layout. Accepts pre-formatted values - consumers handle data formatting `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/container/miners-summary-box/miners-summary-box.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/container/miners-summary-box/miners-summary-box.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `params` | Required | `MinersSummaryParam[]` | - | Array of label-value pairs to display in a 2-column grid | | `className` | Optional | `string` | - | Additional CSS class name | ### `PositionChangeDialog` Modal that moves a miner to a different rack slot, validating the destination first `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/dialogs/position-change-dialog/position-change-dialog.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/dialogs/position-change-dialog/position-change-dialog.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `onClose` | Required | `(currentDialogFlow: string, isDontReset?: boolean \| undefined) => void` | - | Called when dialog closes | | `open` | Required | `boolean` | - | Controls dialog visibility | | `dialogFlow` | Optional | `string` | - | Initial dialog step/flow identifier | | `isContainerEmpty` | Optional | `boolean` | `false` | Whether the target container slot is empty | | `onChangePositionClicked` | Optional | `VoidFunction` | - | Callback when position change is triggered | | `onPositionChangedSuccess` | Optional | `VoidFunction` | - | Callback on successful position change | | `selectedEditSocket` | Optional | `UnknownRecord` | - | [**`Socket`**](/reference/ui/components/widgets/#socket) being edited | | `selectedSocketToReplace` | Optional | `UnknownRecord` | - | [**`Socket`**](/reference/ui/components/widgets/#socket) being replaced | ### `PowerMeters` Per-circuit power-meter panel showing live kW, kWh, and power-factor readings. Displays power meter readings for container devices including: - Communication status - Voltage measurements (A-B, B-C, C-A) - Power factor and frequency - Active and apparent power - Energy consumption `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/micro-bt/power-meters/power-meters.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/micro-bt/power-meters/power-meters.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Optional | `Device` | - | Device data | ### `PumpBox` Single-pump status card with RPM, flow, and fault state for one immersion-cooling pump `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitdeer/settings/container-options/pump-box/pump-box.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitdeer/settings/container-options/pump-box/pump-box.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `pumpTitle` | Required | `string` | - | [**`Label`**](/reference/ui/components/forms/#label) prefix for the pump (e.g. `"Circulation"`) | | `pumpItem` | Optional | `PumpItem` | - | Pump data. `index` is 0-based; displayed as `index + 1` | ### `RemoveMinerDialog` Confirmation modal for removing a miner from a slot. Renders nothing when `isRemoveMinerFlow` is `false` `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/dialogs/position-change-dialog/remove-miner-dialog/remove-miner-dialog.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/dialogs/position-change-dialog/remove-miner-dialog/remove-miner-dialog.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `isRemoveMinerFlow` | Required | `boolean` | - | Controls whether the dialog is open; `false` renders nothing | | `onCancel` | Required | `VoidFunction` | - | Called when the action is cancelled | | `headDevice` | Optional | `Device` | `{}` | The miner being removed | ### `SecondaryStatCard` Compact stat tile rendered alongside a primary stat to provide supporting context. Displays a secondary statistic with a name and value in a card format `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/secondary-stat-card/secondary-stat-card.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/secondary-stat-card/secondary-stat-card.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `className` | Optional | `string` | - | Custom className | | `name` | Optional | `string` | `""` | Stat name/label | | `value` | Optional | `string \| number` | `""` | Stat value | ### `SingleStatCard` Hero stat tile rendering one big number with a label and optional delta indicator. Displays a single statistic with optional animations and variants `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/single-stat-card/single-stat-card.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/single-stat-card/single-stat-card.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `color` | Optional | `string` | `"inherit"` | Color for flash/border | | `flash` | Optional | `boolean` | `false` | Enable flash animation | | `name` | Optional | `string` | - | Stat name/label | | `row` | Optional | `boolean` | `false` | Row layout | | `subtitle` | Optional | `string` | `""` | Subtitle text | | `superflash` | Optional | `boolean` | `false` | Enable superflash animation (faster) | | `unit` | Optional | `string` | `""` | Unit of measurement | | `value` | Optional | `string \| number \| null` | `null` | Stat value | | `variant` | Optional | `"primary" \| "secondary" \| "tertiary" \| "highlighted"` | `"primary"` | Card variant | ### `Socket` Per-socket panel showing the miner slotted into a container slot, its state, and quick actions. Displays a socket in a PDU with power/current info, miner status, and heatmap visualization Features: - Power and current display - Miner status indicators - [**`Heatmap`**](/reference/ui/components/charts/#heatmap) mode with temperature/hashrate - Cooling fan indicator - [**`Socket`**](/reference/ui/components/widgets/#socket) enable/disable states - Add miner flow `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/container/socket/socket.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/container/socket/socket.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `clickDisabled` | Optional | `boolean` | `false` | Whether click is disabled | | `cooling` | Optional | `boolean` | `undefined` | Cooling status | | `current_a` | Optional | `number \| null` | `null` | Current in amperes | | `enabled` | Optional | `boolean` | `false` | Whether socket is enabled | | `heatmap` | Optional | `Heatmap \| null` | `null` | [**`Heatmap`**](/reference/ui/components/charts/#heatmap) configuration | | `innerRef` | Optional | `React.ForwardedRef` | - | Forwarded ref for the container | | `isContainerControlSupported` | Optional | `boolean` | `false` | Whether container control is supported | | `isEditFlow` | Optional | `boolean` | `false` | Whether in edit flow mode | | `isEmptyPowerDashed` | Optional | `boolean` | `false` | Whether to show dashed border for empty power | | `miner` | Optional | `Miner \| null` | `null` | Miner data | | `pdu` | Optional | `Pdu` | - | PDU information | | `power_w` | Optional | `number \| null` | `null` | Power in watts | | `selected` | Optional | `boolean` | `false` | Whether socket is selected | | `socket` | Optional | `number \| null` | `null` | [**`Socket`**](/reference/ui/components/widgets/#socket) number/index | ### `StatsGroupCard` Card grouping multiple [``](/reference/ui/components/widgets/#singlestatcard)s or [``](/reference/ui/components/widgets/#secondarystatcard)s under a shared title. Displays aggregated statistics for selected miners: - Hash rate, Temperature, Frequency, Consumption - Optional: Power mode, Uptime, LED, Status (for single miner) Can render in two modes: - Miner Metrics: Uses [**`MinerMetricCard`**](/reference/ui/components/widgets/#minermetriccard) layout - Stats Grid: Uses [**`SingleStatCard`**](/reference/ui/components/widgets/#singlestatcard) and [**`SecondaryStatCard`**](/reference/ui/components/widgets/#secondarystatcard) in rows `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/stats-group-card/stats-group-card.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/details-view/stats-group-card/stats-group-card.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `isMinerMetrics` | Optional | `boolean` | `false` | Whether to show miner metrics card layout | | `miners` | Optional | `Device[] \| DeviceData[]` | - | Array of miners to display stats for | ### `StatusItem` Compact labelled status pill used inside container panels for boolean or enum readings. Displays a label with a colored status indicator `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain/status-item/status-item.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/explorer/containers/bitmain/status-item/status-item.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `label` | Optional | `string` | - | Status label text | | `status` | Optional | `"warning" \| "normal" \| "fault" \| "unavailable"` | - | Status type | ### `SupplyLiquidBox` Status card for the dielectric supply tank — level, temperature, and pressure readings `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/container/supply-liquid-box/supply-liquid-box.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/container/supply-liquid-box/supply-liquid-box.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `containerSettings` | Optional | `SupplyLiquidBoxContainerSettings \| null` | `null` | Optional threshold map that controls colour and flash states on readings | | `data` | Optional | `Device` | - | Live device object. Returns `null` when omitted | ### `TankRow` Single tank-list row used inside [``](/reference/ui/components/widgets/#tanksbox) to display per-tank temperature and pressure `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/container/tanks-box/tank-row.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/container/tanks-box/tank-row.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `color` | Required | `string` | - | CSS colour for the temperature value (threshold-driven) | | `label` | Required | `string` | - | Tank identifier label (e.g. "Tank 1") | | `oilPumpEnabled` | Required | `boolean` | - | Running state for the oil pump | | `pressure` | Required | `Partial<{ value: number; flash: boolean; color: string; tooltip: string; }>` | - | Pressure reading with optional flash/colour/tooltip | | `temperature` | Required | `number` | - | Current temperature value | | `unit` | Required | `string` | - | Temperature unit string (e.g. "°C") | | `waterPumpEnabled` | Required | `boolean` | - | Running state for the water pump | | `flash` | Optional | `boolean` | - | Enables flash animation on the temperature row | | `tooltip` | Optional | `string` | - | [**`Tooltip`**](/reference/ui/components/overlays/#tooltip) text for the temperature value | ### `TanksBox` Card listing all tanks inside an immersion container with per-tank temperature and pressure rows. Displays tank rows with temperature, pressure, and oil/water pump status `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/container/tanks-box/tanks-box.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/container/tanks-box/tanks-box.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `data` | Optional | `{ oil_pump: Tank[]; water_pump: WaterPump[]; pressure: TanksBoxPressure[]; }` | - | Tank telemetry arrays; returns `null` when omitted | ### `WidgetTopRow` Compact header row used at the top of container / miner widgets — shows the title, per-category alarm badges, and the current power reading (or an error tooltip) `agent-ready`
Source [https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/widget-top-row/index.tsx](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/domain/components/widget-top-row/index.tsx)
#### Props | Prop | Status | Type / Options | Default | Description | |------|--------|----------------|---------|-------------| | `title` | Required | `string` | - | Widget title | | `alarms` | Optional | `Partial>` | - | Per-category alarm badges | | `className` | Optional | `string` | - | Additional class names | | `power` | Optional | `number` | - | Power reading; rendered in kilo-units | | `statsErrorMessage` | Optional | `string \| ErrorWithTimestamp[] \| null` | - | Error tooltip content; replaces power | | `unit` | Optional | `string` | - | Power unit (e.g. `"kW"`) | # Hooks (/reference/ui/hooks) React hooks from `@tetherto/mdk-react-devkit` and `@tetherto/mdk-react-adapter` for building mining application UIs. ## 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. ## Browse by category ### Data & state hooks | Category | Description | Package | |----------|-------------|---------| | [Auth](/reference/ui/hooks/auth) | Authentication state hooks | react-adapter | | [Dashboard](/reference/ui/hooks/dashboard) | Dashboard data and state hooks | react-adapter, react-devkit | | [Permission](/reference/ui/hooks/permission) | Permission and access control hooks | react-adapter | | [Store](/reference/ui/hooks/store) | Zustand store access hooks | react-adapter | ### UI component hooks | Category | Description | Package | |----------|-------------|---------| | [Cards](/reference/ui/hooks/cards) | Card component state hooks | react-devkit | | [Charts](/reference/ui/hooks/charts) | Chart component hooks | react-devkit | | [Device](/reference/ui/hooks/device) | Device and equipment management hooks | react-adapter | | [Feedback](/reference/ui/hooks/feedback) | Toast and notification hooks | react-devkit | | [Filters](/reference/ui/hooks/filters) | Filter state hooks | react-devkit | | [Forms](/reference/ui/hooks/forms) | Form state and validation hooks | react-devkit | | [Navigation](/reference/ui/hooks/navigation) | Navigation state hooks | react-devkit | | [Settings](/reference/ui/hooks/settings) | Settings and preferences hooks | react-devkit | | [Tables](/reference/ui/hooks/tables) | Table state and interaction hooks | react-devkit | | [Widgets](/reference/ui/hooks/widgets) | Widget state hooks | react-devkit | ### Feature hooks | Category | Description | Package | |----------|-------------|---------| | [Alerts](/reference/ui/hooks/alerts) | Alert management hooks | react-adapter | | [Op Center](/reference/ui/hooks/op-center) | Operations center hooks | react-adapter, react-devkit | ### Utility hooks | Category | Description | Package | |----------|-------------|---------| | [Example](/reference/ui/hooks/example) | Example and demo hooks | react-adapter | | [Misc](/reference/ui/hooks/misc) | Miscellaneous hooks | react-devkit | | [Utility](/reference/ui/hooks/utility) | General utility hooks | react-adapter, react-devkit | ## Import pattern ```tsx // From react-adapter (state/data hooks) // From react-devkit (UI component hooks) ``` # Alert (/reference/ui/hooks/alerts) Hooks for managing alerts and notifications. ## Package `@tetherto/mdk-react-adapter` ## Hooks @tetherto/mdk-react-adapter Import the public APIs on this page from [`@tetherto/mdk-react-adapter`](/reference/ui/#tethertomdk-react-adapter). ### `useCurrentAlertDevices` TanStack Query hook returning a flat list of the devices that currently carry one or more alerts, ready to hand straight to the devkit `<Alerts>` / `<CurrentAlerts>` table. ```typescript (options: UseCurrentAlertDevicesOptions = {}) => UseQueryResult ``` ### `useHistoricalAlerts` TanStack Query hook for the historical-alerts log. Fetches the `[start, end]` range as successive 24-hour `history-log` windows (see `fetchHistoricalAlertsInChunks`), merges them by `uuid`, and shapes the rows for the devkit `<HistoricalAl… ```typescript ({ start, end, intervalMs, enabled, }: UseHistoricalAlertsOptions) => UseQueryResult ``` # Auth (/reference/ui/hooks/auth) Hooks for authentication flows and session management. ## Package `@tetherto/mdk-react-adapter` ## Hooks @tetherto/mdk-react-adapter Import the public APIs on this page from [`@tetherto/mdk-react-adapter`](/reference/ui/#tethertomdk-react-adapter). ### `useAuthToken` Adopts whatever session the environment is offering, then returns the current token so callers can react to it (e.g. redirect once signed in). ```typescript () => string | null ``` ### `useCurrentUserEmail` Fetches `/auth/userinfo` and returns the current user's email. Used by `useLiveActions` to partition actions into "mine vs others". ```typescript () => string | undefined ``` ### `useTokenPolling` Keeps the session alive by calling the active `AuthProvider`'s `refresh` on an interval. When a refresh reports the session ended, the provider signs out and `onSessionEnded` fires so the host app can redirect to its sign-in page. ```typescript (options: UseTokenPollingOptions = {}) => void ``` # Card (/reference/ui/hooks/cards) Hooks for card component state management. ## Package `@tetherto/mdk-react-devkit` ## Hooks @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `useCabinetDetail` Reads the selected LV cabinet from `devicesStore`, re-fetches its family of powermeters and temperature sensors by root (`useCabinetDevices`) at the realtime cadence, and shapes them into the read-only rows the [`CabinetDetailCard`](/reference/ui/components/cards/#cabinetdetailcard) renders — powermeter readings (→ kW), the root plus per-position temperature readings (severity-coloured), and the active-warnings timeline folded from every device's `last.alerts` ```typescript (onNavigate?: (path: string) => void) => UseCabinetDetailResult ``` ### `useDeviceAlarms` Shapes the active alarms of the selected devices into timeline items for the Explorer detail panel, mirroring the reference app's `getContainerFormatedAlerts` → `getAlertTimelineItems` chain: `getAlarms` reads each device's `last.alerts`, `getLogFormattedAlertData` formats each with the device's `id`/`info`/`type` and the timezone-aware date formatter, and `getAlertTimelineItems` wires the log/dot rows plus the `onNavigate` click-through to the alert detail route ```typescript (devices?: Device[], onNavigate?: (path: string) => void) => UseDeviceAlarmsResult ``` ### `useExplorerThingDetail` Explorer detail hook: fetches one thing by id (`useThingDetail`) and shapes it into display-ready rows for the Explorer detail panel. Reads the same snapshot fields the container table columns show (status, ambient temp, humidity, power → kW) so the panel and the row stay consistent. Returns a `hasSelection: false` result when no id is given ```typescript (id: string | undefined, options?: UseThingDetailOptions) => UseExplorerThingDetailResult ``` # Chart (/reference/ui/hooks/charts) Hooks for chart components and data visualization. ## Package `@tetherto/mdk-react-devkit` ## Hooks @tetherto/mdk-react-devkit ### Check chart data ```tsx ``` #### Related API - [**`ChartContainer`**](/reference/ui/components/charts/#chartcontainer) ### When to use `useChartDataCheck` Use `useChartDataCheck` to drive a chart's empty state after its data has been shaped for the chart component. It returns `true` when the supplied chart data contains no renderable values. ### `useChartDataCheck` workflow Pass either `dataset` for direct bar-style datasets or `data` for a Chart.js-shaped object containing `datasets` (or `dataset`). Use the returned boolean as the `empty` state for `ChartContainer` or to choose an explicit empty placeholder. ### `useChartDataCheck` data-shape caveat The `data` option expects Chart.js-shaped data such as `{ labels, datasets }`. Raw hook output such as `{ labels, series }` can be non-empty while still being unrecognisable to the checker. Convert that output with [`buildBarChartData`](/reference/ui/components/charts) before calling the hook. Supply at least one of `dataset` or `data`; omitting both is treated as empty. ### `useChartDataCheck` example ```tsx BarChart, buildBarChartData, ChartContainer, useChartDataCheck, } from '@tetherto/mdk-react-devkit' function RevenueBarChart({ hookOutput }) { const chartData = buildBarChartData(hookOutput) const isEmpty = useChartDataCheck({ data: chartData }) return ( ) } ``` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `useChartDataCheck` Hook to check if chart data is empty or unavailable Returns `true` if data is empty/unavailable (should show empty state) Returns `false` if data exists (should show chart) ```typescript ({ dataset, data }: UseChartDataCheckParams) => boolean ``` ### `useEbitda` Transforms an `EbitdaResponse` and date-range options into query params and a chart-ready EBITDA view-model ```typescript ({ ebitda, isLoading, fetchErrors, ...dateRangeOptions }?: UseEbitdaOptions) => { metrics: EbitdaDisplayMetrics | null; ebitdaChartInput: ToBarChartDataInput | null; btcProducedChartInput: ToBarChartDataInput | null; hasBtcProducedAllZeros:… /* see source */ ``` ### `useEnergyBalanceViewModel` Computes the full [**`EnergyBalance`**](/reference/ui/components/charts/#energybalance) view model from raw API data, managing tab selection and display-mode state ```typescript ({ data, isLoading, fetchErrors, dateRange, availablePowerMW, }: UseEnergyBalanceOptions) => { queryParams: EnergyBalanceQueryParams | null; viewModel: { activeTab: EnergyBalanceTab; revenueDisplayMode: DisplayMode; costDisplayMode: Display… /* see source */ ``` # Dashboard (/reference/ui/hooks/dashboard) Hooks for dashboard data and state management. ## From `@tetherto/mdk-react-adapter` @tetherto/mdk-react-adapter Import the public APIs on this page from [`@tetherto/mdk-react-adapter`](/reference/ui/#tethertomdk-react-adapter). ### `useActiveIncidents` TanStack Query hook returning the list of currently-firing alerts, shaped for `<ActiveIncidentsCard items={...} />`. ```typescript (options: UseActiveIncidentsOptions = {}) => UseQueryResult ``` ### `useCancelAction` Cancels pending actions via `DELETE /auth/actions/:type/cancel?ids=…`. Invalidates the pool/miner/actions caches on success. Gated by `actions:w`. ```typescript () => UseCancelActionResult ``` ### `useConsumptionChartData` TanStack Query hook returning raw consumption tail-log samples. Most dashboards should consume the higher-level `useSiteConsumptionChartData`, which wraps this hook with the site powermeter defaults and returns a `<LineChartCard>`-ready `C… ```typescript (params: UseConsumptionChartDataParams) => UseQueryResult ``` ### `useContainerPoolStats` Fetches per-container pool override counts from `GET /auth/pools/stats/containers`. Feeds the Sites Overview cards. ```typescript (options: UseContainerPoolStatsOptions = {}) => UseContainerPoolStatsResult ``` ### `useContainerUnits` Fetches the site's container things from `GET /auth/list-things` (tag `t-container`). Returns the raw rows the Sites Overview merge needs — `id`, `type`, `info.container`, `info.poolConfig`, and the container status under `last.snap.stats.… ```typescript (options: UseContainerUnitsOptions = {}) => UseContainerUnitsResult ``` ### `useDashboardDateRange` Owns the single source of truth for the dashboard's date-range picker. Pass `start` / `end` from the return value into every data hook on the page (`useHashrateChartData`, `useConsumptionChartData`, the export, etc.) so they refetch in loc… ```typescript (options: UseDashboardDateRangeOptions = {}) => UseDashboardDateRangeReturn ``` ### `useDashboardExport` Builds CSV / JSON downloads from the dashboard's already-cached TanStack Query data. Does NOT trigger refetches — the export is exactly what the user is looking at when they click the button. ```typescript (options: UseDashboardExportOptions) => UseDashboardExportReturn ``` ### `useDashboardTimeRange` Tiny piece of shared state for the dashboard's timeline selector. Owns the current `timeline` value plus the canonical option list. The chart hooks (`useHashrateChartData`, etc.) consume `timeline` as a prop. ```typescript (opts: UseDashboardTimeRangeOptions = {}) => DashboardTimeRange ``` ### `useDeviceAction` Entry point for wiring device-control UI (reboot, power mode, LED, socket switches, ...) into the voting/approval workflow: components build a typed submission with the foundation's device-action builders and queue it here. Submission and… ```typescript () => UseDeviceActionResult ``` ### `useGetAvailableDevices` Pure projector turning a flat device list (or the `/auth/list-things` nested shape) into the two type-buckets the device-explorer toolbar needs. ```typescript ({ data, }: UseGetAvailableDevicesOptions) => AvailableDevices ``` ### `useHashrateChartData` Multi-series hashrate chart data — the app-side series (miner tail-log) plus an `Aggr Pool` rollup and one line per individual pool (drawn from the paginated `type=minerpool, key=stats-history` ext-data feed). ```typescript (params: UseHashrateChartDataParams) => HashrateChartResult ``` ### `useLiveActions` Polls `GET /auth/actions` every 5 s (`voting`, `ready`, `executing`, `done`). Partitions results into the current user's actions vs others', and gates `othersVoting` behind the `actions:w` permission. ```typescript () => LiveActionsData ``` ### `useMinerDevices` Fetches miner devices from `GET /auth/list-things` (tag `t-miner`) in the nested shape the devkit Miner Explorer expects. Prefer this over `useMiners` when feeding the devkit — only the nested `info.poolConfig` id can resolve pool na… ```typescript (options: UseMinerDevicesOptions = {}) => UseMinerDevicesResult ``` ### `useMiners` Fetches miners with their assigned `poolConfig` from `GET /auth/miners`. Unwraps the paginated response envelope and returns the page rows for the Miner Explorer table plus the site-wide `totalCount`; row shaping (status mapping, pool labe… ```typescript (options: UseMinersOptions = {}) => UseMinersResult ``` ### `usePendingActions` Fetches submitted actions from `GET /auth/actions` — the server-side voting/approval queue (distinct from the local `actionsStore` staging buffer). Vote/cancel mutations invalidate this query's key. ```typescript (options: UsePendingActionsOptions = {}) => UsePendingActionsResult ``` ### `usePoolBalanceHistory` Fetches per-pool revenue/hashrate history from `GET /auth/pools/:pool/balance-history`. The query is disabled until a non-empty `pool` is supplied. ```typescript (pool: string, options: UsePoolBalanceHistoryOptions = {}) => UsePoolBalanceHistoryResult ``` ### `usePoolConfigsData` Fetches raw pool configurations from `GET /auth/configs/pool`. Returns the untransformed rows in the `{ data, isLoading, error }` shape the devkit `usePoolConfigs` transform consumes — keeping tag/endpoint parsing in the component layer pe… ```typescript (options: UsePoolConfigsDataOptions = {}) => UsePoolConfigsDataResult ``` ### `usePoolManagerDashboard` Composes the Pool Manager dashboard view-model: ```typescript (options: UsePoolManagerDashboardOptions = {}) => UsePoolManagerDashboardResult ``` ### `usePoolRows` Returns one row per configured mining pool, ready for the `<MiningPoolsPanel />` table. Reuses the same TanStack queryKey as `usePoolStats` so subscribing here doesn't trigger an extra fetch — both hooks share the cache entry. ```typescript (options: UsePoolRowsOptions = {}) => UsePoolRowsResult ``` ### `usePools` Fetches aggregated pools from `GET /auth/pools` (hashrate / workers / balance / revenue / summary). Feeds the Dashboard pool panel — distinct from `usePoolConfigsData` (`/auth/configs/pool`), which drives the editable Pools list. ```typescript (options: UsePoolsOptions = {}) => UsePoolsResult ``` ### `usePoolStats` Aggregates per-pool worker counts and hashrate from `GET /auth/ext-data?type=minerpool&query={"key":"stats"}`. Returns the `total`, `online`, and `mismatch` triplets and the summed `hashratePhs` shaped for `<HeaderMinersBox poolTotal/poolO… ```typescript (options: UsePoolStatsOptions = {}) => PoolStats ``` ### `usePowerModeTimelineData` TanStack Query hook returning power-mode/status samples shaped for `<PowerModeTimelineChart data={...} />`. ```typescript (params: UsePowerModeTimelineDataParams) => UseQueryResult ``` ### `useSiteConsumption` Projects the freshest consumption sample out of the dashboard's existing tail-log query for the header stats strip (`<HeaderConsumptionBox />`). ```typescript (params: UseConsumptionChartDataParams) => SiteConsumption ``` ### `useSiteConsumptionChartData` Site-level power consumption time-series, shaped for `<LineChartCard />`. Reads `site_power_w` from the `t-powermeter`-tagged tail-log (same source the header's `useSitePowerMeter` snapshot uses), converts W → MW, and emits `highlightedVal… ```typescript (params: UseSiteConsumptionChartDataParams) => SiteConsumptionChartResult ``` ### `useSiteContainerCapacity` Reads the aggregated nominal miner capacity across the site's containers — i.e. the maximum number of miners the facility was designed to host. Used as the "denominator" of the `<HeaderMinersBox />` row: e.g. the `2,188` in `158 / 2,188`. ```typescript (options: UseSiteContainerCapacityOptions = {}) => SiteContainerCapacity ``` ### `useSiteDetailMiners` Resolves the container that backs `selectedUnitId`, then returns the subset of miner devices assigned to that container. Both underlying queries are already issued by the PoolManager page, so React Query deduplicates them — no extra networ… ```typescript (selectedUnitId: string | null) => UseSiteDetailMinersResult ``` ### `useSiteEfficiency` Derives W/TH/s from the latest consumption + hashrate samples. Reuses both existing tail-log queries (no extra fetch) so the header efficiency box stays in step with the corresponding chart cards. Pass `powerW` to swap the numerator in (e.… ```typescript (params: UseSiteEfficiencyParams) => SiteEfficiency ``` ### `useSiteHashrate` Projects the freshest hashrate sample from the dashboard's tail-log query. Shares the TanStack queryKey with `useHashrateChartData`, so subscribing here does NOT trigger an extra fetch — both hooks read the same cache entry. ```typescript (params: UseSiteHashrateParams) => SiteHashrate ``` ### `useSiteMinerCounts` Counts active miners by status for the header `<HeaderMinersBox />`. Hits `/auth/list-things?status=1` with a tight projection (id, type, last.status only) so the response stays small even on big sites. ```typescript (options: UseSiteMinerCountsOptions = {}) => UseQueryResult ``` ### `useSiteMinerStats` Live miner-status breakdown for the header `<HeaderMinersBox />` strip. Reads the realtime tail-log (`key=stat-rtd, type=miner, tag=t-miner`) and projects four aggregate counts. Values reflect what is reporting right now, not the full inve… ```typescript (options: UseSiteMinerStatsOptions = {}) => SiteMinerStats ``` ### `useSitePowerMeter` Site-level power reading for the header's `<HeaderConsumptionBox />`. Reads the freshest snapshot from a `t-powermeter`-tagged thing at `info.pos = 'site'`; falls back to a `t-container`-tagged thing if no powermeter is configured (matches… ```typescript (options: UseSitePowerMeterOptions = {}) => SitePowerMeter ``` ### `useSitesOverview` Composes the Sites Overview dataset from three sources: ```typescript (options: UseSitesOverviewOptions = {}) => UseSitesOverviewResult ``` ### `useSitesOverviewData` Projects raw site-overview rows into a `<PoolManagerSitesOverview />`- ready shape: each container gets its per-container hashrate (in MH/s), an attached pool-stats row keyed by container id, and a `mining` / `offline` status derived from… ```typescript ({ units: rawUnits, poolStats, isLoading, tailLogItem, }: UseSitesOverviewDataOptions) => UseSitesOverviewDataResult ``` ### `useSiteStatusLive` Polls `GET /auth/site/status/live?overwriteCache=true` every 5s for the composite site snapshot (hashrate, power, efficiency, miner / alert / pool counts). Returns the raw typed payload for a hook or component to shape into dashboard cards. ```typescript (options: UseSiteStatusLiveOptions = {}) => UseSiteStatusLiveResult ``` ### `useSubmitPendingActions` Submits the locally-staged `actionsStore` queue to the voting/approval workflow. This is the network half of the write flow: the devkit modals only *enqueue* (`setAddPendingSubmissionAction`); nothing posted to `/auth/actions` until this h… ```typescript () => UseSubmitPendingActionsResult ``` ### `useSubmitSingleAction` Submits a single action from the staged queue by its local `id`. Inspects the 200 response body for embedded errors before treating the call as successful. Removes the action from the queue only on genuine success. ```typescript () => UseSubmitSingleActionResult ``` ### `useVoteOnAction` Casts an approve/reject vote on a pending action via `PUT /auth/actions/voting/:id/vote`. Invalidates the pool/miner/actions caches on success so the review tray and dashboard reflect the new state. Gated by `actions:w`. ```typescript () => UseVoteOnActionResult ``` ## From `@tetherto/mdk-react-devkit` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `useContainerWidgetsData` Site Overview data hook: composes the container inventory + realtime miner aggregate (`useContainerWidgets`) with the per-model thresholds (`useContainerSettings`) and shapes them into the card-ready `ContainerWidgetItem[]` the [``](/reference/ui/components/features/#containerwidgets) grid renders. All aggregate-field slicing and alarm math lives in `@tetherto/mdk-ui-foundation`; this hook only formats display values and selects card props ```typescript (options?: UseContainerWidgetsOptions) => UseContainerWidgetsDataResult ``` ### `usePoolConfigs` Transforms raw pool-configuration rows from the host's API into `PoolSummary` objects for Pool Manager components ```typescript ({ data, isLoading, error, }: Partial) => UsePoolConfigsResult ``` ### `useSiteOverviewDetailsData` Composes the per-site overview view-model: pools, performance series, and recent activity ```typescript (unit?: UnitData | undefined, options?: SiteOverviewDetailsDataOptions) => UseSiteOverviewDetailsDataResult ``` # Device (/reference/ui/hooks/device) Hooks for device validation, facility management, and equipment operations. ## Package `@tetherto/mdk-react-adapter` ## Hooks @tetherto/mdk-react-adapter Import the public APIs on this page from [`@tetherto/mdk-react-adapter`](/reference/ui/#tethertomdk-react-adapter). ### `useMinerDuplicateValidation` Async validation hook that flags duplicate miners against the device inventory (MAC / serial / IP / human-facing code). ```typescript () => { duplicateError: boolean; isDuplicateCheckLoading: boolean; checkDuplicate: (selectedEditSocket: { miner?: { id?: string | undefined; } | undefined; } | null, { macAddress, serialNumber, addre… ``` ### `useNominalConfig` Normalises the raw `globalConfig` payload (single object or array — APIs differ across environments) into a flat `NominalConfig` with sane zero-defaults. ```typescript ({ globalConfig }: UseNominalConfigInput) => NominalConfig ``` ### `usePduViewer` Pan/zoom controller for the PDU floor-plan viewer. ```typescript () => UsePduViewerReturn ``` ### `useStaticMinerIpAssignment` Derives the static IP address a miner should receive based on its physical position (container number + socket coordinates). ```typescript (selectedEditSocket: Partial) => { minerIp: string; setMinerIp: React.Dispatch>; isStaticIpAssignment: boolean; } ``` # Example (/reference/ui/hooks/example) Example hooks for learning and demonstration purposes. ## Package `@tetherto/mdk-react-adapter` ## Hooks @tetherto/mdk-react-adapter Import the public APIs on this page from [`@tetherto/mdk-react-adapter`](/reference/ui/#tethertomdk-react-adapter). ### `useSystemInfo` Reference example hook. Composes three read-only Gateway endpoints into a single page-ready payload for the shell's System Info page: • `GET /auth/site` → configured site label • `GET /auth/userinfo` → signed-in user's email + roles • `GET… ```typescript () => UseSystemInfoResult ``` # Feedback (/reference/ui/hooks/feedback) Hooks for user feedback and notifications. ## Package `@tetherto/mdk-react-devkit` ## Hooks @tetherto/mdk-react-devkit ### Toast notifications ```tsx ``` #### Related API - [**`useNotifications`**](/reference/ui/hooks/store/#usenotifications) ### When to use `useNotification` Use `useNotification` when a React interaction must show a success, error, information, or warning message (toast). It is the styled notification surface; use [`useNotifications`](/reference/ui/hooks/store/#usenotifications) when a component only needs the headless unread counter. ### `useNotification` example ```tsx function SaveButton() { const { notifySuccess, notifyError } = useNotification() const handleSave = async () => { try { await saveData() notifySuccess('Saved', 'Your changes have been saved.') } catch { notifyError('Save failed', 'Try again.', { dontClose: true }) } } return } ``` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `useNotification` Custom hook for showing notifications backed by the headless `notificationStore`. Uses `@tetherto/mdk-react-devkit/primitives` [**`Toast`**](/reference/ui/components/feedback/#toast) and [**`Toaster`**](/reference/ui/components/feedback/#toaster) components ```typescript () => { notifySuccess: (message: string, description?: string | undefined, options?: NotificationOptions | undefined) => void; notifyError: (message: string, description?: string | undefined, options?: NotificationOptions | undefined) => vo… /* see source */ ``` # Filter (/reference/ui/hooks/filters) Hooks for filter components and state management. ## Package `@tetherto/mdk-react-devkit` ## Hooks @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `useListViewFilters` Persistent filter-state hook for list views — syncs the active filter set to URL search params ```typescript ({ site, selectedType, availableDevices: _availableDevices, typeFiltersForSite, }: UseListViewFiltersParams) => { onFiltersChange: (selections: CascaderValue[]) => void; listViewFilterOptions: FilterOption[]; filters: LocalFilters | undefin… /* see source */ ``` ### `useReportTimeFrameSelectorState` State hook backing the reporting time-frame selector — exposes the active window and setters ```typescript () => ReportTimeFrameSelectorState ``` ### `useTimeframeControls` Core state machine for [**`TimeframeControls`**](/reference/ui/components/filters/#timeframecontrols) — owns year / month / week selection and resolves the date-range output ```typescript ({ dateRange, timeframeType: timeframeTypeProp, onRangeChange, onTimeframeTypeChange, isWeekSelectVisible, weekTree, }: UseTimeframeControlsParams) => { timezone: string; selectedYear: number; selectedMonth: number; visibleWeeks: Week[]; ha… /* see source */ ``` # Form (/reference/ui/hooks/forms) Hooks for form state management and validation. ## Package `@tetherto/mdk-react-devkit` ## Hooks @tetherto/mdk-react-devkit ### Used by form primitives ```tsx ``` #### Related API - [**`FormField`**](/reference/ui/components/forms/#formfield) ### When to use `useFormField` Use `useFormField` inside a custom form-field child that needs the current field's ID, validation state, or ARIA attributes. Standard pre-built fields already consume this context internally. ### `useFormField` workflow `FormField` provides the field context, `FormItem` provides the generated IDs, and custom descendants read that combined context through `useFormField`. ### `useFormField` prerequisites The hook must be rendered beneath `FormField` and `FormItem`. It is not a standalone form-state hook and does not replace `react-hook-form`'s `useForm`. ### `useFormField` example ```tsx FormControl, FormField, FormItem, FormLabel, useFormField, } from '@tetherto/mdk-react-devkit' function FieldStatusDot() { const { invalid, isDirty } = useFormField() const state = invalid ? 'error' : isDirty ? 'dirty' : 'clean' return } function NamedField({ control }) { return ( ( Name )} /> ) } ``` @tetherto/mdk-react-devkit ### Reset form state ```tsx ``` #### Related API - [**`Form`**](/reference/ui/components/forms/#form) - [**`FormInput`**](/reference/ui/components/forms/#forminput) ### When to use `useFormReset` Use `useFormReset` when resetting a [`react-hook-form`](https://react-hook-form.com/) form also needs before/after callbacks or a reusable dirty-state guard. Call the form object's `reset` method directly when no lifecycle behaviour is needed. ### `useFormReset` workflow Create the form with `useForm`, pass that instance to `useFormReset`, then wire the returned `resetForm` handler to a reset or cancel action. The hook reports `isDirty` from the same form state so the action can be disabled until values change. ### `useFormReset` prerequisite The hook requires a configured `react-hook-form` instance. It composes with [`Form`](/reference/ui/components/forms/#form) and fields such as [`FormInput`](/reference/ui/components/forms/#forminput); it does not create or submit the form. ### `useFormReset` example ```tsx type MinerFields = { name: string; ip: string } function MinerEditForm({ onSubmit }: { onSubmit: (values: MinerFields) => void }) { const form = useForm({ defaultValues: { name: '', ip: '' } }) const { resetForm, isDirty } = useFormReset({ form, onAfterReset: () => console.log('Form reset'), }) return (
) } ``` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `useFormField` Read-only context hook for form field children — returns the field's id, error state, and ARIA attributes ```typescript () => UseFormFieldReturn ``` ### `useFormReset` Hook to handle form reset with callbacks ```typescript ({ form, onBeforeReset, onAfterReset, }: UseFormResetOptions) => UseFormResetReturn ``` # Miscellaneous (/reference/ui/hooks/misc) Miscellaneous and general-purpose hooks. ## Package `@tetherto/mdk-react-devkit` ## Hooks @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `useEnergyReportSite` Merges site energy consumption (v2 /auth/metrics/consumption) with snapshot tail-log and container list data for the Energy report site tab ```typescript ({ dateRange: _dateRange, consumptionLog, consumptionLoading, consumptionFetching, consumptionError, nominalPowerAvailabilityMw, nominalConfigLoading, tailLog, tailLogLoading, containers, containersLoading, }: UseEnergyReportSiteInput) => U… /* see source */ ``` ### `useHashBalance` Derives hash-balance metrics and chart datasets from finance log entries for the active date range, currency, and timeframe type. Used by hash balance panels ```typescript ({ data, log, currency, dateRange, timeframeType, }: UseHashBalanceInput) => { siteHashRevenueChartData: BarChartDataResult; networkHashpriceChartData: BarChartDataResult; combinedCostChartData: BarChartDataResult; isEmpty: boolean; periodT… /* see source */ ``` ### `useSubsidyFees` Aggregates raw subsidy-fee log entries into chart-ready datasets keyed by the active period type (day / week / month / year) and surfaces a summary for the matching reporting widgets. Used by [`SubsidyFee`](/reference/ui/components/dashboards/#subsidyfee); expose to downstream apps that need to recompose the same datasets in a custom UI ```typescript ({ data, log, dateRange }: UseSubsidyFeesInput) => { summary: SubsidyFeeSummary; filteredLog: SubsidyFeesLogEntry[]; aggregatedData: AggregatedPeriodData[]; subsidyFeesChartData: BarChartDataResult; averageFeesChartData: BarChartDataResult;… /* see source */ ``` ### `useUpdateExistedActions` Mutation hook that updates only the changed fields of an existing action record. Removes tags from existing pending submissions if those tags belong to the newly selected devices; if the resulting tag list is empty, the submission itself is removed. This avoids duplicate pending actions for the same device ```typescript () => { updateExistedActions: ({ actionType, pendingSubmissions, selectedDevices, }: UpdateExistedActionsParams) => void; } ``` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `useHashrate` Base hook for a single [**`Hashrate`**](/reference/ui/components/charts/#hashrate) tab (single-site mode). Normalizes a grouped-hashrate query result to the `{ log, isLoading, error }` shape consumed by [``](/reference/ui/components/charts/#hashratesiteview), [``](/reference/ui/components/charts/#hashrateminertypeview), and [``](/reference/ui/components/charts/#hashrateminingunitview). Call once per tab the consumer needs to render - each tab fetches independently because they use different `groupBy` axes and (typically) different date ranges ```typescript ({ query }?: UseHashrateOptions) => UseHashrateResult ``` ### `useOperationsDashboard` Shapes raw operational metric logs into chart-ready payloads for the four operational-dashboard cards. DI-style: it never fetches - wire your data layer (RTK Query, TanStack, fixtures) and pass the results in. All unit conversion and series shaping happens here so the chart components stay purely presentational ```typescript (input?: Partial<{ hashrate: Partial<{ log: readonly OperationsTrendPoint[]; nominalValue: number | null; isLoading: boolean; error: unknown; }>; consumption: Partial<{ log: readonly OperationsTrendPoint[]; nominalValue: number | null; isLo… /* see source */ ``` # Navigation (/reference/ui/hooks/navigation) Hooks for navigation and routing state. ## Package `@tetherto/mdk-react-devkit` ## Hooks @tetherto/mdk-react-devkit ### Persist sidebar state ```tsx ``` #### Related API - [**`useSidebarSectionState`**](/reference/ui/hooks/navigation/#usesidebarsectionstate) - [**`useSidebarExpandedState`**](/reference/ui/hooks/navigation/#usesidebarexpandedstate) ### When to use sidebar state hooks Use `useSidebarExpandedState` for the whole sidebar's expanded state and `useSidebarSectionState` for an individual section. Both persist their value in `localStorage`, so navigation choices survive a reload. ### Sidebar state example ```tsx useSidebarExpandedState, useSidebarSectionState, } from '@tetherto/mdk-react-devkit' function AppSidebar() { const [expanded, setExpanded] = useSidebarExpandedState(false) const [devicesOpen, setDevicesOpen] = useSidebarSectionState('devices', true) return ( ) } ``` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `useSidebarExpandedState` Custom hook to persist sidebar expanded state in localStorage ```typescript (defaultExpanded: boolean) => [boolean, (expanded: boolean) => void] ``` ### `useSidebarSectionState` Custom hook to persist individual section open/closed states in localStorage ```typescript (sectionId: string, defaultOpen: boolean) => [boolean, (open: boolean) => void] ``` # Operations center (/reference/ui/hooks/op-center) Hooks for operations center functionality and monitoring. ## From `@tetherto/mdk-react-adapter` @tetherto/mdk-react-adapter Import the public APIs on this page from [`@tetherto/mdk-react-adapter`](/reference/ui/#tethertomdk-react-adapter). ### `useCabinetDevices` Fetches one LV cabinet's family of devices — the powermeters and temperature sensors whose `info.pos` sits under the cabinet `root` (`buildCabinetDetailParams`) — polled at the Op-Center realtime cadence and flattened across the per-… ```typescript (root: string, options: UseCabinetDevicesOptions = {}) => UseCabinetDevicesResult ``` ### `useCabinetGroups` Fetches the Explorer cabinet-tab devices (powermeters + temperature sensors) and groups them by their owning container (`info.container`); devices without a container assignment (site-level meters) collect under the `site` group, sorted la… ```typescript (options: UseCabinetGroupsOptions = {}) => UseCabinetGroupsResult ``` ### `useContainerSettings` Fetches per-model container thresholds/parameters from `GET /auth/global/data?type=containerSettings`. Feeds the threshold status indicators (tank pressure, oil/water temperature) on the container widgets and detail tabs. ```typescript (options: UseContainerSettingsOptions = {}) => UseContainerSettingsResult ``` ### `useContainerSnapshots` Fetches the detail snapshots for the selected containers — the richer projection (`buildContainerDetailParams`) that carries `container_specific.pdu_data` plus the tank / cooling / power-mode config the detail panel controls read. `c… ```typescript (containerKeys: string[], options: UseContainerSnapshotsOptions = {}) => UseContainerSnapshotsResult ``` ### `useContainerWidgets` Data source for the Site Overview Container Widgets grid: the container inventory (one card per container) plus the latest per-miner realtime aggregate sample the cards derive their summaries from. Card-shaped payload derivation lives with… ```typescript (options: UseContainerWidgetsOptions = {}) => UseContainerWidgetsResult ``` ### `useExplorerList` Fetches the thing list behind one Explorer tab (`container` / `miner` / `cabinet`) from `GET /auth/list-things`, tag-filtered and projected by the foundation's Explorer params builder. Rows are flattened across the per-Kernel envelope so r… ```typescript (tab: ExplorerTabValue, options: UseExplorerListOptions = {}) => UseExplorerListResult ``` ### `useFeatureFlags` Fetches the deployment feature flags from `GET /auth/featureConfig` (camelCase route — there is no `/auth/feature-config`). Static deployment config — fetched once per session, no polling. Gates optional tabs/sub-routes (`containerCharts`,… ```typescript (options: UseFeatureFlagsOptions = {}) => UseFeatureFlagsResult ``` ### `usePduLayout` Fetches a container type's static PDU socket grid from `GET /auth/pdu-layout`. The grid is provisioned in the container worker's `pduGridLayout` config keyed by the exact type string — an unprovisioned type is a backend 400 (`ERR_PDU_LAYOU… ```typescript (params: UsePduLayoutParams, options: UsePduLayoutOptions = {}) => UsePduLayoutResult ``` ### `useRackLayout` Fetches the rack structure for a worker type from `GET /auth/list-racks` (`type` is required — the backend 400s with `ERR_TYPE_INVALID` without it). Feeds the Explorer rack grouping. ```typescript (params: UseRackLayoutParams, options: UseRackLayoutOptions = {}) => UseRackLayoutResult ``` ### `useSite` Fetches the configured site label from `GET /auth/site`. Static deployment config — fetched once per session, no polling. ```typescript (options: UseSiteOptions = {}) => UseSiteResult ``` ### `useThingComment` Device-comment writes for the Explorer detail panel — add, edit, and delete against `/auth/thing/comment` (the author is stamped server-side from the session token). Comments ride on the thing rows themselves (`comments` in the list-things… ```typescript () => UseThingCommentResult ``` ### `useThingDetail` Fetches a single thing by id from `GET /auth/list-things` with the full Op Center field projection — the data source for the Explorer detail panel and the container Thing-detail view. ```typescript (id: string | undefined, options: UseThingDetailOptions = {}) => UseThingDetailResult ``` ## From `@tetherto/mdk-react-devkit` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `useExplorerSelection` Bridges the Explorer table selection into the shared `devicesStore` that the write-control cards read. Given the active tab and the table's row-selection, it dispatches the matching setters — containers → `selectMultipleContainers`, miners → `setSelectDevice` + `selectDeviceTag`, cabinets → `selectLVCabinet` — and, for containers/miners, fetches the richer detail snapshots (`useContainerSnapshots`) so the controls see the full `last.snap` config (tank / cooling / power-mode) the lean list projection omits, then derives the per-socket selection into the store. Selections are reset whenever the selection or tab changes and on unmount, so a stale selection can never drive the panel ```typescript ({ deviceType, rows, selected, }: UseExplorerSelectionParams) => UseExplorerSelectionResult ``` ### `useMinerDetail` Reads the miner selection the [`useExplorerSelection`](/reference/ui/hooks/op-center/#useexplorerselection) bridge writes into `devicesStore` and shapes the head miner for the read-only cards of the miner detail panel — the info rows ([`MinerInfoCard`](/reference/ui/components/widgets/#minerinfocard)) and the per-chip frequency / temperature stats ([`MinerChipsCard`](/reference/ui/components/widgets/#minerchipscard)). The write controls ([`MinerControlsCard`](/reference/ui/components/widgets/#minercontrolscard)) and the aggregate stats ([`StatsGroupCard`](/reference/ui/components/widgets/#statsgroupcard)) read the same store directly ```typescript () => UseMinerDetailResult ``` # Permission (/reference/ui/hooks/permission) Hooks for checking permissions and managing access control. ## Package `@tetherto/mdk-react-adapter` ## Hooks @tetherto/mdk-react-adapter ### Check a permission ```tsx ``` #### Related API - [**`useAuth`**](/reference/ui/hooks/store/#useauth) ### When to use `useCheckPerm` Use `useCheckPerm` for a single reactive permission, write-access or capability gate in a React component. It reads the current permissions from the same auth store exposed by [`useAuth`](/reference/ui/hooks/store/#useauth). ### `useCheckPerm` example ```tsx function EditUsersButton() { const canEditUsers = useCheckPerm({ perm: 'users:write' }) return canEditUsers ? : null } ``` @tetherto/mdk-react-adapter Import the public APIs on this page from [`@tetherto/mdk-react-adapter`](/reference/ui/#tethertomdk-react-adapter). ### `useCheckPerm` Hook to check if the current user has a specific permission. Reads `permissions` from the headless `authStore` via `@tetherto/mdk-react-adapter`. ```typescript ({ perm, write, cap }: PermissionCheck) => boolean ``` ### `useHasPerms` Hook returning a permission-check callback bound to the current `authStore.permissions`. ```typescript () => ((req: PermissionRequest) => boolean) ``` ### `useIsFeatureEditingEnabled` Hook to check if the current user has the capability to edit feature flags. ```typescript () => boolean ``` # Settings (/reference/ui/hooks/settings) Hooks for managing user settings and preferences. ## Package `@tetherto/mdk-react-devkit` ## Hooks @tetherto/mdk-react-devkit ### Header controls ```tsx ``` #### Related API - [**`useNotification`**](/reference/ui/hooks/feedback/#usenotification) ### When to use `useHeaderControls` Use `useHeaderControls` in settings UI that reads or changes the shared header item preferences. It exposes the current preferences together with focused toggle and reset handlers. ### `useHeaderControls` notification behaviour `handleToggle` and `handleReset` show a success notification. Avoid calling either handler from rapidly changing state or an effect that can repeat, because each invocation creates another toast. ### `useHeaderControls` example ```tsx function HeaderSettings() { const { preferences, handleToggle, handleReset } = useHeaderControls() return (
{Object.entries(preferences).map(([key, visible]) => ( ))}
) } ``` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `useHeaderControls` Read/write hook for the global header-controls store (toggles, sticky flag, theme) ```typescript () => { preferences: { poolMiners: boolean; appMiners: boolean; poolHashrate: boolean; appHashrate: boolean; consumption: boolean; efficiency: boolean; }; isLoading: boolean; error: null; handleToggle: (key: keyof HeaderPreferences, value:… /* see source */ ``` # Store (/reference/ui/hooks/store) Hooks for accessing Zustand stores and managing application state. ## Package `@tetherto/mdk-react-adapter` ## Hooks @tetherto/mdk-react-adapter ### Core store hooks ```tsx ``` #### Related API - [**`useNotification`**](/reference/ui/hooks/feedback/#usenotification) ### When to use core store hooks Use these hooks when a React component needs a reactive view of the headless Foundation stores: `useAuth` for session state, `useTimezone` for the selected IANA timezone, `useNotifications` for the unread count and `useActions` for the pending operator-action queue. Non-React code can read the corresponding Foundation store directly. ### Core store hooks example ```tsx useActions, useAuth, useNotifications, useTimezone, } from '@tetherto/mdk-react-adapter' function ShellStatus() { const { token } = useAuth() const { timezone } = useTimezone() const { count } = useNotifications() const { pendingSubmissions } = useActions() return (
Session
{token ? 'Active' : 'Signed out'}
Timezone
{timezone}
Unread
{count}
Pending actions
{pendingSubmissions.length}
) } ``` @tetherto/mdk-react-adapter ### Device store access ```tsx ``` ### When to use `useDevices` Use `useDevices` in React components that need the headless device inventory or current device selection. It is the React-bound view of `devicesStore`; code outside React should use the foundation store directly. ### `useDevices` example ```tsx function DeviceToolbar() { const { selectedDevices, setSelectedDevices } = useDevices() return (

Selected: {selectedDevices.length}

) } ``` @tetherto/mdk-react-adapter Import the public APIs on this page from [`@tetherto/mdk-react-adapter`](/reference/ui/#tethertomdk-react-adapter). ### `useActions` React-bound view of the headless `actionsStore` (command queue + lifecycle). ```typescript () => ActionsStore ``` ### `useAuth` React-bound view of the headless `authStore`. Equivalent of `useStore(authStore)` — exposed as a hook for ergonomic callsites. ```typescript () => AuthStore ``` ### `useDevices` React-bound view of the headless `devicesStore` (miners, containers, PDUs). ```typescript () => DevicesStore ``` ### `useNotifications` React-bound view of the headless `notificationStore` (toast queue + history). ```typescript () => NotificationStore ``` ### `useTimezone` React-bound view of the headless `timezoneStore` (selected IANA zone). ```typescript () => TimezoneStore ``` # Table (/reference/ui/hooks/tables) Hooks for table components and data management. ## Package `@tetherto/mdk-react-devkit` ## Hooks @tetherto/mdk-react-devkit ### Explorer data ```tsx ``` #### Related API - [**`DataTable`**](/reference/ui/components/tables/#datatable) @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `useExplorerData` Explorer list data hook: fetches the things behind one tab (`useExplorerList`) and shapes them for [``](/reference/ui/components/tables/#deviceexplorer) — applying the toolbar's search + filter selections client-side and deriving the search-autocomplete and filter-cascader options from the fetched rows. The tag-based backend query lives in `@tetherto/mdk-ui-foundation`; this hook only reads snapshot fields for display filtering. Search, status-filter and (in [`DeviceExplorer`](/reference/ui/components/tables/#deviceexplorer)) column sort are all **client-side**, over a tag-filtered, capped fetch — this mirrors the reference app. Fine for containers/cabinets; for very large miner fleets this fetches the cap and filters in the browser (no server paging). Push status into the foundation query + wire `limit`/`offset` if that ceiling is ever hit ```typescript (options: UseExplorerDataOptions) => UseExplorerDataResult ``` # Utility (/reference/ui/hooks/utility) General-purpose utility hooks. ## From `@tetherto/mdk-react-adapter` @tetherto/mdk-react-adapter Import the public APIs on this page from [`@tetherto/mdk-react-adapter`](/reference/ui/#tethertomdk-react-adapter). ### `useBeepSound` Plays a repeating beep sound at a configurable interval. ```typescript ({ isAllowed = DEFAULTS.IS_ALLOWED, volume = DEFAULTS.VOLUME, delayMs = DEFAULTS.DELAY_MS, }: UseBeepSoundOptions = {}) => void ``` ### `useContextualModal` Headless open/close state for a modal that needs to remember the subject it was opened against (the row being edited, the device being inspected, etc.). ```typescript ({ onOpen, onClose, }: UseContextualModalParams = {}) => { modalOpen: boolean; handleClose: () => void; handleOpen: (sub: T | null) => void; subject: T | null; setSubject: React.Dispatch DeviceResolution ``` ### `useKeyDown` Tracks whether a specific keyboard key is currently held down. ```typescript (keyName: string) => boolean ``` ### `useLocalStorage` Custom hook for type-safe localStorage access with cross-tab sync. ```typescript (key: string, defaultValue: T) => [T, (value: T | ((prev: T) => T)) => void, VoidFunction] ``` ### `usePagination` Custom hook for managing pagination state ```typescript (args: PaginationArgs = {}) => UsePaginationReturn ``` ### `usePlatform` SSR-safe React hook returning the detected client OS. ```typescript () => PlatformResult ``` ### `useSubtractedTime` Returns `Date.now() - diff`, refreshing on a fixed interval (default 5s). ```typescript (diff: number, interval = DEFAULT_INTERVAL) => number ``` ### `useTimezoneFormatter` Timezone-aware date formatting hook. ```typescript () => UseTimezoneFormatterReturn ``` ### `useWindowSize` Hook to track window size changes ```typescript () => WindowSize ``` ## From `@tetherto/mdk-react-devkit` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `useCostSummary` Base hook for the cost-summary reporting page (single-site mode). Owns the date-range / period UI state and the pure transform from a v2 `/auth/finance/cost-summary` response into headline metrics and time-series. Consumers wire their own fetch (RTK Query, TanStack Query, fixtures, ...) and pass the result through `query` - the hook never fetches itself. Multi-site mode is composed at the page level (T-13) by feeding a different response shape to the same view-model primitives; this base hook stays single-site to keep the input contract narrow ```typescript ({ query, ...dateRangeOptions }?: UseCostSummaryOptions) => { queryParams: CostSummaryQueryParams | null; isLoading: boolean; error: {} | null; metrics: CostSummaryDisplayMetrics | null; costLog: readonly CostTimeSeriesEntry[]; btcPriceLog:… /* see source */ ``` # Widget (/reference/ui/hooks/widgets) Hooks for dashboard widgets and data cards. ## Package `@tetherto/mdk-react-devkit` ## Hooks @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `useContainerThresholds` Hook that reads and updates the temperature/pressure/power thresholds for a single container. Features: - Load and save container threshold settings - Auto-save default thresholds when none exist - Validate and auto-adjust overlapping thresholds - Handle reset to saved/default values ```typescript ({ data, onSave, }: UseContainerThresholdsProps) => UseContainerThresholdsReturn ``` ### `useFinancialDateRange` Resolves the active financial date range (start/end) used by every reporting-section query ```typescript (options?: UseFinancialDateRangeOptions | undefined) => UseFinancialDateRangeResult ``` # Query helpers (/reference/ui/query-helpers) TanStack Query helper functions for fetching API data, managing mutations, and configuring the query client. These utilities streamline integration with the MDK Gateway API. ## Package `@tetherto/mdk-ui-foundation` ## Query helpers @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `appendQuery` Append query params to a URL. `undefined` / `null` values and empty arrays are skipped; array values serialize comma-separated (`{ ids: ['a', 'b'] }` → `?ids=a,b`), the `qs` `arrayFormat: 'comma'` convention the Gateway expects, without th… ```typescript (url: string, params: Record) => string ``` ### `buildUrl` Join a base URL and a path, tolerating a trailing slash on the base and a missing leading slash on the path. ```typescript (base: string, path: string) => string ``` ### `createBearerFetcher` Build a `Fetcher` that injects `Authorization: Bearer <token>` from the supplied token getter (defaults to `authStore`). Non-2xx responses throw an `MdkFetchError` carrying the HTTP status and parsed body. ```typescript (options: { /** Override the token source — defaults to `authStore.getState().token`. */ getToken?: () => string | null /** Override `fetch` — pass a stub in tests. */ fetchImpl?: typeof fetch } = {}… ``` ### `createGetQueryFn` Build a signal-aware GET `queryFn`. This is the single place the `AbortSignal` is threaded into the fetcher: TanStack cancels a query (firing the signal) when its last observer unmounts on navigation, or when an invalidation supersedes a r… ```typescript (fetcher: Fetcher, url: string | (() => string)) => ({ signal }?: QueryFnContext) => Promise ``` ### `createMdkQueryClient` Build a TanStack `QueryClient` carrying the data-source runtime. ```typescript (options: CreateMdkQueryClientOptions = {}) => QueryClient ``` ### `createResourceMutation` Build a write factory: `(client, ...) => { mutationKey, mutationFn, invalidates }`. ```typescript (config: ResourceMutationConfig) => (client: on, fetcher?: Fetcher) => { mutationKey: ResourceKey; invalidates: ResourceKey[]; mutationFn: (payload: TBody) => Promise; } ``` ### `createResourceQuery` Build a read factory: `(client, input) => { queryKey, queryFn }`. ```typescript (config: ResourceQueryConfig) => (client: on, input: TInput, fetcher?: Fetcher) => { queryKey: ResourceKey; queryFn: ({ signal }?: QueryFnContext) => Promise; } ``` ### `endpointUrl` Resolve a named endpoint against the client's map into an absolute URL. ```typescript (client: QueryClient, name: EndpointName, pathParams?: Record) => string ``` ### `getApiBaseUrl` Read the configured base URL back from a `QueryClient`. Falls back to the default when the client was not produced by `createMdkQueryClient`. ```typescript (client: QueryClient) => string ``` ### `getEndpoints` Read the injected endpoint map, or `undefined` when the client carries none. The mining factories fall back to the bundled mining preset. ```typescript (client: QueryClient) => EndpointMap | undefined ``` ### `getFetcher` Read the injected transport, or `undefined` when the client carries none — which is the case for a hand-built `QueryClient`. Callers decide the fallback; the mining factories fall back to the bundled bearer fetcher. ```typescript (client: QueryClient) => Fetcher | undefined ``` ### `getMdkRuntime` Read the whole runtime at once. `baseUrl` is always resolved; `fetcher` and `endpoints` are `undefined` unless injected. ```typescript (client: QueryClient) => MdkRuntime ``` ### `mdkFetch` Module-level singleton bearer fetcher reading from the global `authStore`. Used as the default by the mining query factories (`tailLogQuery`, etc.). ```typescript Fetcher ``` ### `resolveApiBaseUrl` Resolves the API base URL: ```typescript (override?: string) => string ``` ### `resolvePath` Substitute `:name` segments in a path template, URL-encoding each value. ```typescript (template: string, params: Record = {}) => string ``` ### `resourceKey` ```typescript (name: string, params?: unknown, scope: readonly string[] = []) => ResourceKey ``` ### `runtimeEndpoints` Endpoint map for a factory call: whatever the client carries, else mining. ```typescript (client: QueryClient) => EndpointMap ``` ### `runtimeFetcher` Transport for a factory call: whatever the client carries, else the bundled bearer fetcher. ```typescript (client: QueryClient) => Fetcher ``` # Stores (/reference/ui/stores) Zustand stores providing global state management for authentication, devices, actions, notifications, and timezone handling. ## Package `@tetherto/mdk-ui-foundation` ## Stores @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `actionsStore` Module-level singleton holding the queue of pending submission actions and the sidebar open/pinned state. #### State | Field | Type | |-------|------| | `pendingSubmissions` | `-` | | `sidebarOpen` | `-` | | `sidebarPinned` | `-` | #### Actions **`setPendingSubmissionActions`** ```typescript (actions: PendingSubmissionAction[]) => void ``` **`setAddPendingSubmissionAction`** ```typescript (action: Omit) => void ``` **`removeTagsFromPendingAction`** ```typescript (payload: { submissionId: number; tags: string[] }) => void ``` **`removePendingSubmissionAction`** ```typescript (payload: { id: number }) => void ``` **`updatePendingSubmissionAction`** ```typescript (action: Partial & { id: number }) => void ``` **`clearAllPendingSubmissions`** ```typescript () => void ``` **`setSidebarOpen`** ```typescript (open: boolean) => void ``` **`setSidebarPinned`** ```typescript (pinned: boolean) => void ``` @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `authStore` Module-level singleton store holding the current session token and resolved permissions config. React adapters bind to this through `useAuth()`; non-React callers can `authStore.getState()` directly. #### State | Field | Type | |-------|------| | `token` | `-` | | `permissions` | `-` | #### Actions **`setToken`** ```typescript (token: string | null) => void ``` **`setPermissions`** ```typescript (permissions: unknown | null) => void ``` **`reset`** ```typescript () => void ``` @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `devicesStore` Module-level singleton tracking selected devices, containers, sockets, and the device-tag map across the UI. Drives bulk-action toolbars, filtering, and the device explorer. #### State | Field | Type | |-------|------| | `selectedDevices` | `-` | | `selectedSockets` | `-` | | `filterTags` | `-` | | `selectedDevicesTags` | `-` | | `selectedContainers` | `-` | | `selectedLvCabinets` | `-` | #### Actions **`selectContainer`** ```typescript (device: DevicePayload) => void ``` **`selectLVCabinet`** ```typescript (device: DevicePayload) => void ``` **`removeSelectedContainer`** ```typescript (device: DevicePayload) => void ``` **`removeSelectedLVCabinet`** ```typescript (device: DevicePayload) => void ``` **`selectMultipleContainers`** ```typescript (devices: DevicePayload[]) => void ``` **`removeMultipleContainers`** ```typescript (devices: DevicePayload[]) => void ``` **`setSelectedDevices`** ```typescript (devices: DevicePayload[]) => void ``` **`setSelectedLvCabinets`** ```typescript (devices: Record) => void ``` **`setMultipleSelectedDevices`** ```typescript (devices: DevicePayload[]) => void ``` **`removeMultipleSelectedDevices`** ```typescript (deviceIds: string[]) => void ``` **`setSelectDevice`** ```typescript (device: DevicePayload) => void ``` **`removeSelectedDevice`** ```typescript (deviceId: string) => void ``` **`setFilterTags`** ```typescript (tags: string[]) => void ``` **`removeFilterTag`** ```typescript (tag: string) => void ``` **`setSelectedSockets`** ```typescript (sockets: Record) => void ``` **`setSelectSocket`** ```typescript (socket: SocketData) => void ``` **`removeSelectedSocket`** ```typescript (payload: RemoveSocketPayload) => void ``` **`setMultipleSelectedSockets`** ```typescript (sockets: SocketData[]) => void ``` **`removeMultipleSelectedSockets`** ```typescript (sockets: SocketData[]) => void ``` **`setResetSelections`** ```typescript () => void ``` **`resetSelectedDevicesTags`** ```typescript () => void ``` **`selectDeviceTag`** ```typescript (payload: DeviceTagPayload) => void ``` **`removeDeviceTag`** ```typescript (payload: DeviceTagPayload) => void ``` @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `notificationStore` Module-level singleton exposing the unread-notification counter that drives header badges and the toast viewport. Increment/decrement from anywhere; React subscribers re-render automatically. #### State | Field | Type | |-------|------| | `count` | `-` | #### Actions **`increment`** ```typescript () => void ``` **`decrement`** ```typescript () => void ``` **`reset`** ```typescript () => void ``` @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `timezoneStore` Module-level singleton holding the user's currently selected IANA timezone (e.g. `'America/New_York'`). Drives every timestamp renderer in the UI and the `useTimezoneFormatter` adapter hook. #### State | Field | Type | |-------|------| | `timezone` | `-` | #### Actions **`setTimezone`** ```typescript (timezone: string) => void ``` # Types (/reference/ui/types) TypeScript type definitions exported from `@tetherto/mdk-react-devkit` and `@tetherto/mdk-ui-foundation`. ## Browse by category ### From `@tetherto/mdk-react-devkit` | Category | Description | |----------|-------------| | [General](/reference/ui/types/devkit-general) | Component and hook prop types | ### From `@tetherto/mdk-ui-foundation` | Category | Description | |----------|-------------| | [API](/reference/ui/types/api) | API request/response types | | [Dashboard](/reference/ui/types/foundation-dashboard) | Dashboard data types | | [General](/reference/ui/types/foundation-general) | General utility types | ## Import pattern ```tsx ``` # API (/reference/ui/types/api) Type definitions for API requests and responses. ## Package `@tetherto/mdk-ui-foundation` ## Types @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `ActionsParams` Free-form query parameters for `GET /auth/actions`. Array values are serialized comma-separated (e.g. `?status=VOTING,APPROVED`). ```typescript type ActionsParams = Record ``` ### `ActionTypeQuery` One entry in the `queries` array sent to `GET /auth/actions?queries=…`. ```typescript type ActionTypeQuery = { type: 'voting' | 'ready' | 'executing' | 'done' | string opts?: { reverse?: boolean; limit?: number; [key: string]: unknown } } ``` ### `AggregatedPool` Aggregated pool row from `GET /auth/pools` (hashrate / workers / balance / revenue / summary). Feeds the Dashboard pool panel, not the Pools list. ```typescript type AggregatedPool = { name?: string hashrate?: number workers?: number balance?: number revenue?: number [key: string]: unknown } ``` ### `AlertSeverity` Severity literal expected by the `ActiveIncidentsCard` row component. ```typescript type AlertSeverity = 'critical' | 'high' | 'medium' ``` ### `AuthTokenRequest` Request body for `POST /auth/token`. The token endpoint refreshes the session and (optionally) downgrades the role set. ```typescript type AuthTokenRequest = { roles?: string[] ttl?: number ips?: string[] scope?: string } ``` ### `AuthTokenResponse` Response from `POST /auth/token`. ```typescript type AuthTokenResponse = { token: string } ``` ### `CancelActionsPayload` Arguments for `DELETE /auth/actions/:type/cancel?ids=<comma>`. ```typescript type CancelActionsPayload = { /** Action type URL segment (e.g. `voting`). */ type: string ids: Array } ``` ### `ContainerPoolStat` Per-container override-count row from `GET /auth/pools/stats/containers`. ```typescript type ContainerPoolStat = { container: string overriddenConfig?: number [key: string]: unknown } ``` ### `ContainerSettingsEntry` One row of `GET /auth/global/data?type=containerSettings` (verified live 2026-07-01 — the response is a flat array, not the per-Kernel envelope). `thresholds` is keyed by threshold type (`oilTemperature`, `tankPressure`, `waterTemperature`… ```typescript type ContainerSettingsEntry = { model: string site?: string parameters?: Record thresholds?: Record } ``` ### `ContainerThresholdLevels` Threshold band for one container parameter. Which levels are present varies per parameter (verified live: `oilTemperature` carries `alert`/`alarm`, `waterTemperature` carries `alarmLow`/`alarmHigh`). ```typescript type ContainerThresholdLevels = { criticalLow?: number alarmLow?: number alert?: number normal?: number alarm?: number alarmHigh?: number criticalHigh?: number } ``` ### `DeviceAlert` Single alert record carried on a device under `last.alerts`. The list-things endpoint returns alerts as nested arrays — see `getAlertsForDevices` for the canonical extractor. ```typescript type DeviceAlert = { uuid?: string name: string description: string message?: string severity: string createdAt: number | string type?: string } ``` ### `ExtDataParams` Query parameters for `GET /auth/ext-data` — a small key-value gateway the backend exposes for non-tail-log data sources (minerpool, mempool, etc.). ```typescript type ExtDataParams = { /** Provider id — e.g. `minerpool`, `mempool`. */ type: string /** JSON-stringified provider-specific filter. */ query?: string } ``` ### `FeatureConfigResponse` Response from `GET /auth/featureConfig` (note the camelCase path — there is no `/auth/feature-config` route). Typed loosely: the flag set is deployment-specific. The backend may also return multi-site keys (`isMultiSiteModeEnabled`, `siteL… ```typescript type FeatureConfigResponse = { [key: string]: unknown } ``` ### `GlobalDataParams` Query parameters for `GET /auth/global/data`. `type` selects the global data set (e.g. `containerSettings`); `model` optionally narrows container settings to one settings-model (`bd`, `mbt`, `hydro`, `immersion`). ```typescript type GlobalDataParams = { type: string model?: string overwriteCache?: boolean } ``` ### `HashRateLogEntry` Narrowed variant where the hashrate aggregate is present. The dashboard chart components default to this attribute when no `powerAttribute` override is provided. ```typescript type HashRateLogEntry = TailLogEntry & { hashrate_mhs_1m_sum_aggr?: number hashrate_mhs_5m_sum_aggr?: number } ``` ### `HistoricalAlert` A single historical alert row returned by `GET /auth/history-log?logType=alerts`. ```typescript type HistoricalAlert = { uuid?: string name: string description: string message?: string severity: string createdAt: number | string code?: string | number /** Owning device, when th… ``` ### `HistoryLogParams` Query parameters for `GET /auth/history-log` (alerts / info-level history). ```typescript type HistoryLogParams = { logType: 'alerts' | 'info' start?: number end?: number limit?: number offset?: number query?: string } ``` ### `ListRacksParams` Query parameters for `GET /auth/list-racks`. `type` is the worker type (e.g. `miner`, `container`) — omitting it returns `ERR_TYPE_INVALID`. ```typescript type ListRacksParams = { type: string overwriteCache?: boolean } ``` ### `ListThingsDevice` Shape returned by `GET /auth/list-things` for a single device entry. Fields are typed to the union of what the known field projections request (miner explorer, container units, dashboard). Consumers that project fewer fields still satisfy… ```typescript type ListThingsDevice = { id: string type: string status?: string tags?: string[] code?: string rack?: string containerId?: string username?: string address?: string | null err?: stri… ``` ### `ListThingsParams` Query parameters for `GET /auth/list-things`. `query` and `fields` are JSON-stringified Mongo-style selectors. ```typescript type ListThingsParams = { type?: string tag?: string status?: number | string query?: string fields?: string limit?: number offset?: number } ``` ### `LiveAction` A single live action returned by the backend voting queue. ```typescript type LiveAction = { id: string /** The action verb (e.g. `setupPools`, `registerPoolConfig`). */ action?: string type?: string status?: string /** Email/username of the submitte… ``` ### `LiveActionsResponse` Response shape of `GET /auth/actions?queries=…` — a one-element array whose single object maps each requested type to its result list. ```typescript type LiveActionsResponse = { voting?: LiveAction[] ready?: LiveAction[] executing?: LiveAction[] done?: LiveAction[] [key: string]: LiveAction[] | undefined } ``` ### `MinerEntry` A miner row from `GET /auth/miners`, carrying its assigned `poolConfig`. Only the id is guaranteed; the rest is consumed via `lodash.get`. ```typescript type MinerEntry = { id: string [key: string]: unknown } ``` ### `MinerpoolExtDataEntry` Single minerpool ext-data envelope. Backend nests these in `Array<Array<…>>` (per-pool grouping + per-timestamp grouping), so consumers `_head(_head(…))`. ```typescript type MinerpoolExtDataEntry = { ts?: string stats?: PoolMinerStats[] } ``` ### `MinerpoolStatsHistoryEntry` Single sample from the paginated `type=minerpool, key=stats-history` ext-data feed used by the multi-series Hash Rate chart. Each entry carries a timestamp and a snapshot of every configured pool's `hashrate` at that point in time. ```typescript type MinerpoolStatsHistoryEntry = { ts: number stats: PoolMinerStats[] } ``` ### `MinersParams` Query parameters for `GET /auth/miners`. `filter`, `fields`, and `sort` are JSON-stringified Mongo-style selectors; `search` is free text matched across id / code / serial / mac. ```typescript type MinersParams = { filter?: string fields?: string sort?: string search?: string limit?: number offset?: number } ``` ### `MinersResponse` Paginated envelope returned by `GET /auth/miners` — page `data` plus site-wide pagination metadata. ```typescript type MinersResponse = { data: MinerEntry[] totalCount: number offset: number limit: number hasMore: boolean } ``` ### `PduLayoutItem` One PDU row in the static grid layout. `power_w` / `current_a` / `offline` are absent in the static layout and filled from live `pdu_data`. ```typescript type PduLayoutItem = { pdu: string sockets: PduLayoutSocket[] power_w?: number | string current_a?: number | string offline?: boolean } ``` ### `PduLayoutParams` Query parameters for `GET /auth/pdu-layout`. `type` is the full container type string (e.g. `container-bd-d40-m56`). ```typescript type PduLayoutParams = { type: string overwriteCache?: boolean } ``` ### `PduLayoutResponse` Response from `GET /auth/pdu-layout` (verified live 2026-07-01). The backend sources this from the container worker's `pduGridLayout` config, keyed by the exact container type, and 400s with `ERR_PDU_LAYOUT_NOT_FOUND` when no layout is pro… ```typescript type PduLayoutResponse = { type: string layout: PduLayoutItem[] } ``` ### `PduLayoutSocket` One socket in a PDU grid. `enabled` reflects the static layout default; live on/off state comes from the device's `pdu_data` merge. ```typescript type PduLayoutSocket = { socket: string enabled: boolean cooling?: boolean } ``` ### `PoolBalanceHistoryEntry` A single revenue/hashrate sample from `GET /auth/pools/:pool/balance-history`. ```typescript type PoolBalanceHistoryEntry = { ts?: number revenue?: number hashrate?: number /** Settled balance for the bucket (equals `revenue` in the current backend). */ balance?: number [key: string… ``` ### `PoolBalanceHistoryParams` Query parameters for `GET /auth/pools/:pool/balance-history`. The backend requires both `start` and `end` (Unix ms) and rejects the request otherwise; they are typed optional only so a param object can be built incrementally. ```typescript type PoolBalanceHistoryParams = { /** Window start (Unix ms). Required by the backend. */ start?: number /** Window end (Unix ms). Required by the backend. */ end?: number range?: '1D' | '1W'… ``` ### `PoolBalanceHistoryResponse` Response envelope for `GET /auth/pools/:pool/balance-history` — the backend wraps the samples in `{ log }`. ```typescript type PoolBalanceHistoryResponse = { log: PoolBalanceHistoryEntry[] } ``` ### `PoolConfigEntry` Raw pool-configuration row from `GET /auth/configs/pool`. This is the shape the devkit `usePoolConfigs` transform consumes to build a `PoolSummary`. ```typescript type PoolConfigEntry = { id: string poolConfigName: string description: string poolUrls: PoolConfigUrl[] miners: number containers: number updatedAt: string | number } ``` ### `PoolConfigForDeviceResponse` Response for `GET /auth/pools/config/:id` — the device's assigned pool config id and the count of miners overriding their container's config. ```typescript type PoolConfigForDeviceResponse = { poolConfig: string | null overriddenConfig: number } ``` ### `PoolConfigUrl` A single pool-URL endpoint as stored on a pool configuration. `url` is a `stratum+tcp://host:port` string; the devkit `usePoolConfigs` transform parses it into host/port/role for display. ```typescript type PoolConfigUrl = { url: string pool: string workerName?: string workerPassword?: string } ``` ### `PoolMinerStats` Per-pool stats entry returned by `GET /auth/ext-data?type=minerpool`. Each configured pool (`f2pool`, `ocean`, …) contributes one row. ```typescript type PoolMinerStats = { poolType?: string /** Subaccount / user the pool worker submits shares under. */ username?: string /** * Pool-reported hashrate in **H/s** (raw hashes per se… ``` ### `PoolsResponse` Response envelope for `GET /auth/pools` — the aggregated `pools` list plus a site-wide `summary`. The backend wraps the array, so consumers must read `.pools` rather than treating the payload as an array. ```typescript type PoolsResponse = { pools: AggregatedPool[] summary: PoolsSummary } ``` ### `PoolsSummary` Site-wide totals returned alongside the pool list by `GET /auth/pools`. ```typescript type PoolsSummary = { poolCount: number totalHashrate: number totalWorkers: number totalBalance: number } ``` ### `PowerModeTimelineEntry` Power-mode timeline entry — carries grouped per-miner mode/status maps keyed by miner id. Consumed by `PowerModeTimelineChart`. ```typescript type PowerModeTimelineEntry = TailLogEntry & { power_mode_group_aggr?: Record status_group_aggr?: Record } ``` ### `Rack` A single rack entry from `GET /auth/list-racks`. Typed loosely — the `listRacks` RPC response shape has not been captured against a live backend yet (staging returned no rack data at verification time). ```typescript type Rack = { id?: string name?: string [key: string]: unknown } ``` ### `SiteResponse` Response from `GET /auth/site` — the configured site label. ```typescript type SiteResponse = { site: string } ``` ### `SiteStatusAlerts` Alert counts by severity from the live site-status snapshot. ```typescript type SiteStatusAlerts = { critical: number high: number medium: number total: number } ``` ### `SiteStatusLive` Composite live site-status snapshot from `GET /auth/site/status/live`. Aggregates site-wide hashrate, power, efficiency, miner/alert/pool counts, and the snapshot timestamp (`ts`, Unix ms). Polled on a short interval. ```typescript type SiteStatusLive = { hashrate: SiteStatusMetric power: SiteStatusPower efficiency: SiteStatusMetric miners: SiteStatusMiners alerts: SiteStatusAlerts pools: SiteStatusPools /** S… ``` ### `SiteStatusMetric` A `{ value, unit }` measurement, optionally annotated with a `nominal` (rated) value and a `utilization` percentage (`value / nominal * 100`). ```typescript type SiteStatusMetric = { value: number unit: string nominal?: number utilization?: number } ``` ### `SiteStatusMiners` Miner population counts from the live site-status snapshot. ```typescript type SiteStatusMiners = { online: number offline: number error: number total: number containerCapacity: number } ``` ### `SiteStatusPools` Pool-side aggregates from the live site-status snapshot. ```typescript type SiteStatusPools = { totalHashrate: { value: number; unit: string } activeWorkers: number totalWorkers: number } ``` ### `SiteStatusPower` Power metric from the live site-status snapshot. Like `SiteStatusMetric` but with an `alert` message and a hard `error` flag. ```typescript type SiteStatusPower = SiteStatusMetric & { alert?: string error?: boolean } ``` ### `SubmitBatchActionsPayload` Body for `POST /auth/actions/voting/batch` — a set of staged actions plus a client-generated `batchActionUID` the backend uses to group them. ```typescript type SubmitBatchActionsPayload = { batchActionsPayload: VotingActionPayload[] batchActionUID: string suffix?: string /** Batch-level annotations (e.g. `{ isBackFromMaintenance }` on miner move… ``` ### `TailLogEntry` A single time-bucketed entry from `GET /auth/tail-log`. Backend returns `Array<Array<TailLogEntry>>` (outer wrapping is the per-worker grouping — single-site dashboards take `_head(response)`). ```typescript type TailLogEntry = { ts: number /** All other fields are dynamic aggregates requested via `aggrFields`. */ [key: string]: unknown } ``` ### `TailLogMultiParams` Query parameters for `GET /auth/tail-log/multi` — the batched variant of tail-log. `keys` is required (comma-separated `stat-*` keys); the rest mirrors the Fastify schema in the gateway for live sites. ```typescript type TailLogMultiParams = { keys: string start?: number end?: number offset?: number limit?: number fields?: string aggrFields?: string aggrTimes?: string overwriteCache?: boolean } ``` ### `TailLogParams` Query parameters for `GET /auth/tail-log`. Mirrors the Fastify schema in the gateway for live sites. `aggrFields` is a JSON-stringified object describing which aggregate columns to include in each row. ```typescript type TailLogParams = { key: string type?: string tag?: string start?: number end?: number offset?: number limit?: number fields?: string aggrFields?: string aggrTimes?: string merg… ``` ### `ThingCommentBody` Body for `POST | PUT | DELETE /auth/thing/comment` (add / edit / delete a device comment — one Fastify schema covers all three verbs). `rackId` + `thingId` + `comment` are required; `id` and `ts` identify an existing comment for edit/delet… ```typescript type ThingCommentBody = { rackId: string thingId: string comment: string /** Existing comment id — required when editing or deleting. */ id?: string pos?: string ts?: number } ``` ### `ThingConfigParams` Query parameters for `GET /auth/thing-config` — both fields are required by the Fastify schema. ```typescript type ThingConfigParams = { type: string requestType: string } ``` ### `VoteActionPayload` Body for `PUT /auth/actions/voting/:id/vote`. ```typescript type VoteActionPayload = { id: string | number approve: boolean } ``` ### `VotingActionObjectParam` Object-shaped `params[]` entry on a voting submission. Pool create/update carry `{ type: 'pool', data }` (+ `id` for updates); assign-pool carries `{ poolConfigId, configType: 'pool' }`; `switchSocket` carries `{ pdu, socket, enabled }`; `… ```typescript type VotingActionObjectParam = { type?: string id?: string poolConfigId?: string configType?: string data?: Record [key: string]: unknown } ``` ### `VotingActionParam` A single `params[]` entry on a voting submission. Positional and action-specific: device actions carry primitives (`setPowerMode` sends `['sleep']`, `setLED` sends `[true]`, `setTankEnabled` sends `[3, true]`), pool/thing actions carry {@l… ```typescript type VotingActionParam = string | number | boolean | VotingActionObjectParam ``` ### `VotingActionPayload` Body for `POST /auth/actions/:type` (default `voting`). `type` selects the URL segment and is stripped from the JSON body before posting — the remaining fields (`query`, `action`, `params`, `rackType`, …) form the request body. ```typescript type VotingActionPayload = { /** URL segment under `/auth/actions/:type`. Defaults to `voting`. */ type?: string query?: Record action?: string params?: VotingActionPara… ``` # Devkit general (/reference/ui/types/devkit-general) General type definitions from the react-devkit package. ## Package `@tetherto/mdk-react-devkit` ## Types @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `ActionButtonProps` ```typescript type ActionButtonProps = { /** Button label text */ label?: string /** Shows a spinner on the trigger button */ loading?: boolean /** Disables the trigger button */ disabled?: boolean /** Additional class for the trigger button */ className?: string /** * Visual s… ``` ### `ActualEbitdaCardProps` ```typescript type ActualEbitdaCardProps = { value: number } ``` ### `AddSparePartModalProps` Props for `AddSparePartModal`; option lists and handlers are supplied by the caller ```typescript type AddSparePartModalProps = { /** Whether the modal is open */ isOpen: boolean /** Called when the modal requests to close */ onClose: VoidFunction /** Part-type tabs */ partTypes: SparePartSubTypesModalPartType[] /** Initially selected part type */ defaultPartTypeId… ``` ### `AddUserModalProps` ```typescript type AddUserModalProps = { /** Controls whether the dialog is visible */ open: boolean /** Called when the user dismisses the modal (cancel or backdrop) */ onClose: VoidFunction /** List of assignable roles rendered in the role select drop-down */ roles: RoleOptio… ``` ### `AlarmsBellButtonProps` ```typescript type AlarmsBellButtonProps = { /** * Severity-bucketed alarm counts rendered in the stacked badge. * @default {} */ counts?: AlarmsBellButtonCounts /** Click handler — typically opens an alerts panel or routes to /alerts. */ onClick?: (event: MouseEvent['data'] /** Chart.js options - merged with defaults */ options?: ChartJS<'line'>['options'] /** Custom HTML tooltip configuration. When provided, replaces the default… ``` ### `ArrowIconProps` ```typescript type ArrowIconProps = { isOpen?: boolean } & IconProps ``` ### `AssignPoolModalProps` ```typescript type AssignPoolModalProps = { /** Controls modal visibility */ isOpen: boolean /** Called when the modal is dismissed (× button or backdrop) */ onClose: () => void /** Called with the selected pool when the form is submitted */ onSubmit: (values: { pool: PoolSummary… ``` ### `AverageDowntimeChartProps` ```typescript type AverageDowntimeChartProps = Partial<{ /** * Chart title (unit renders on its own line below) * @default "Monthly Average Downtime" */ title: string /** * Unit subtitle under the title * @default % */ unit: string /** * Chart height in pixels * @default 280 */ height:… ``` ### `AvgAllInCostChartProps` ```typescript type AvgAllInCostChartProps = { data?: ReadonlyArray dateRange: FinancialDateRange | null isLoading?: boolean } ``` ### `BadgeProps` ```typescript type BadgeProps = { /** * Badge content (wraps children with badge) */ children?: ReactNode /** * Number to display in badge * If > overflowCount, will show "overflowCount+" * @default 0 */ count?: number /** * Maximum count to display * @default 99 */ over… ``` ### `BarChartProps` ```typescript type BarChartProps = { /** Chart data - required, provided by parent. Use `as any` for mixed bar+line datasets. */ data: any /** Chart.js options - merged with defaults */ options?: ChartJS<'bar'>['options'] /** * Stack bars on top of each other * @default fal… ``` ### `BatchMoveSparePartsModalProps` Props for `BatchMoveSparePartsModal`; unselected location/status arrive as `null` on submit ```typescript type BatchMoveSparePartsModalProps = { /** Whether the modal is open */ isOpen: boolean /** Called when the modal requests to close */ onClose: VoidFunction /** The parts to move, rendered in the table */ spareParts: BatchMoveSparePart[] /** New-location options */ locationOp… ``` ### `BitcoinPriceCardProps` ```typescript type BitcoinPriceCardProps = { value: number } ``` ### `BitcoinProducedCardProps` ```typescript type BitcoinProducedCardProps = { value: number } ``` ### `BitcoinProducedChartProps` ```typescript type BitcoinProducedChartProps = { chartData: BarChartDataResult isLoading?: boolean hasAllZeros?: boolean height?: number } ``` ### `BitcoinProductionCostCardProps` ```typescript type BitcoinProductionCostCardProps = { value: number } ``` ### `BitMainControlsTabProps` ```typescript type BitMainControlsTabProps = { /** Device data */ data: Device } ``` ### `BitMainHydroSettingsProps` ```typescript type BitMainHydroSettingsProps = { /** Device data */ data?: Device } ``` ### `BitMainImmersionSummaryBoxProps` ```typescript type BitMainImmersionSummaryBoxProps = { /** Live device object from the devices store. Returns `null` when omitted. */ data?: Device /** * Optional threshold configuration that drives colour/flash states on temperature stats * @default null */ containerSettings?: BitMainImmers… ``` ### `BreadcrumbsProps` ```typescript type BreadcrumbsProps = { /** Ordered trail; the last item is rendered as current */ items: BreadcrumbItem[] /** * Show a leading "Back" button * @default false */ showBack?: boolean /** * Label for the back button * @default "Back" */ backLabel?: string /** Root… ``` ### `BtcAveragePriceProps` ```typescript type BtcAveragePriceProps = Partial<{ /** * BTC price in USD; formatted with grouping and no decimal places. * When `null`, `undefined`, non-finite, or negative, the value shows `-` (`FALLBACK` from format utils). */ price: number | null /** * Label for the BTC avera… ``` ### `BulkAddSparePartsModalProps` Props for `BulkAddSparePartsModal`; `onSubmit` receives the parsed CSV records ```typescript type BulkAddSparePartsModalProps = { /** Whether the modal is open */ isOpen: boolean /** Called when the modal requests to close */ onClose: VoidFunction /** Submit handler; return `{ error }` to show an inline error */ onSubmit: (records: CSVRecord[]) => Promise<{ error?:… ``` ### `ButtonProps` Props for `Button`. Extends all native `<button>` attributes ```typescript type ButtonProps = Partial< { /** * Show a spinner instead of the content and disable the button. * @default false */ loading: boolean /** * Make the button stretch to fill its container. * @default false */ fullWidth: boolean /** Icon node rendered alongsid… ``` ### `CabinetDetailCardProps` ```typescript type CabinetDetailCardProps = { /** Cabinet display title (`LV Cabinet 1` / transformer title). */ title: string /** Non-root powermeter reading rows. */ powerMeters: CabinetReadingRow[] /** The cabinet-root temperature reading, when present. */ rootTempSensor?: Cabine… ``` ### `CardBodyProps` Props for `CardBody` slot. Forwards all native `<div>` attributes ```typescript type CardBodyProps = HTMLAttributes ``` ### `CardFooterProps` Props for `CardFooter` slot. Forwards all native `<div>` attributes ```typescript type CardFooterProps = HTMLAttributes ``` ### `CardHeaderProps` Props for `CardHeader` slot. Forwards all native `<div>` attributes ```typescript type CardHeaderProps = HTMLAttributes ``` ### `CascaderProps` Cascader component props ```typescript type CascaderProps = { /** * Hierarchical options to display in the cascader * Parent options with children appear in the left panel * Child options appear in the right panel when parent is selected */ options: CascaderOption[] /** * Current selected value(s)… ``` ### `ChangeConfirmationModalProps` ```typescript type ChangeConfirmationModalProps = { /** Controls whether the dialog is visible */ open: boolean /** Dialog header title */ title: string /** Called when the user clicks the confirm button */ onConfirm: VoidFunction /** Called when the user cancels or dismisses the dialog *… ``` ### `ChartContainerProps` ```typescript type ChartContainerProps = { /** Chart heading (renders as `

` unless `header` is provided) */ title?: string /** * Optional node rendered immediately after the title text (e.g. an info * tooltip). Only shown when `title` is set and `header` is not. Additive - *… ``` ### `ChartExpandActionProps` ```typescript type ChartExpandActionProps = { /** Whether the parent chart is currently expanded to full width. */ isExpanded: boolean /** Toggles the expanded state. */ onToggle?: VoidFunction } ``` ### `ChartStatsFooterProps` ```typescript type ChartStatsFooterProps = Partial<{ /** Min/Max/Avg values row */ minMaxAvg: MinMaxAvgValues /** Additional stats displayed in a columnar grid */ stats: ChartStatsFooterItem[] /** * Number of stat items per column (default: 1) * @default 1 */ statsPerColumn: number… ``` ### `CheckboxProps` ```typescript type CheckboxProps = { // Key controlled-state props re-declared from the Radix root so they surface in // the generated docs (types reuse Radix's via indexed access — cannot drift). /** Controlled checked state */ checked?: CheckboxRootProps['checked'] /** Un… ``` ### `ConfirmDeleteSparePartModalProps` Props for `ConfirmDeleteSparePartModal` ```typescript type ConfirmDeleteSparePartModalProps = { /** Whether the modal is open */ isOpen?: boolean /** Called when the modal requests to close */ onClose?: VoidFunction /** Called with the part when the user confirms */ onConfirm?: (sparePart: ConfirmDeleteSparePartModalSparePart) => P… ``` ### `ContainerChartsProps` ```typescript type ContainerChartsProps = { /** When false, shows an empty state (feature gate). @default true */ featureEnabled?: boolean /** * Message when `featureEnabled` is false * @default 'Container Charts feature is not enabled' */ disabledMessage?: string /** Options for… ``` ### `ContainerControlsBoxProps` ```typescript type ContainerControlsBoxProps = { /** The container device object */ data?: Device /** * When `true`, operates on `selectedDevices` instead of a single `data` record * @default false */ isBatch?: boolean isCompact?: boolean // --- data from outside (no API calls inside)… ``` ### `ContainerDetailProps` ```typescript type ContainerDetailProps = { /** * Container display name shown in the header. Optional — omit it when the * host already renders the container name as the page title (e.g. the shell's * `PageLayout`), so the name is not shown twice. */ name?: ReactNode /** Ordered… ``` ### `ContainerWidgetCardProps` ```typescript type ContainerWidgetCardProps = { /** Container display name shown in the header row. */ title: string /** Latest container power draw in watts (rendered in kW by the top row). */ power?: number /** Power unit label shown next to the reading. */ powerUnit?: string /** Pe… ``` ### `ContainerWidgetsProps` ```typescript type ContainerWidgetsProps = { /** Card-ready data for every container, shaped by the data hook. */ containers: ContainerWidgetItem[] /** Section heading. */ title?: string /** * Shows a spinner while the first load is in flight. * @default false */ isLoading?: boolea… ``` ### `CostChartsProps` ```typescript type CostChartsProps = { costLog: ReadonlyArray btcPriceLog: ReadonlyArray totals: CostSummaryMonetaryTotals | null dateRange: FinancialDateRange | null avgAllInCostData?: ReadonlyArray isLoadi… ``` ### `CostContentProps` ```typescript type CostContentProps = CostViewModelProps & CostQueryStateProps & { /** Active date range; drives x-axis labels across all charts */ dateRange: FinancialDateRange | null /** Optional revenue/cost time-series for the Avg All-in Cost panel. */ avgAllInCostData?: R… ``` ### `CostMetricsProps` ```typescript type CostMetricsProps = { metrics: CostSummaryDisplayMetrics } ``` ### `CostProps` ```typescript type CostProps = CostContentProps & CostChromeProps ``` ### `CurrentAlertsProps` ```typescript type CurrentAlertsProps = { /** * Devices carrying alerts (`last.alerts`), as a flat list. Unwrapping any * backend envelope is the data layer's job, not this component's. * Shape mirrors the API response from the source app. */ devices?: Device[] /** * Show DataTa… ``` ### `DashboardDateRangePickerProps` ```typescript type DashboardDateRangePickerProps = { /** Current range as `{ start, end }` epoch-millisecond timestamps. */ value: DashboardDateRange /** Fires with the next `{ start, end }` window when the user applies a range. */ onChange: (next: DashboardDateRange) => void /** * Display… ``` ### `DataLabelProps` ```typescript type DataLabelProps = Partial<{ /** * Range start; formatted in the active timezone (`dd/MM/yy`). */ startDate: Date | null /** * Range end; formatted in the active timezone (`dd/MM/yy`). */ endDate: Date | null /** * Label text; defaults to `PERIOD`. * @defaul… ``` ### `DataTableProps` ```typescript type DataTableProps = { /** * The data to be shown in the table. See https://tanstack.com/table/v8/docs/guide/data */ data: I[] /** * The column configuration table. See https://tanstack.com/table/v8/docs/guide/column-defs */ columns: DataTableColumnDef… ``` ### `DatePickerProps` ```typescript type DatePickerProps = { /** * Currently selected date */ selected?: Date /** * Callback when date changes */ onSelect?: (date: Date | undefined) => void /** * Placeholder text when no date is selected * @default "Pick a date" */ placeholder?: string /** * Date… ``` ### `DateRangePickerProps` ```typescript type DateRangePickerProps = { /** * Selected date range */ selected?: DateRange /** * Callback when date range changes */ onSelect?: (range: DateRange | undefined) => void /** * Placeholder text when no range is selected * @default "Pick a date range" */ placeholder?… ``` ### `DetailLegendProps` ```typescript type DetailLegendProps = { /** Legend items to display */ items: DetailLegendItem[] /** Callback when a legend item is toggled */ onToggle?: (label: string, index: number) => void /** Custom class name */ className?: string } ``` ### `DeviceExplorerProps` ```typescript type DeviceExplorerProps = { /** Active device-type tab */ deviceType: DeviceExplorerDeviceType /** Additional class names */ className?: string /** Controlled row-selection state */ selectedDevices?: DataTableRowSelectionState /** Setter for row selection */ onSele… ``` ### `DialogContentProps` ```typescript type DialogContentProps = { /** Renders `DialogTitle` (and optional `DialogDescription`) inside the header */ title?: string /** Renders `DialogDescription` below the title */ description?: string /** * Whether clicking the overlay closes the dialog * @default true… ``` ### `DialogHeaderProps` ```typescript type DialogHeaderProps = { /** * Applies `mdk-dialog__header--bare` to the header * @default false */ bare?: boolean /** Shows an ✕ close button in the header */ closable?: boolean /** Fired when the ✕ button is clicked */ onClose?: VoidFunction } ``` ### `DividerProps` ```typescript type DividerProps = { /** * Line orientation * @default 'horizontal' */ orientation?: DividerOrientation /** * Line style * @default false */ dashed?: boolean /** * Renders a dotted line (takes precedence over `dashed`) * @default false */ dotted?: boolean /*… ``` ### `DoughnutChartProps` ```typescript type DoughnutChartProps = { /** Array of labelled slices */ data: DoughnutChartDataset[] /** * Unit suffix appended to values in tooltips and legends * @default '' */ unit?: string /** Chart.js options – merged with defaults */ options?: ChartJS<'doughnut'>['option… ``` ### `EbitdaChartsProps` ```typescript type EbitdaChartsProps = { showEbitdaBarChart: boolean ebitdaChartData: BarChartDataResult btcDisplayData: BarChartDataResult isLoading: boolean hasBtcProducedAllZeros: boolean } ``` ### `EbitdaHodlCardProps` ```typescript type EbitdaHodlCardProps = { value: number currentBTCPrice: number } ``` ### `EbitdaMetricsProps` ```typescript type EbitdaMetricsProps = { metrics: EbitdaDisplayMetrics currentBTCPrice: number } ``` ### `EbitdaProps` ```typescript type EbitdaProps = { /** Computed EBITDA metrics */ metrics: EbitdaDisplayMetrics | null /** Data for the EBITDA bar chart */ ebitdaChartInput: ToBarChartDataInput | null /** Data for the BTC produced chart */ btcProducedChartInput: ToBarChartDataInput | nul… ``` ### `EbitdaSellingCardProps` ```typescript type EbitdaSellingCardProps = { value: number } ``` ### `EfficiencyMinerTypeViewProps` ```typescript type EfficiencyMinerTypeViewProps = Omit ``` ### `EfficiencyMinerUnitViewProps` ```typescript type EfficiencyMinerUnitViewProps = Omit ``` ### `EfficiencySiteViewProps` ```typescript type EfficiencySiteViewProps = { /** * Efficiency log entries * @default [] */ log?: MetricsEfficiencyLogEntry[] /** * Average efficiency value * @default null */ avgEfficiency?: number | null /** * Nominal target efficiency * @default null */ nominalValue?: number | nu… ``` ### `EmptyStateProps` ```typescript type EmptyStateProps = { /** * Description text or ReactNode displayed below the image */ description: ReactNode /** * Image to display. Use "default" for the standard illustration, * "simple" for a minimal icon, or pass a custom ReactNode. * @default "default"… ``` ### `EnabledDisableToggleProps` ```typescript type EnabledDisableToggleProps = { /** Current state. A boolean drives a switch display; non-boolean shows action buttons. */ value: unknown /** Tank identifier used in the label (`Tank {N} Circulation`). Pass an empty string for the air exhaust label. */ tankNumber: numb… ``` ### `EnergyBalanceCostChartsProps` ```typescript type EnergyBalanceCostChartsProps = { costChartData: BarChartDataResult btcUnit: EnergyCostChartInput['btcUnit'] powerChartInput: ThresholdLineChartInput displayMode: DisplayMode barLabelFormatter: (v: number) => string onDisplayModeChange: (mode: DisplayMode) => void /** Sh… ``` ### `EnergyBalanceCostMetricsProps` ```typescript type EnergyBalanceCostMetricsProps = { metrics: EnergyCostMetrics } ``` ### `EnergyBalancePowerChartProps` ```typescript type EnergyBalancePowerChartProps = { /** * Chart height when `fillHeight` is false * @default 280 */ height?: number /** * Stretch the panel and chart to fill a mosaic cell (uses height `320` and `mdk-energy-balance__panel--fill`). Used on the revenue tab power column in `E… ``` ### `EnergyBalanceProps` ```typescript type EnergyBalanceProps = { /** All display state: chart inputs, metrics, active tab, display modes, loading/error flags. Returned directly by `useEnergyBalanceViewModel`. */ viewModel: EnergyBalanceViewModel /** Called when the user switches between Revenue and Co… ``` ### `EnergyBalanceRevenueChartsProps` ```typescript type EnergyBalanceRevenueChartsProps = { revenueChartData: BarChartDataResult averageDowntimeData: AverageDowntimeChartData powerChartInput: ThresholdLineChartInput displayMode: DisplayMode barLabelFormatter: (v: number) => string onDisplayModeChange: (mode: DisplayMode) => voi… ``` ### `EnergyBalanceRevenueMetricsProps` ```typescript type EnergyBalanceRevenueMetricsProps = { metrics: EnergyRevenueMetrics } ``` ### `EnergyCostChartProps` ```typescript type EnergyCostChartProps = { chartData: BarChartDataResult btcUnit: EnergyCostChartInput['btcUnit'] displayMode: DisplayMode barLabelFormatter: (v: number) => string onDisplayModeChange: (mode: DisplayMode) => void height?: number } ``` ### `EnergyMetricCardProps` ```typescript type EnergyMetricCardProps = { /** Metric label shown on the card */ name: string /** Metric value, formatted via `formatNumber` */ value: number /** Unit suffix shown next to the value */ unit: string /** Text shown when `value` can't be formatted */ fallback?: strin… ``` ### `EnergyReportMinerTypeViewProps` ```typescript type EnergyReportMinerTypeViewProps = EnergyReportGroupedBarViewProps ``` ### `EnergyReportMinerUnitViewProps` ```typescript type EnergyReportMinerUnitViewProps = EnergyReportGroupedBarViewProps ``` ### `EnergyReportProps` ```typescript type EnergyReportProps = { defaultTab?: EnergyReportTabValue siteView?: Omit & { dateRange?: EnergyReportDateRange } minerTypeView?: EnergyReportMinerTypeViewProps minerUnitView?: EnergyReportMinerUnitViewProps className?: s… ``` ### `EnergyReportSiteViewProps` ```typescript type EnergyReportSiteViewProps = Omit & { snapshotLoading?: boolean onRefetchSnapshot?: VoidFunction dateRange: EnergyReportDateRange onDateRangeChange?: (range: EnergyReportDateRange) => void } ``` ### `EnergyRevenueChartProps` ```typescript type EnergyRevenueChartProps = { chartData: BarChartDataResult displayMode: DisplayMode barLabelFormatter: (v: number) => string onDisplayModeChange: (mode: DisplayMode) => void height?: number } ``` ### `ErrorCardProps` ```typescript type ErrorCardProps = { /** * Error message string. Supports `\n` for line breaks. */ error: string /** * Title displayed above the error message * @default "Errors" */ title?: string /** * Display variant. "card" shows a bordered container, "inline" shows flat… ``` ### `ExplorerDetailProps` ```typescript type ExplorerDetailProps = { /** The active Explorer tab — selects which per-type panel renders. */ deviceType: DeviceExplorerDeviceType /** * Router navigate used by alarm rows to deep-link into `/alerts/:id`. * @default no-op */ onNavigate?: (path: string) => void… ``` ### `ExplorerLayoutProps` ```typescript type ExplorerLayoutProps = { /** Page heading; nothing renders when omitted */ title?: string /** Optional header controls (export button, etc.) shown next to the title. */ headerActions?: ReactNode /** The list column — typically a tab switch plus the device/contai… ``` ### `ExportButtonProps` ```typescript type ExportButtonProps = { /** Fires with the chosen format when the user picks an item. */ onExport: (format: ExportFormat) => void /** * Formats to offer in the dropdown — defaults to `['csv', 'json']`. * @default ['csv', 'json'] */ formats?: readonly ExportForm… ``` ### `FeatureFlagsSettingsProps` ```typescript type FeatureFlagsSettingsProps = { /** Current flag values keyed by flag name */ featureFlags: Record /** Whether editing is permitted */ isEditingEnabled: boolean /** * Show loading state * @default false */ isLoading?: boolean /** * Show saving spinner… ``` ### `FormCascaderProps` ```typescript type FormCascaderProps = BaseFormFieldProps & { options: CascaderOption[] multiple?: boolean cascaderProps?: Omit< React.ComponentProps, 'value' | 'onChange' | 'options' | 'placeholder' > } ``` ### `FormCheckboxProps` ```typescript type FormCheckboxProps = BaseFormFieldProps & { checkboxProps?: React.ComponentProps layout?: 'row' | 'column' } ``` ### `FormDatePickerProps` ```typescript type FormDatePickerProps = BaseFormFieldProps & { datePickerProps?: Omit, 'selected' | 'onSelect'> } ``` ### `FormInputProps` ```typescript type FormInputProps = BaseFormFieldProps & { type?: React.ComponentProps['type'] variant?: React.ComponentProps['variant'] inputProps?: Omit, 'type' | 'variant'> } ``` ### `FormProps` Form wrapper that provides react-hook-form context to child components ```typescript type FormProps = Omit< ComponentProps<'form'>, 'children' > & { form: UseFormReturn children: ReactNode } ``` ### `FormRadioGroupProps` ```typescript type FormRadioGroupProps = BaseFormFieldProps & { options: FormRadioOption[] orientation?: 'horizontal' | 'vertical' radioGroupProps?: Omit< React.ComponentProps, 'onValueChange' | 'defaultValue' | 'orientation' > } ``` ### `FormSelectProps` ```typescript type FormSelectProps = BaseFormFieldProps & { options: FormSelectOption[] selectProps?: Omit, 'onValueChange' | 'defaultValue'> } ``` ### `FormSwitchProps` ```typescript type FormSwitchProps = BaseFormFieldProps & { switchProps?: Omit, 'checked' | 'onCheckedChange'> layout?: 'row' | 'column' } ``` ### `FormTagInputProps` ```typescript type FormTagInputProps = BaseFormFieldProps & { options?: TagInputOption[] allowCustomTags?: boolean variant?: 'default' | 'search' tagInputProps?: Omit< React.ComponentProps, 'value' | 'onTagsChange' | 'label' | 'placeholder'… ``` ### `FormTextAreaProps` ```typescript type FormTextAreaProps = BaseFormFieldProps & { textAreaProps?: React.ComponentProps } ``` ### `GaugeChartProps` ```typescript type GaugeChartProps = { /** Value between 0 and 1 (e.g. 0.75 = 75%). Values outside the range are clamped. */ percent: number /** * Arc colours in HEX format. * @default [COLOR.GREEN, COLOR.RED] */ colors?: string[] /** * Arc thickness as a fraction of the gaug… ``` ### `HashBalanceCostPanelProps` ```typescript type HashBalanceCostPanelProps = HashBalancePanelProps ``` ### `HashBalanceProps` ```typescript type HashBalanceProps = Partial<{ /** * Show error state * @default false */ isError: boolean /** * Show loading state * @default false */ isLoading: boolean /** * Error copy when `isError` * @default 'Error loading hash balance data. Please try again later.' */… ``` ### `HashBalanceRevenuePanelProps` ```typescript type HashBalanceRevenuePanelProps = HashBalancePanelProps & { /** `USD` or `BTC` label for per-PH/day units */ currency: HashBalanceCurrency /** Currency toggle handler */ onCurrencyChange: (currency: HashBalanceCurrency) => void } ``` ### `HashrateMinerTypeViewProps` ```typescript type HashrateMinerTypeViewProps = { /** * Hashrate log grouped by miner type (groupBy=miner). * @default [] */ log?: HashrateGroupedLog /** * Drives the chart spinner * @default false */ isLoading?: boolean /** Selected date range */ dateRange?: HashrateDateRange /** Fires… ``` ### `HashrateMiningUnitViewProps` ```typescript type HashrateMiningUnitViewProps = { /** * Hashrate log grouped by container / mining unit (groupBy=container). * @default [] */ log?: HashrateGroupedLog /** * Drives the chart spinner * @default false */ isLoading?: boolean /** Selected date range */ dateRange?: HashrateDa… ``` ### `HashrateProps` ```typescript type HashrateProps = { /** * Tab selected on first render. Defaults to the Site View. * @default "site-view" */ defaultTab?: HashrateTabValue /** Props forwarded to the Site View tab. */ siteView?: HashrateSiteViewProps /** Props forwarded to the Miner Type Vi… ``` ### `HashrateSiteViewProps` ```typescript type HashrateSiteViewProps = { /** * Hashrate log grouped by miner type. * @default [] */ log?: HashrateGroupedLog /** * Loading state - drives the chart spinner. * @default false */ isLoading?: boolean /** Selected date range used by the host to drive the query. */ d… ``` ### `HeaderConsumptionBoxProps` ```typescript type HeaderConsumptionBoxProps = { /** Icon shown next to the stat */ icon?: ReactNode /** Current site-level power consumption, in megawatts. */ valueMw?: number /** * Unit label — defaults to `MW`. * @default MW */ unit?: string /** Additional class names */ className?:… ``` ### `HeaderControlsSettingsProps` ```typescript type HeaderControlsSettingsProps = { /** Current header preference values */ preferences: HeaderPreferences /** * Show loading state * @default false */ isLoading?: boolean /** Called when a preference toggle changes */ onToggle: (key: keyof HeaderPreferences, value: boolea… ``` ### `HeaderEfficiencyBoxProps` ```typescript type HeaderEfficiencyBoxProps = { /** Icon shown next to the stat */ icon?: ReactNode /** Efficiency in watts per TH/s. */ valueWthS?: number /** * Unit label — defaults to `W/TH/S`. * @default W/TH/S */ unit?: string /** Additional class names */ className?: string } ``` ### `HeaderHashrateBoxProps` ```typescript type HeaderHashrateBoxProps = { /** Icon shown next to the stat */ icon?: ReactNode /** App-side aggregate hashrate in PH/s. */ appPhs?: number /** Pool-side aggregate hashrate in PH/s. */ poolPhs?: number /** * Hashrate unit label — defaults to `PH/s`. * @default PH/s… ``` ### `HeaderMinersBoxProps` ```typescript type HeaderMinersBoxProps = { /** Icon shown next to the "Miners" label. Caller-provided so the package stays icon-agnostic. */ icon?: ReactNode /** Total miners across the site (denominator of the `158 / 2,188` ratio). */ total?: number /** Online miners (the `158`… ``` ### `HeaderStatsBarProps` ```typescript type HeaderStatsBarProps = { /** Stat boxes to render in order, left-to-right. */ children: ReactNode /** Optional class hook. */ className?: string } ``` ### `HeatmapLegendProps` ```typescript type HeatmapLegendProps = { /** Value (or pre-formatted label) at the low end of the scale. */ min: number | string /** Value (or pre-formatted label) at the high end of the scale. */ max: number | string /** Unit suffix appended to `min`/`max`. */ unit?: string /*… ``` ### `HeatmapProps` ```typescript type HeatmapProps = { /** Rows of cells (row-major). Rows may be ragged. */ data: HeatmapCell[][] /** * Range floor (maps to the first gradient stop); auto-derived from the finite values when omitted. * @default auto */ min?: number /** * Range ceiling (maps… ``` ### `HistoricalAlertsProps` ```typescript type HistoricalAlertsProps = { /** * Pre-fetched historical alerts log entries (each with a `thing` device payload). * @default [] */ alerts?: Alert[] /** * Show DataTable loading overlay * @default false */ isLoading?: boolean /** * Filters and search tags coming fro… ``` ### `ImportExportSettingsProps` ```typescript type ImportExportSettingsProps = { /** Trigger configuration export */ onExport: VoidFunction /** Apply imported configuration */ onImport: (data: SettingsExportData) => void /** Custom file-parsing function */ onParseFile?: (file: File) => Promise /**… ``` ### `IndicatorProps` ```typescript type IndicatorProps = { /** * Color variant of the indicator * @default 'gray' */ color?: IndicatorColor /** * Size variant of the indicator * @default 'md' */ size?: ComponentSize /** * Custom className for the root element */ className?: string /** * When tru… ``` ### `InputProps` ```typescript type InputProps = Omit, 'prefix' | 'size'> & { /** * Optional label displayed above the input */ label?: string /** * HTML id for the input. Required when using label for accessibility. * @default auto-generated */ id?: string /** *… ``` ### `LabeledCardProps` ```typescript type LabeledCardProps = Partial<{ /** * Applies a dark background modifier * @default false */ isDark: boolean /** Additional class for the root element */ className: string /** * Prevents content from wrapping * @default false */ hasNoWrap: boolean /** * Sets `p… ``` ### `LineChartCardProps` ```typescript type LineChartCardProps = Partial<{ /** Pre-adapted chart data (use this OR rawData+dataAdapter) */ data: LineChartCardData /** Raw data to be transformed by dataAdapter */ rawData: unknown /** Adapter to transform rawData into LineChartCardData */ dataAdapter: (da… ``` ### `LoaderProps` ```typescript type LoaderProps = { /** * Size of each dot in pixels * @default 10 */ size?: number /** * Number of dots to display * @default 5 */ count?: 3 | 5 | 7 /** * Color variant of the loader * @default 'orange' */ color?: 'red' | 'gray' | 'blue' | 'amber' | 'orang… ``` ### `LogActivityIconProps` ```typescript type LogActivityIconProps = { status: string } ``` ### `LogDotProps` ```typescript type LogDotProps = { /** `'Incidents'` renders a colored circle; `'Activity'` renders an activity icon */ type: string /** Severity string for color mapping */ status: string } ``` ### `LogItemProps` ```typescript type LogItemProps = { data: LogData onLogClicked?: (uuid: string) => void } ``` ### `LogRowProps` ```typescript type LogRowProps = { /** Log entry data */ log: LogData /** Log type (controls dot appearance) */ type: string /** Inline style for the row container */ style?: CSSProperties /** Click handler */ onLogClicked?: (uuid: string) => void } ``` ### `LogsCardProps` ```typescript type LogsCardProps = Partial<{ /** Log type (`'Incidents'` or `'Activity'`); controls the `LogDot` appearance */ type: string /** Card header label */ label: string /** * Applies dark card theme * @default false */ isDark: boolean /** * Shows skeleton rows whi… ``` ### `ManageUserModalProps` ```typescript type ManageUserModalProps = { /** Controls dialog visibility */ open: boolean /** Called when the dialog closes */ onClose: VoidFunction /** The user being edited */ user: SettingsUser /** Available role options */ roles: RoleOption[] /** Permission levels per role *… ``` ### `MdkWordmarkProps` ```typescript type MdkWordmarkProps = { /** * Visual size of the wordmark. `sm` ≈ 24px tall, `md` ≈ 32px, `lg` ≈ 64px * @default 'md' */ size?: MdkWordmarkSize /** Optional class hook on the outer ``. */ className?: string /** * Accessible label * @default 'MDK' */ title?… ``` ### `MicroBTWidgetBoxProps` ```typescript type MicroBTWidgetBoxProps = { /** Live device object. Returns `null` when omitted. */ data?: Device } ``` ### `MinersSummaryBoxProps` ```typescript type MinersSummaryBoxProps = { /** Array of label-value pairs to display in a 2-column grid */ params: MinersSummaryParam[] /** Additional CSS class name */ className?: string } ``` ### `MiningPoolsPanelProps` ```typescript type MiningPoolsPanelProps = Partial<{ /** * Override the card title — defaults to `Mining Pools`. * @default "Mining Pools" */ label: string /** * Hide the title row entirely. * @default false */ hideHeader: boolean /** * Loading state — renders skeleton rows. * @def… ``` ### `MinMaxAvgProps` ```typescript type MinMaxAvgProps = MinMaxAvgValues & { /** Additional root class */ className?: string } ``` ### `MonthlyEbitdaChartProps` ```typescript type MonthlyEbitdaChartProps = { chartData: BarChartDataResult height?: number } ``` ### `MovementDetailsModalProps` Props for `MovementDetailsModal`; pass the selected movement and open/close handlers ```typescript type MovementDetailsModalProps = Partial<{ /** * Whether the modal is open * @default false */ isOpen: boolean /** Called when the modal requests to close (overlay click, escape, or close button) */ onClose: () => void /** The selected movement; when omitted the modal ren… ``` ### `MoveSparePartModalProps` Props for `MoveSparePartModal` ```typescript type MoveSparePartModalProps = { /** Whether the modal is open */ isOpen?: boolean /** Called when the modal requests to close */ onClose?: VoidFunction /** The part to move; when omitted the modal renders nothing */ sparePart?: MoveSparePartModalSparePart /** Pre-seeds… ``` ### `MultiSelectProps` ```typescript type MultiSelectProps = { /** `{ value, label, disabled? }` entries to render as option rows */ options: MultiSelectOption[] /** Controlled selected values. Omit to use `defaultValue` for uncontrolled mode. */ value?: string[] /** * Initial values for uncontrolle… ``` ### `NotFoundPageProps` ```typescript type NotFoundPageProps = { /** * Callback fired when the "Go Home" button is clicked */ onGoHome?: VoidFunction /** * Page title * @default "404" */ title?: string /** * Message displayed below the title * @default "The page you are looking for does not exist." */… ``` ### `OperationalDashboardProps` Props for the operational dashboard composite ```typescript type OperationalDashboardProps = Partial<{ /** Shaped hashrate trend (`LineChartCardData`) */ hashrate: OperationalDashboardTrendInput /** Shaped power-consumption trend */ consumption: OperationalDashboardTrendInput /** Shaped site-efficiency trend */ efficiency: Operati… ``` ### `OperationalMinersStatusChartProps` Props for the miners-status chart component ```typescript type OperationalMinersStatusChartProps = Partial<{ data: MinersStatusChartData isLoading: boolean isExpanded: boolean onToggleExpand: VoidFunction }> ``` ### `OperationsEfficiencyProps` ```typescript type OperationsEfficiencyProps = { /** * Initially selected tab * @default 'site-view' */ defaultTab?: EfficiencyTabValue /** Props forwarded to `EfficiencySiteView` */ siteView?: EfficiencySiteViewProps /** Props forwarded to `EfficiencyMinerTypeView` */ minerTypeView?:… ``` ### `OperationsEnergyChartProps` ```typescript type OperationsEnergyChartProps = { totals: CostSummaryMonetaryTotals | null isLoading?: boolean } ``` ### `OperationsEnergyCostChartProps` ```typescript type OperationsEnergyCostChartProps = Partial<{ /** * Chart title * @default "Operations vs Energy Cost" */ title: string /** * Subtitle and tooltip unit label * @default $/MWh */ unit: string /** * Doughnut height in pixels * @default 200 */ height: number /** Extra class on… ``` ### `PaginationProps` ```typescript type PaginationProps = { /** * Current active page number * @default 1 */ current?: number /** * Total number of items * @default 0 */ total?: number /** * Number of items per page * @default 20 */ pageSize?: number /** * Page size options for the select dropdow… ``` ### `PendingActionsButtonProps` ```typescript type PendingActionsButtonProps = { /** * Click handler override — defaults to toggling the actionsStore sidebar. * @default toggles `actionsStore` sidebar */ onClick?: (event: MouseEvent) => void /** Additional class names */ className?: string } ``` ### `PoolDetailsCardProps` ```typescript type PoolDetailsCardProps = PoolDetailsCardPartialProps & { /** Detail rows to render */ details: PoolDetailItem[] } ``` ### `PoolManagerMinerExplorerProps` ```typescript type PoolManagerMinerExplorerProps = { /** Miners to render in the explorer table. */ miners: ListThingsDevice[] /** Pool configurations powering the "Assign Pool" dropdown. */ poolConfig: PoolConfigData[] /** Called when the operator clicks the "Pool Manager" back link. */ b… ``` ### `PoolManagerPoolsProps` ```typescript type PoolManagerPoolsProps = { /** Array of pool configurations to render. */ poolConfig: PoolConfigData[] /** Called when the operator clicks the "Pool Manager" back link. */ backButtonClick: VoidFunction } ``` ### `PoolManagerProps` ```typescript type PoolManagerProps = { /** Pool configurations shared by every sub-view (Pools, Miner Explorer, Sites). */ poolConfig: PoolConfigData[] /** Dashboard site-level stat blocks. */ stats?: DashboardStats /** Dashboard stats loading flag. */ isStatsLoading?: boolea… ``` ### `PoolManagerSiteOverviewDetailsProps` ```typescript type PoolManagerSiteOverviewDetailsProps = { /** The site (container unit) to render details for. */ unit: UnitData /** Display name shown in the breadcrumb (`Site Overview / `). */ unitName: string /** Pool configurations powering the per-pool detail rows. */ poolConfig:… ``` ### `PoolManagerSitesOverviewProps` ```typescript type PoolManagerSitesOverviewProps = { /** Sites to render (already normalised through `useSitesOverviewData`). */ units: ProcessedContainerUnit[] /** Pool configurations powering each card's pool summary. */ poolConfig: PoolConfigData[] /** * Show a skeleton placeholder whil… ``` ### `PowerModeTimelineChartProps` Props for `PowerModeTimelineChart` ```typescript type PowerModeTimelineChartProps = Partial<{ /** * Initial power-mode entries (each with start/end ts + mode). * @default [] */ data: PowerModeTimelineEntry[] /** * Streaming updates appended to the initial data. * @default [] */ dataUpdates: PowerModeTimelineEntry[] /** *… ``` ### `ProductionCostChartProps` ```typescript type ProductionCostChartProps = { costLog: ReadonlyArray btcPriceLog: ReadonlyArray dateRange: FinancialDateRange | null isLoading?: boolean } ``` ### `ProfileMenuProps` ```typescript type ProfileMenuProps = { /** Items rendered in the dropdown, top-to-bottom. */ items: ProfileMenuItem[] /** Optional user label rendered at the top of the dropdown (e.g. an email). */ user?: ReactNode /** * Override the trigger icon — defaults to the user-avatar… ``` ### `RadioGroupProps` ```typescript type RadioGroupProps = { /** * Layout orientation * @default 'vertical' */ orientation?: 'horizontal' | 'vertical' /** * Remove gap between radio items * @default false */ noGap?: boolean /** * Custom className for the group */ className?: string } & ComponentPr… ``` ### `RadioProps` ```typescript type RadioProps = { // Key prop re-declared from the Radix item so it surfaces in the generated docs // (type reuses Radix's via indexed access — cannot drift). /** Value associated with this option */ value: RadioItemProps['value'] /** * Size variant of th… ``` ### `RBACControlSettingsProps` ```typescript type RBACControlSettingsProps = { /** List of current users */ users: SettingsUser[] /** Available role options */ roles: RoleOption[] /** Permission levels per role */ rolePermissions: Record> /** Display labels for permission keys */ p… ``` ### `RepairLogChangesSubRowProps` ```typescript type RepairLogChangesSubRowProps = { /** * The repair batch action whose part changes should be displayed. */ batchAction: RepairBatchAction /** * Devices referenced by the batch action, pre-fetched by the parent. */ devices: RepairDevice[] /** * Show a spinner while the pa… ``` ### `ReportTimeFrameSelectorProps` ```typescript type ReportTimeFrameSelectorProps = Pick< ReportTimeFrameSelectorState, 'presetTimeFrame' | 'dateRange' | 'setPresetTimeFrame' | 'setDateRange' > ``` ### `RequireAuthProps` ```typescript type RequireAuthProps = { /** Rendered when a token is present. */ children: ReactNode /** Rendered when no token is present — typically ``. */ fallback: ReactNode /** * When true (default), the current location is persisted to sessionSto… ``` ### `RevenueChartProps` Props for `RevenueChart`; pass pre-fetched data and optional legend layout overrides ```typescript type RevenueChartProps = Partial<{ /** * Raw API response, each entry is one time period with site IDs as dynamic keys * @default [] */ data: RevenueDataItem[] /** * Shows a loading spinner while data is being fetched * @default false */ isLoading: boolean /** * L… ``` ### `SelectProps` ```typescript type SelectProps = SelectRootProps & { // Key controlled-state props re-declared from the Radix root so they surface in // the generated docs (types reuse Radix's via indexed access — cannot drift). /** Controlled value */ value?: SelectRootProps['value'] /*… ``` ### `SettingsDashboardProps` ```typescript type SettingsDashboardProps = { /** Danger-zone action buttons (reset, delete) */ dangerActions?: ActionButtonProps[] /** Props forwarded to `HeaderControlsSettings` */ headerControlsProps?: HeaderControlsSettingsProps /** Props forwarded to `RBACControlSettings` */ rb… ``` ### `SidebarProps` ```typescript type SidebarProps = SidebarOptions & SidebarCallbacks & { /** Menu items (supports nested `items`) */ items: SidebarMenuItem[] } ``` ### `SignInGoogleButtonProps` ```typescript type SignInGoogleButtonProps = Omit & { /** * Base URL of the OAuth backend (no trailing slash). Click navigates to * `${oauthBaseUrl}/oauth/google`. */ oauthBaseUrl: string /** * Override the visible button label. * @default "Sign i… ``` ### `SiteStatsBarProps` ```typescript type SiteStatsBarProps = { /** Site label rendered in the header row. */ title: string /** Current site-level power consumption, in watts (or whatever `powerUnit` says). */ power?: number /** * Display unit for `power` — defaults to `kW`. * @default 'kW' */ powerU… ``` ### `SkeletonBlockProps` ```typescript type SkeletonBlockProps = Partial<{ /** * Renders a perfect circle using `height` as the diameter * @default false */ circle: boolean /** Additional class for the element */ className: string /** Width in pixels (number) or any CSS value (string) */ width: number |… ``` ### `SocketProps` ```typescript type SocketProps = { /** * Current in amperes * @default null */ current_a?: number | null /** * Power in watts * @default null */ power_w?: number | null /** * Whether socket is enabled * @default false */ enabled?: boolean /** * Socket number/index * @defa… ``` ### `SparePartSubTypesModalProps` Props for `SparePartSubTypesModal`; the active part type is controlled by the caller ```typescript type SparePartSubTypesModalProps = { /** Whether the modal is open */ isOpen: boolean /** Called when the modal requests to close */ onClose: VoidFunction /** Part-type tabs */ partTypes: SparePartSubTypesModalPartType[] /** The selected part type (controlled by the parent)… ``` ### `SpinnerProps` ```typescript type SpinnerProps = { /** * Size variant of the spinner * @default 'md' */ size?: ComponentSize /** * Color variant of the spinner * @default 'primary' */ color?: 'primary' | 'secondary' /** * Whether to display in fullscreen mode * @default false */ fullScre… ``` ### `SupplyLiquidBoxProps` ```typescript type SupplyLiquidBoxProps = { /** Live device object. Returns `null` when omitted. */ data?: Device /** * Optional threshold map that controls colour and flash states on readings * @default null */ containerSettings?: SupplyLiquidBoxContainerSettings | null } ``` ### `SwitchProps` ```typescript type SwitchProps = { // Key controlled-state props re-declared from the Radix root so they surface in // the generated docs (types reuse Radix's via indexed access — cannot drift). /** Controlled checked state */ checked?: SwitchRootProps['checked'] /** Unco… ``` ### `TagFilterBarProps` ```typescript type TagFilterBarProps = { /** Active tag filter values */ filterTags: string[] /** Current local filter state */ localFilters: AlertLocalFilters /** Called when tag filter changes */ onSearchTagsChange: (tags: string[]) => void /** Called when any local filter ch… ``` ### `TagInputProps` ```typescript type TagInputProps = { /** * Controlled tags (array of tag values) * @default [] */ value?: string[] /** * Callback when tags change (add/remove) */ onTagsChange?: (tags: string[]) => void /** * Callback when input value changes (typing). Receives current inpu… ``` ### `TagProps` ```typescript type TagProps = { /** * Color variant of the tag * @default 'dark' */ color?: 'dark' | 'red' | 'green' | 'amber' | 'blue' /** * Custom className for the root element */ className?: string /** * Children content */ children?: ReactNode } & ComponentPropsWi… ``` ### `TankRowProps` ```typescript type TankRowProps = { /** Tank identifier label (e.g. "Tank 1") */ label: string /** Current temperature value */ temperature: number /** Temperature unit string (e.g. "°C") */ unit: string /** Running state for the oil pump */ oilPumpEnabled: boolean /** Run… ``` ### `TanksBoxProps` ```typescript type TanksBoxProps = { /** Tank telemetry arrays; returns `null` when omitted */ data?: { oil_pump: Tank[] water_pump: WaterPump[] pressure: TanksBoxPressure[] } } ``` ### `TextAreaProps` ```typescript type TextAreaProps = ComponentProps<'textarea'> & { /** * Optional label displayed above the textarea */ label?: string /** * HTML id for the textarea. Required when using label for accessibility. * @default auto-generated */ id?: string /** * Validation error… ``` ### `ThresholdLineChartProps` ```typescript type ThresholdLineChartProps = Partial<{ /** Chart title (unit appended when `unit` is set) */ title: string /** Shown in title and axis/tooltip formatting */ unit: string /** * Chart height in pixels (`360` when `isTall`) * @default 280 */ height: number /** * When tru… ``` ### `TimeframeControlsProps` ```typescript type TimeframeControlsProps = Partial<{ /** Helper text below the controls */ hint: string /** Called when the Reset button is clicked */ onReset: VoidFunction /** * Shows the Reset button * @default false */ showResetButton: boolean /** * Show week selector * @default… ``` ### `TimeframeWeekFlatContentProps` ```typescript type TimeframeWeekFlatContentProps = { visibleWeeks: ReturnType } ``` ### `TimeframeWeekTreeContentProps` ```typescript type TimeframeWeekTreeContentProps = { timezone: string selectedYear: number selectedMonth: number } ``` ### `TimelineChartProps` ```typescript type TimelineChartProps = { /** Initial timeline data */ initialData: TimelineChartData /** Streaming updates appended to the initial data */ newData?: TimelineChartData /** * Ignore `newData` * @default false */ skipUpdates?: boolean /** Visible time window */ ran… ``` ### `TimelineSelectorProps` ```typescript type TimelineSelectorProps = { /** Currently selected timeline value (e.g. `'1m'`, `'5m'`). */ value: string /** Called whenever the user picks a new option. */ onChange: (next: string) => void /** * Available options — defaults to {@link getTimelineOptions}. Pass a c… ``` ### `TypographyProps` ```typescript type TypographyProps = { /** * Determines the rendered HTML element and base style * @default 'body' */ variant?: 'heading1' | 'heading2' | 'heading3' | 'body' | 'secondary' | 'caption' /** * Text size; defaults to the variant's size when unset. */ size?: 'xs' |… ``` ### `WidgetTopRowProps` ```typescript type WidgetTopRowProps = { /** Widget title */ title: string /** Power reading; rendered in kilo-units */ power?: number /** Power unit (e.g. `"kW"`) */ unit?: string /** Error tooltip content; replaces power */ statsErrorMessage?: string | ErrorWithTimestamp[] |… ``` # Foundation dashboard (/reference/ui/types/foundation-dashboard) Type definitions for dashboard data structures. ## Package `@tetherto/mdk-ui-foundation` ## Types @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `ChartCardData` Minimum chart-ready payload — assignable to `LineChartCardData` from `@tetherto/mdk-react-devkit`. ```typescript type ChartCardData = { datasets: ChartDataset[] } & Partial<{ /** Y-axis tick formatter (e.g. `(v) => \`${v.toFixed(2)} MW\``). */ yTicksFormatter: (value: number) => string /** Cr… ``` ### `ChartDataPoint` A single (x, y) sample. `x` is a Unix timestamp in **milliseconds** — the `LineChart` primitive divides it by 1000 to derive the lightweight-charts `UTCTimestamp`. `y` is `null` to render gaps. ```typescript type ChartDataPoint = { x: number y: number | null } ``` ### `ChartDataset` A named line series with colour and points. Optional `visible` lets pages pre-hide datasets without removing them from the data array. ```typescript type ChartDataset = { label?: string borderColor: string data: ChartDataPoint[] visible?: boolean } ``` # Foundation general (/reference/ui/types/foundation-general) General type definitions from the ui-foundation package. ## Package `@tetherto/mdk-ui-foundation` ## Types @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `ApiError` Shared error contract used across HTTP/API surfaces. ```typescript type ApiError = { error: string message: string status: number data?: { message?: string } } ``` ### `ImportResult` ```typescript type ImportResult = { success: boolean applied?: string[] errors?: string[] message?: string } ``` ### `PermLevel` ```typescript type PermLevel = 'rw' | 'r' | false ``` ### `RoleOption` ```typescript type RoleOption = { label: string value: string } ``` ### `RolesPermissionsData` ```typescript type RolesPermissionsData = { permissions: Record> labels: Record } ``` ### `SettingsExportData` ```typescript type SettingsExportData = { headerControls?: Record featureFlags?: Record timestamp?: string version?: string } & TExtra ``` ### `SettingsUser` ```typescript type SettingsUser = { id: string name?: string email: string role: string last_login?: string lastActive?: string [key: string]: unknown } ``` ### `SubscriberCallback` ```typescript type SubscriberCallback = (value: T) => void ``` ### `UnknownRecord` Generic type for objects with unknown structure. ```typescript type UnknownRecord = Record ``` ### `Unsubscribe` ```typescript type Unsubscribe = () => void ``` # Utilities (/reference/ui/utilities) Helper functions, formatters, and utilities exported from `@tetherto/mdk-ui-foundation`. ## Browse by category | Category | Description | |----------|-------------| | [Alerts](/reference/ui/utilities/alerts) | Alert query builders and time-range utilities | | [Auth](/reference/ui/utilities/auth) | Authentication helper functions | | [Dashboard](/reference/ui/utilities/dashboard) | Dashboard data utilities | | [General](/reference/ui/utilities/general) | General-purpose utilities | | [Utils](/reference/ui/utilities/utils) | Core utility functions | ## Import pattern ```tsx ``` # Alert (/reference/ui/utilities/alerts) Utilities for building alert queries and managing time ranges. ## Package `@tetherto/mdk-ui-foundation` ## Utilities @tetherto/mdk-ui-foundation ### Default historical range ```tsx ``` ### When to use `getDefaultHistoricalAlertsRange` Use `getDefaultHistoricalAlertsRange` to initialise an alert-history view with the standard 14-day look-back window. Pass an explicit `now` value when the range must be deterministic, such as in a test. ### `getDefaultHistoricalAlertsRange` example ```tsx const initialRange = getDefaultHistoricalAlertsRange() const testRange = getDefaultHistoricalAlertsRange(1_700_000_000_000) ``` @tetherto/mdk-ui-foundation ### Build the current-alert query ```tsx ``` ### When to use `buildCurrentAlertDevicesParams` Use `buildCurrentAlertDevicesParams` to construct the `list-things` request for devices that currently carry alerts. Pass active search tags when the server should narrow the current-alert result before client-side presentation. ### `buildCurrentAlertDevicesParams` example ```tsx const allCurrentAlerts = buildCurrentAlertDevicesParams() const matchingAlerts = buildCurrentAlertDevicesParams([ 'ip-192.168.1.1', 'sn-ABC123', ]) ``` @tetherto/mdk-ui-foundation ### Time intervals ```tsx ``` #### Related API - [**`fetchHistoricalAlertsInChunks`**](/reference/ui/utilities/alerts/#fetchhistoricalalertsinchunks) - [**`mergeAlertsByUuid`**](/reference/ui/utilities/alerts/#mergealertsbyuuid) ### When to use `breakTimeIntoIntervals` Use `breakTimeIntoIntervals` when a historical request must be split into bounded windows before fetching. Alert history uses this to avoid requesting a large time range in one call. ### Historical-alert chunking workflow Create the windows with `breakTimeIntoIntervals`, fetch them from oldest to newest, then combine overlapping alert results with `mergeAlertsByUuid`. `fetchHistoricalAlertsInChunks` composes that workflow when its default error and abort behaviour fits the caller. ### Range behaviour The function returns an empty array when the bounds are non-finite, when `start >= end`, or when `intervalMs <= 0`. The final interval is clamped to `end`, so it may be shorter than `intervalMs`; callers must not assume that every window has equal duration. ### `breakTimeIntoIntervals` example ```tsx breakTimeIntoIntervals, mergeAlertsByUuid, ONE_DAY_MS, } from '@tetherto/mdk-ui-foundation' let alerts = [] for (const window of breakTimeIntoIntervals(start, end, ONE_DAY_MS)) { const next = await fetchAlerts(window) alerts = mergeAlertsByUuid(alerts, next) } ``` @tetherto/mdk-ui-foundation ### Fetch historical alerts ```tsx ``` #### Related API - [**`fetchHistoricalAlertsInChunks`**](/reference/ui/utilities/alerts/#fetchhistoricalalertsinchunks) - [**`buildHistoricalAlertsParams`**](/reference/ui/utilities/alerts/#buildhistoricalalertsparams) - [**`mergeAlertsByUuid`**](/reference/ui/utilities/alerts/#mergealertsbyuuid) ### When to use the historical-alert workflow Use `fetchHistoricalAlertsInChunks` for a full alert-history range rather than issuing one unbounded request. It coordinates the range splitting and result merging; `buildHistoricalAlertsParams` adapts each window to the Gateway query. ### Historical-alert fetch workflow Start with the requested range, let `fetchHistoricalAlertsInChunks` visit each window from oldest to newest, and build one `history-log` request per callback. The helper uses `mergeAlertsByUuid` between windows. Call `mergeAlertsByUuid` again only when integrating the returned range into alerts already held by the caller. ### Historical-alert failure and abort behaviour Individual window failures are swallowed so one bad request does not discard the rest of the range. An abort signal is checked between windows rather than during the caller's active request; the request function must use that signal as well if it needs in-flight cancellation. ### Historical-alert workflow example ```tsx buildHistoricalAlertsParams, fetchHistoricalAlertsInChunks, mergeAlertsByUuid, } from '@tetherto/mdk-ui-foundation' const fetchedAlerts = await fetchHistoricalAlertsInChunks( range, async (window) => { const params = buildHistoricalAlertsParams(window) const result = await gateway.historyLog(params) return result.data }, { signal: abortController.signal }, ) const allAlerts = mergeAlertsByUuid(existingAlerts, fetchedAlerts) ``` @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `breakTimeIntoIntervals` Split `[start, end]` into consecutive windows of `intervalMs`. The final window is clamped to `end`. Returns an empty array when the range is empty or inverted. Mirrors the reference app's `breakTimeIntoIntervals`. **Function** ```typescript (start: number, end: number, intervalMs: number = ONE_DAY_MS) => TimeInterval[] ``` ### `buildCurrentAlertDevicesParams` `list-things` params for the current-alerts table: every device that currently carries one or more alerts, with the fields the `<CurrentAlerts>` table reads. Consumed by `useCurrentAlertDevices`. **Function** ```typescript (filterTags: string[] = []) => ListThingsParams ``` ### `buildHistoricalAlertsParams` `history-log` params for a single alerts window. The chunked fetch (`useHistoricalAlerts`) calls this once per 24h sub-window. **Function** ```typescript (range: HistoricalAlertsRange) => HistoryLogParams ``` ### `fetchHistoricalAlertsInChunks` Fetch a historical-alerts range as successive 24h windows, merging the results by `uuid`. `fetchWindow` is called once per window (oldest → newest); individual window failures are swallowed (matches the reference app) so one bad window doe… **Function** ```typescript (range: { start: number; end: number }, fetchWindow: (window: TimeInterval) => Promise, options: FetchHistoricalAlertsOptions = {}) => Promise ``` ### `getAlertsForDevices` Flatten an array of devices into a list of incident rows, one per alert. Devices without `last.alerts` are skipped. The output is **not yet sorted**; pair with `sortIncidentsBySeverity` for the final list-view order. **Function** ```typescript (devices: ListThingsDevice[], formatDate: (d: Date) => string = (d) => d.toISOString()) => IncidentRow[] ``` ### `getDefaultHistoricalAlertsRange` Default historical-alerts range: the last `DEFAULT_HISTORICAL_WINDOW_MS` ending now. Used by the devkit `<Alerts>` feature and the shell Alerts page to seed their range state. **Function** ```typescript (now: number = Date.now()) => HistoricalAlertsRange ``` ### `mapDevicesToIncidents` One-shot helper: `devices → sorted rows`. Used by the `useActiveIncidents` hook's `select` projection. **Function** ```typescript (devices: ListThingsDevice[], formatDate?: (d: Date) => string) => IncidentRow[] ``` ### `mapHistoryLogToAlerts` Normalise raw `history-log` alert rows into the shape the devkit `<HistoricalAlerts>` table consumes. The table derives its device label, short code, and filter tokens from each row's `thing` (treated as a device), so this guarantees `thin… **Function** ```typescript (rows: HistoricalAlert[] = []) => HistoricalAlert[] ``` ### `mergeAlertsByUuid` Concatenate `next` onto `prev`, replacing any row that shares a `uuid` (later windows win) and appending the rest. Rows without a `uuid` are always appended. Mirrors the reference app's `updateHistoricalData`. **Function** ```typescript (prev: T[], next: T[]) => T[] ``` ### `sortIncidentsBySeverity` Sort rows by severity (critical → high → medium), then by `id` for deterministic ordering when severities tie. Returns a new array. **Function** ```typescript (rows: IncidentRow[]) => IncidentRow[] ``` # Auth (/reference/ui/utilities/auth) Helper functions for authentication flows. ## Package `@tetherto/mdk-ui-foundation` ## Utilities @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `extractAuthTokenFromUrl` Extract `?authToken=` from a URL search string. Accepts either a full URL, a query string with leading `?`, or a bare query string. **Function** ```typescript (search: string) => string | null ``` ### `gatewayRedirectAuth` Build the mining Gateway auth provider. **Function** ```typescript (options: GatewayRedirectAuthOptions = {}) => AuthProvider ``` ### `stripAuthTokenFromUrl` Build a URL string with the `?authToken=` parameter stripped. Used after the OAuth callback to remove the token from the address bar without losing any other query state. **Function** ```typescript (search: string) => string ``` # Dashboard (/reference/ui/utilities/dashboard) Utilities for dashboard data processing. ## Package `@tetherto/mdk-ui-foundation` ## Utilities @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `buildHashrateTailLogParams` Hashrate tail-log params — per-miner 1-minute aggregate, summed across the `t-miner` tag. **Function** ```typescript (range: DashboardQueryRange) => TailLogParams ``` ### `buildMinerpoolStatsHistoryExtDataParams` Ext-data params for `type=minerpool, key=stats-history` — per-pool hashrate snapshots over time. Pair with `extDataQuery` to feed the multi-series Hash Rate chart (the reference app + Aggr Pool + per-pool lines). **Function** ```typescript (range: MinerpoolStatsHistoryRange = {}) => ExtDataParams ``` ### `buildSiteConsumptionTailLogParams` Site-level consumption tail-log params — reads the dedicated powermeter's `site_power_w` aggregate (the reference app's `type=powermeter, tag=t-powermeter, aggrFields={site_power_w:1}` query). Returns the same series the header's `useSiteP… **Function** ```typescript (range: DashboardQueryRange) => TailLogParams ``` ### `DEFAULT_TIMELINE_OPTIONS` Canonical short-form intervals exposed by MDK UI Shell. The keys map onto the `key=stat-<value>` query parameter expected by `GET /auth/tail-log`. **Constant** ```typescript readonly TimelineOption[] ``` ### `getTimelineOptions` Default options for the dashboard timeline selector. Mirrors the reference app's `timelineRadioButtons` (5m / 30m / 3h / 1D) — the production dashboard doesn't expose `stat-1m` because the backend typically only emits 5-minute and longer a… **Function** ```typescript (opts: { includeOneMinute?: boolean } = {}) => TimelineOption[] ``` ### `normalizeAlertSeverity` Narrow an arbitrary backend severity string to the `AlertSeverity` literal union expected by the `ActiveIncidentsCard` row component. Unknown values fall back to `'medium'` so the row still renders rather than crashing on an unexpected pay… **Function** ```typescript (raw: string | null | undefined) => AlertSeverity ``` ### `readHashrateMhs` Reads the hashrate aggregate from a tail-log entry. The reference app emits `hashrate_mhs_1m_sum_aggr` across every `stat-*` bucket, so a single field check covers all timelines; the `_5m_` legacy fallback is retained as a defensive second… **Function** ```typescript (entry: { hashrate_mhs_1m_sum_aggr?: unknown hashrate_mhs_5m_sum_aggr?: unknown }) => number | undefined ``` ### `SEVERITY_WEIGHT` Severity level → numeric weight. Higher is more urgent. Used for sorting the active-incidents list (most-severe first). **Constant** ```typescript Record ``` # General (/reference/ui/utilities/general) General-purpose helper functions. ## Package `@tetherto/mdk-ui-foundation` ## Utilities @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `ALERT_TYPE_POOL_NAME` **Constant** ```typescript { readonly ip_worker_name: "IP worker name"; readonly wrong_miner_pool: "Wrong miner pool"; readonly wrong_worker_name: "Wrong worker name"; readonly wrong_miner_subaccount: "Wrong miner subaccount";… ``` ### `ALERT_TYPE_POOL_VALUE` **Constant** ```typescript { readonly IP_WORKER_NAME: "ip_worker_name"; readonly WRONG_MINER_POOL: "wrong_miner_pool"; readonly WRONG_WORKER_NAME: "wrong_worker_name"; readonly WRONG_MINER_SUBACCOUNT: "wrong_miner_subaccount";… ``` ### `appendContainerToTag` **Function** ```typescript (deviceId: string) => string ``` ### `appendIdToTag` **Function** ```typescript (deviceId: string) => string ``` ### `appendIdToTags` **Function** ```typescript (deviceIdList: string[]) => string[] ``` ### `AUTH_LEVELS` **Constant** ```typescript { readonly READ: "r"; readonly WRITE: "w"; } ``` ### `AUTH_PERMISSIONS` **Constant** ```typescript { readonly TEMP: "temp"; readonly MINER: "miner"; readonly USERS: "users"; readonly ALERTS: "alerts"; readonly TICKETS: "tickets"; readonly ACTIONS: "actions"; readonly REVENUE: "revenue"; readonly F… ``` ### `AUTH_TOKEN_QUERY_PARAM` Pure URL helpers shared by the auth flow. Kept framework-agnostic so the react-adapter (and any future framework adapter) can call them without pulling in router-specific APIs. **Constant** ```typescript "authToken" ``` ### `buildCabinetDetailParams` List-things params for one LV cabinet's family of devices — the powermeters and temperature sensors whose `info.pos` sits under the cabinet `root`. Mirrors the reference app's `getLvCabinetDevicesByRoot(root)`; the detail hook groups the r… **Function** ```typescript (root: string) => ListThingsParams ``` ### `buildContainerCrossThing` `{ type: 'container', params: { containers } }` fan-out for miner-level actions. **Function** ```typescript (containers: string[]) => DeviceActionCrossThing ``` ### `buildContainerDetailParams` List-things params for the selected containers' detail snapshots. Takes the raw container keys (the `selectedDevicesTags` outer keys / `info.container` names), tags them with `container-` and filters to `t-container` things — mirrors the r… **Function** ```typescript (containerKeys: string[]) => ListThingsParams ``` ### `buildContainerWidgetsListParams` List-things params for the Site Overview container widgets grid. **Function** ```typescript () => ListThingsParams ``` ### `buildContainerWidgetsRealtimeTailLogParams` Tail-log params for the Container Widgets realtime snapshot — the latest `stat-realtime` sample across all miners, grouped so the cards can slice per container. **Function** ```typescript () => TailLogParams ``` ### `buildDeviceActionSubmission` Core submission assembler. Prefer the per-action builders below — they pin each action's param arity/encoding; this is the escape hatch for actions without a dedicated builder. `extras` is spread first so it can never override the pinned s… **Function** ```typescript (action: DeviceActionValue, tags: string[], params: VotingActionParam[] = [], extras: Record = {}) => DeviceActionSubmission ``` ### `buildExplorerListThingsParams` List-things params for one Explorer tab: tag-filtered, status-enriched, projected to `OP_CENTER_LIST_THINGS_FIELDS`. **Function** ```typescript (tab: ExplorerTabValue, options: { limit?: number; offset?: number } = {}) => ListThingsParams ``` ### `buildMinerCrossThing` `{ type: 'miner', params: { containers } }` fan-out for container-level actions. **Function** ```typescript (containers: string[]) => DeviceActionCrossThing ``` ### `buildRebootAction` `reboot` — no params. **Function** ```typescript (tags: string[]) => DeviceActionSubmission ``` ### `buildResetAlarmAction` `resetAlarm` — no params. **Function** ```typescript (tags: string[]) => DeviceActionSubmission ``` ### `buildSetAirExhaustEnabledAction` `setAirExhaustEnabled` — single boolean param. **Function** ```typescript (tags: string[], isOn: boolean) => DeviceActionSubmission ``` ### `buildSetLedAction` `setLED` — single boolean param. **Function** ```typescript (tags: string[], isOn: boolean) => DeviceActionSubmission ``` ### `buildSetPlcRegistersAction` `setPlcRegisters` (Gamma) — single `{ register: value }` map param. **Function** ```typescript (tags: string[], registers: Record) => DeviceActionSubmission ``` ### `buildSetPowerModeAction` `setPowerMode` — single power-mode param, optional container fan-out. **Function** ```typescript (tags: string[], mode: PowerModeValue, crossThing?: DeviceActionCrossThing) => DeviceActionSubmission ``` ### `buildSetPowerPctAction` `setPowerPct` — percentage encoded as a string, optional container fan-out. **Function** ```typescript (tags: string[], percentage: number, crossThing?: DeviceActionCrossThing) => DeviceActionSubmission ``` ### `buildSetTankEnabledAction` `setTankEnabled` — positional `[tankNumber, isOn]`. **Function** ```typescript (tags: string[], tankNumber: number, isOn: boolean) => DeviceActionSubmission ``` ### `buildSwitchContainerAction` `switchContainer` — single boolean param. **Function** ```typescript (tags: string[], isOn: boolean) => DeviceActionSubmission ``` ### `buildSwitchCoolingSystemAction` `switchCoolingSystem` — single boolean param, optional miner fan-out. **Function** ```typescript (tags: string[], isOn: boolean, crossThing?: DeviceActionCrossThing) => DeviceActionSubmission ``` ### `buildSwitchSocketAction` `switchSocket` — one `{ pdu, socket, enabled }` param per toggled socket. **Function** ```typescript (tags: string[], sockets: SocketSwitch[]) => DeviceActionSubmission ``` ### `buildUpdateThingBatchEntry` One `updateThing` entry inside a move/add/replace-miner batch — carries the miner's new rack/position/network config plus the owning `minerId` the backend uses to group batch progress. **Function** ```typescript (params: UpdateThingParams, minerId: string = params.id) => VotingActionPayload ``` ### `CABINET_DEVICES_TYPES_NAME_MAP` **Constant** ```typescript { readonly "powermeter-abb-b24": "Powermeter ABB B24"; readonly "sensor-temp-seneca": "Sensor Temp Seneca"; readonly "powermeter-abb-m4m20": "Powermeter ABB M4M20"; readonly "powermeter-abb-m1m20": "… ``` ### `checkPermission` Check if user has the requested permission **Function** ```typescript (config: AuthConfig | null | undefined, { perm, write, cap }: PermissionCheck) => boolean ``` ### `COMPLETE_CONTAINER_TYPE` **Constant** ```typescript { readonly BITMAIN_HYDRO: "container-as-hk3"; readonly BITDEER_M30: "container-bd-d40-m30"; readonly BITDEER_M56: "container-bd-d40-m56"; readonly MICROBT_ALPHA: "container-mbt-alpha"; readonly BITDE… ``` ### `COMPLETE_MINER_TYPES` **Constant** ```typescript { readonly ANTMINER_AM_S21: "miner-am-s21"; readonly WHATSMINER_WM_63: "miner-wm-m63"; readonly WHATSMINER_WM_53: "miner-wm-m53s"; readonly AVALON_AV_a1346: "miner-av-a1346"; readonly WHATSMINER_WM_5… ``` ### `CONTAINER_LIST_THINGS_LIMIT` **Constant** ```typescript 1000 ``` ### `CONTAINER_MODEL` Container tag / type / threshold literals. Data-layer contracts shared across the toolkit — owned by ui-foundation so the React layers stay free of tag strings. **Constant** ```typescript { readonly M221: "m221"; readonly GAMMA: "gamma"; readonly BITDEER: "bitdeer"; readonly BITMAIN: "bitmain"; readonly MICROBT: "microbt"; readonly ANTSPACE: "antspace"; readonly BITMAIN_IMM: "bitmain-… ``` ### `CONTAINER_MODEL_FAMILY` Container model families the tab matrix distinguishes. Detection order matters and mirrors the reference app's if/else chain — see `resolveContainerModelFamily`. **Constant** ```typescript { readonly BITDEER: "bitdeer"; readonly ANTSPACE_HYDRO: "antspace-hydro"; readonly ANTSPACE_IMMERSION: "antspace-immersion"; readonly MICROBT: "microbt"; readonly GAMMA: "gamma"; } ``` ### `CONTAINER_SETTINGS_MODEL` **Constant** ```typescript { BITDEER: string; MICROBT: string; HYDRO: string; IMMERSION: string; } ``` ### `CONTAINER_STATUS` **Constant** ```typescript { readonly RUNNING: "running"; readonly OFFLINE: "offline"; readonly STOPPED: "stopped"; } ``` ### `CONTAINER_TAB` Container detail-view tab keys. The per-model availability matrix lives in `utils/container-tabs.ts`. **Constant** ```typescript { readonly PDU: "pdu"; readonly HOME: "home"; readonly ALARM: "alarm"; readonly CHARTS: "charts"; readonly HEATMAP: "heatmap"; readonly CONTROLS: "controls"; readonly SETTINGS: "settings"; readonly P… ``` ### `CONTAINER_TAB_LABEL` Display labels for the container detail tabs, mirroring the reference app's `containerTabsHelper` (`PDU` renders as "PDU Layout", the rest are capitalised keys). **Constant** ```typescript { readonly pdu: "PDU Layout"; readonly home: "Home"; readonly alarm: "Alarm"; readonly charts: "Charts"; readonly heatmap: "Heatmap"; readonly controls: "Controls"; readonly settings: "Settings"; rea… ``` ### `CONTAINER_TAB_MATRIX` Base tab sequence per model family — order is display order. Power Adjustment is intentionally absent here: it is inserted positionally by `getSupportedContainerTabs`. **Constant** ```typescript Record ``` ### `CONTAINER_TACTICS_TYPE` **Constant** ```typescript { readonly COIN: "coin"; readonly DISABLED: "disabled"; readonly ELECTRICITY: "electricity"; } ``` ### `CONTAINER_TYPE` **Constant** ```typescript { readonly BITDEER: "bd"; readonly ANTSPACE: "as"; readonly MICROBT: "mbt"; readonly ANTSPACE_HYDRO: "as-hk3"; readonly ANTSPACE_IMMERSION: "as-immersion"; } ``` ### `CONTAINER_TYPE_NAME_MAP` **Constant** ```typescript { readonly "container-bd-d40-m30": "Bitdeer M30"; readonly "container-bd-d40-m56": "Bitdeer M56"; readonly "container-bd-d40-s19xp": "Bitdeer S19XP"; readonly "container-as-hk3": "Bitmain Hydro"; rea… ``` ### `CONTAINER_WIDGETS_AGGR_FIELD_KEYS` All realtime aggregate fields the cards read — the tail-log `aggrFields` set. **Constant** ```typescript readonly string[] ``` ### `CONTAINER_WIDGETS_SUMMARY_FIELD` Realtime summary aggregate field names (these carry the `_aggr` suffix). **Constant** ```typescript { readonly HASHRATE_MHS_1M_SUM: "hashrate_mhs_1m_group_sum_aggr"; readonly TEMPERATURE_MAX: "temperature_c_group_max_aggr"; readonly TEMPERATURE_AVG: "temperature_c_group_avg_aggr"; } ``` ### `CONTAINERS_MINER_TYPE` **Constant** ```typescript { readonly M56: "m56"; readonly M30: "m30"; readonly A1346: "a1346"; readonly S19XP: "s19xp"; } ``` ### `DEFAULT_HISTORICAL_WINDOW_MS` Default historical-alerts look-back window (14 days), matching the devkit `<Alerts>` feature default. Wider ranges fan out into more 24h requests — see `fetchHistoricalAlertsInChunks`. **Constant** ```typescript number ``` ### `deriveContainerActivity` Slice the realtime aggregate into one container's per-status miner counts. `total` is the container's nominal miner capacity; miners not accounted for by any status count collapse into `disconnected` (never negative). With no realtime samp… **Function** ```typescript (realtime: TailLogEntry | undefined, containerModel: string, total: number) => ContainerActivity ``` ### `deriveContainerSummary` **Function** ```typescript (realtime: TailLogEntry | undefined, containerModel: string) => ContainerSummary ``` ### `deriveContainerTanks` Derive the per-tank readings for a container's immersion cooling system — one entry per oil pump, joined with the matching water pump and (when present) the tank pressure. Returns an empty array for containers without an immersion cooling… **Function** ```typescript (container: ListThingsDevice) => TankReading[] ``` ### `deriveSelectedSockets` Derive the store's `selectedSockets` map (keyed by container tag) from the selected device-tags — the pure body of the reference app's `findAndSetSelectedSockets`. Each per-container tag key (`pos-…` / `id-…`) is stripped of its `pos-` pre… **Function** ```typescript (containers: ContainerSnapshotForSockets[] | undefined, selectedDevicesTags: Record>, allDevices: MinerForSocket[] | undefined) => Record ``` ### `exportSettingsToFile` **Function** ```typescript (data: SettingsExportData) => string ``` ### `filterUsers` **Function** ```typescript ({ users, email, role }: FilterUsersParams) => SettingsUser[] ``` ### `findMatchingContainer` Find the container-settings row for a container type: an exact `model` match first, else the settings-model family fallback. Mirrors the reference app's `findMatchingContainer`. **Function** ```typescript (settings: ContainerSettingsEntry[] | undefined, containerType: string | undefined) => ContainerSettingsEntry | null ``` ### `flattenKernelEnvelope` Flattens the per-Kernel response envelope the gateway wraps around merged worker responses (`/auth/list-things`, `/auth/list-racks`, ... return `[[row, ...], [row, ...]]` — one inner array per Kernel). Null-safe at every level: a missing o… **Function** ```typescript (envelope: ReadonlyArray | null | undefined) => T[] ``` ### `formatLastActive` **Function** ```typescript (timestamp: string | undefined) => string ``` ### `formatRoleLabel` **Function** ```typescript (role: string) => string ``` ### `GATEWAY_REFRESH_INTERVAL_MS` Refresh cadence — 250 s, mirroring the reference deployment. The Gateway's token TTL defaults to 5 min, so this refreshes comfortably inside the window. **Constant** ```typescript 250000 ``` ### `getAntspaceHydroIndexes` Antspace Hydro position → `[rack, pdu, socket]`. **Function** ```typescript (pos: string) => string[] ``` ### `getAntspaceImmersionIndexes` Antspace Immersion position → `[pdu, socket]`. **Function** ```typescript (pos: string) => string[] ``` ### `getBitdeerIndexes` Bitdeer / MicroBT position → `[pdu, socket]`. **Function** ```typescript (pos: string) => string[] ``` ### `getByIdsQuery` **Function** ```typescript (ids: string[], allowEmptyArray?: boolean) => string ``` ### `getByTagsQuery` **Function** ```typescript (filterTags: string[], allowEmptyArray?: boolean) => string ``` ### `getByTagsWithAlertsQuery` **Function** ```typescript (filterTags: string[], allowEmptyArray?: boolean) => string ``` ### `getByTagsWithCriticalAlertsQuery` **Function** ```typescript (filterTags: string[], allowEmptyArray?: boolean) => string ``` ### `getByThingsAttributeQuery` **Function** ```typescript (filterAttributes: FilterAttribute[], selectedTypes: string[], allowEmptyArray?: boolean) => UnknownRecord ``` ### `getByTypesQuery` **Function** ```typescript (filterTypes: string[], allowEmptyArray?: boolean) => string ``` ### `getConnectedMinerForSocket` The selected miner sitting at `pos`, if any (miners only, exact position). **Function** ```typescript (devices: MinerForSocket[] | undefined, pos: string) => MinerForSocket | undefined ``` ### `getContainerByContainerTagsQuery` **Function** ```typescript (filterTags: string[], allowEmptyArray?: boolean) => string ``` ### `getContainerMinersByContainerTagsQuery` **Function** ```typescript (filterTags: string[], allowEmptyArray?: boolean) => string ``` ### `getContainerSettingsModel` Map a container type string to its settings-model key (`bd` / `mbt` / `hydro` / `immersion`), or `null` for an unknown family. Mirrors the reference app's `getContainerSettingsModel`. **Function** ```typescript (containerType: string | undefined) => string | null ``` ### `getDeviceByAlertId` **Function** ```typescript (uuid: string) => string ``` ### `getFiltersQuery` **Function** ```typescript (filterTags?: string[], filters?: Record, selectedTypes: string[] = ['t-container']) => UnknownRecord ``` ### `getListQuery` **Function** ```typescript (filterTags: string[], filters?: Record, selectedTypes: string[] = ['t-container']) => string ``` ### `getLvCabinetDevicesByRoot` **Function** ```typescript (root: string) => string ``` ### `getMinersByContainerTagsQuery` **Function** ```typescript (filterTags: string[], allowEmptyArray?: boolean) => string ``` ### `getPduByIndex` The PDU row whose `pdu` matches `pduIndex` on this container, or undefined. **Function** ```typescript (container: ContainerSnapshotForSockets | undefined, pduIndex: string | number) => PduSnapshot | undefined ``` ### `getPduData` The `pdu_data` array off a container detail snapshot, or undefined. **Function** ```typescript (last: ContainerSnapshotForSockets['last']) => PduSnapshot[] | undefined ``` ### `getRolesFromAuthToken` Extract roles from authentication token **Function** ```typescript (authToken?: string) => string[] ``` ### `getSignInRedirectUrl` Get redirect URL based on user's primary role **Function** ```typescript (authToken: string | null | undefined) => string ``` ### `getSitePowerMeterQuery` **Function** ```typescript () => string ``` ### `getSocketInfo` Resolve one socket for `pos` on `container`: pick the vendor index scheme off the container name, then (for Bitdeer / MicroBT) join the live PDU row so `enabled` / `cooling` reflect the snapshot. Hydro / Immersion have no live socket table… **Function** ```typescript (container: ContainerSnapshotForSockets, pos: string, allDevices: MinerForSocket[] | undefined) => DerivedSocket ``` ### `getSupportedContainerTabs` Full tab sequence for a container type: the family's base sequence, plus Power Adjustment spliced in after the PDU tab for Whatsminer containers. Unknown types resolve to an empty list. **Function** ```typescript (type: string | undefined) => ContainerTabValue[] ``` ### `getWidgetAlarmState` Resolve a container's alarm state from its live stats and matched settings. **Function** ```typescript (_container: ListThingsDevice, _settings: ContainerSettingsEntry | null = null) => ContainerAlarmState ``` ### `isAntminer` **Function** ```typescript (type: string | undefined) => boolean ``` ### `isAntspaceHydroContainer` Antspace/Bitmain hydro family (`as-hk3`, `antspace-hydro`, `bitmain-hydro`). **Function** ```typescript (type: string | undefined) => boolean ``` ### `isAntspaceImmersionContainer` Antspace/Bitmain immersion family (`as-immersion`, `bitmain-imm[ersion]`). **Function** ```typescript (type: string | undefined) => boolean ``` ### `isAvalon` **Function** ```typescript (type: string | undefined) => boolean ``` ### `isBitdeerContainer` Bitdeer family — `container-bd-*` and anything mentioning `bitdeer`. **Function** ```typescript (type: string | undefined) => boolean ``` ### `isContainer` **Function** ```typescript (type: string | undefined) => boolean ``` ### `isGammaContainer` Gamma family (`m221`, `gamma`). **Function** ```typescript (type: string | undefined) => boolean ``` ### `isMicroBTContainer` MicroBT family (`mbt`, `microbt`). **Function** ```typescript (type: string | undefined) => boolean ``` ### `isMiner` **Function** ```typescript (type: string | undefined) => boolean ``` ### `isPduContainerTab` The PDU grid renders under the `pdu` tab key. **Function** ```typescript (tab: string | undefined) => boolean ``` ### `isWhatsminer` **Function** ```typescript (type: string | undefined) => boolean ``` ### `isWhatsminerContainer` Containers populated with Whatsminer miners (`m56` / `m30` positions, or any MicroBT container) — the set that gets the Power Adjustment tab. **Function** ```typescript (type: string | undefined) => boolean ``` ### `LIVE_ACTIONS_REFETCH_KEY` Live-actions key — refetched (not just invalidated) so new cards appear at once. **Constant** ```typescript readonly ["auth", "actions", "live"] ``` ### `LV_CABINET_DEVICES_TAG` **Constant** ```typescript { readonly POWERMETER: "t-powermeter"; readonly SENSOR_TEMP: "t-sensor-temp"; } ``` ### `LV_CABINET_DEVICES_TYPE` **Constant** ```typescript { readonly POWERMETER_ABB_B24: "powermeter-abb-b24"; readonly SENSOR_TEMP_SENECA: "sensor-temp-seneca"; readonly POWERMETER_ABB_M1M20: "powermeter-abb-m1m20"; readonly POWERMETER_ABB_M4M20: "powermet… ``` ### `MAINTENANCE_CONTAINER` **Constant** ```typescript "maintenance" ``` ### `MINER_BRAND_NAMES` **Constant** ```typescript { readonly av: "Avalon"; readonly am: "Antminer"; readonly wm: "Whatsminer"; } ``` ### `MINER_MODEL` Device / miner / power-meter tag + type literals. These are data-layer contracts shared between the API surface and the UI — owned by ui-foundation so the React layers can stay free of tag strings. **Constant** ```typescript { readonly AVALON: "avalon"; readonly ANTMINER: "antminer"; readonly WHATSMINER: "whatsminer"; } ``` ### `MINER_MODEL_TO_TYPE_MAP` **Constant** ```typescript { readonly av: "avalon"; readonly am: "antminer"; readonly wm: "whatsminer"; } ``` ### `MINER_POWER_MODE` **Constant** ```typescript { readonly SLEEP: "sleep"; readonly LOW: "low"; readonly NORMAL: "normal"; readonly HIGH: "high"; } ``` ### `MINER_TYPE` **Constant** ```typescript { readonly AVALON: "av"; readonly ANTMINER: "am"; readonly WHATSMINER: "wm"; } ``` ### `MINER_TYPE_MESSAGE` **Constant** ```typescript { readonly "miner-av-a1346": "A1346 miners do not report consumption individually, so Avg. efficiency cannot be calculated"; } ``` ### `MINER_TYPE_NAME_MAP` **Constant** ```typescript { readonly "miner-av-a1346": "Avalon A1346"; readonly "miner-am-s21": "Antminer S21"; readonly "miner-wm-m63": "Whatsminer M63"; readonly "miner-wm-m56s": "Whatsminer M56S"; readonly "miner-wm-m53s":… ``` ### `MinerStatuses` **Constant** ```typescript { readonly MINING: "mining"; readonly OFFLINE: "offline"; readonly SLEEPING: "sleeping"; readonly ERROR: "error"; readonly NOT_MINING: "not_mining"; readonly MAINTENANCE: "maintenance"; readonly ALER… ``` ### `NO_MAINTENANCE_CONTAINER` **Constant** ```typescript "no_maintenance" ``` ### `ONE_DAY_MS` One day in milliseconds. **Constant** ```typescript number ``` ### `OP_CENTER_CABINET_DETAIL_FIELDS` The cabinet-detail field projection — the sensor/powermeter fields the LV cabinet detail reads: each device's `power_w` / `temp_c` reading, status (for the offline marker) and `last.alerts` (for the warnings timeline). **Constant** ```typescript string ``` ### `OP_CENTER_CONTAINER_DETAIL_FIELDS` The container-detail field projection — a superset of the list projection that also pulls the full `last.snap.stats` (so the socket transform sees `container_specific.pdu_data`, ambient temp, humidity, …) and the full `last.snap.config` (p… **Constant** ```typescript string ``` ### `OP_CENTER_CONTAINER_WIDGETS_FIELDS` Container list projection for the Site Overview widgets — the lean list fields plus `last.snap.stats.container_specific` (cooling system: oil / water pumps, tanks) and `last.snap.config` (per-vendor thresholds), which the vendor boxes (e.g… **Constant** ```typescript string ``` ### `OP_CENTER_LIST_THINGS_FIELDS` The list-things field projection the Explorer tables and Container Widgets cards read — the reference app's `LIST_THINGS_FIELDS`, kept as one projection so every consumer sees the same row shape. **Constant** ```typescript string ``` ### `parseSettingsFile` **Function** ```typescript (file: File) => Promise ``` ### `PM_ATTRIBUTE_LABEL_MAP` **Constant** ```typescript { readonly 'I1 a': "Current L1"; readonly 'I2 a': "Current L2"; readonly 'I3 a': "Current L3"; readonly 'V3 n v': "Voltage L3-N"; readonly 'V2 n v': "Voltage L2-N"; readonly 'V1 n v': "Voltage L1-N";… ``` ### `POWER_MODE` Miner power modes accepted by `setPowerMode`. **Constant** ```typescript { readonly SLEEP: "sleep"; readonly LOW: "low"; readonly NORMAL: "normal"; readonly HIGH: "high"; } ``` ### `queryKeys` Centralised query key factories. All keys are arrays so TanStack Query can perform structural equality matching for invalidations. **Constant** ```typescript { readonly auth: () => readonly ["auth"]; readonly authPermissions: () => readonly ["auth", "permissions"]; readonly authToken: () => readonly ["auth", "token"]; readonly devices: () => readonly ["de… ``` ### `removeContainerPrefix` **Function** ```typescript (text: string) => string ``` ### `resolveContainerModelFamily` Resolves a raw container `type` string (e.g. `container-bd-d40-m56`) to its model family, or `undefined` for unknown types. First match wins, in the reference app's original branch order. **Function** ```typescript (type: string | undefined) => ContainerModelFamily | undefined ``` ### `SITE_OVERVIEW_STATUSES` **Constant** ```typescript { readonly OFFLINE: "offline"; readonly EMPTY: "empty"; readonly NOT_MINING: "not_mining"; readonly MINING: "mining"; } ``` ### `SOCKET_STATUSES` **Constant** ```typescript { readonly ERROR_MINING: "errorMining"; readonly MINER_DISCONNECTED: "disconnected"; readonly CONNECTING: "connecting"; readonly SLEEP: "sleep"; readonly LOW: "low"; readonly NORMAL: "normal"; readon… ``` ### `THRESHOLD_LEVEL` **Constant** ```typescript { readonly ALERT: "alert"; readonly ALARM: "alarm"; readonly NORMAL: "normal"; readonly ALARM_LOW: "alarmLow"; readonly ALARM_HIGH: "alarmHigh"; readonly CRITICAL_LOW: "criticalLow"; readonly CRITICA… ``` ### `THRESHOLD_TYPE` **Constant** ```typescript { readonly TANK_PRESSURE: "tankPressure"; readonly OIL_TEMPERATURE: "oilTemperature"; readonly WATER_TEMPERATURE: "waterTemperature"; readonly SUPPLY_LIQUID_PRESSURE: "supplyLiquidPressure"; } ``` ### `USER_ROLE` **Constant** ```typescript { readonly ADMIN: "admin"; readonly READ_ONLY: "read_only_user"; readonly SITE_MANAGER: "site_manager"; readonly SITE_OPERATOR: "site_operator"; readonly FIELD_OPERATOR: "field_operator"; readonly RE… ``` ### `validateSettingsJson` **Function** ```typescript (data: unknown) => data is SettingsExportData ``` ### `VOTING_SUBMISSION_TYPE` Submission `type` for the voting/approval workflow. **Constant** ```typescript "voting" ``` ### `WEBAPP_DISPLAY_NAME` **Constant** ```typescript "Application" ``` ### `WEBAPP_NAME` Neutral application display names used anywhere the toolkit needs to refer to the running product (header labels, chart legends, settings copy). Single source of truth — rebrands change these three values only. **Constant** ```typescript "Appl." ``` ### `WEBAPP_SHORT_NAME` **Constant** ```typescript "APP" ``` # Queries & mutations (/reference/ui/utilities/query) TanStack Query and mutation factories for Gateway API resources — miners, pools, containers, actions, site status, and device history. Each factory returns a query key and fetcher (or mutation key and mutation function) ready to pass to `useQuery` / `useMutation`. For the lower-level fetcher, URL, and query-client building blocks these factories are built on, see [Query helpers](/reference/ui/query-helpers). ## Package `@tetherto/mdk-ui-foundation` ## Queries & mutations @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `ACTION_WRITE_INVALIDATE_PREFIXES` Query-key prefixes refreshed after any action write (submit / vote / cancel). A write can change pool configs, miner assignments, the aggregated pools and the actions queue, so all four refresh together. **Constant** ```typescript readonly [readonly ["auth", "configs", "pool"], readonly ["auth", "miners"], readonly ["auth", "pools"], readonly ["auth", "actions"]] ``` ### `actionsQuery` `GET /auth/actions` — pending/voting actions list (the review-tray source). Array params serialize comma-separated. **Function** ```typescript (client: QueryClient, params: ActionsParams = {}, fetcher: Fetcher = runtimeFetcher(client)) => { queryKey: readonly ["auth", "actions", ActionsParams]; queryFn: ({ signal }?: QueryFnContext) => Prom… ``` ### `addThingCommentMutation` TanStack Mutation factory for `POST /auth/thing/comment` — add a device comment. Requires the `comments:write` permission; the backend stamps the author from the session token. **Constant** ```typescript (client: on, fetcher?: Fetcher) => { mutationKey: ResourceKey; invalidates: ResourceKey[]; mutationFn: (payload: ThingCommentBody) => Promise; } ``` ### `authTokenMutation` TanStack Mutation factory for `POST /auth/token`. Used by `useTokenPolling` to refresh the session token every 250 s. **Function** ```typescript (client: QueryClient, fetcher: Fetcher = runtimeFetcher(client)) => { mutationKey: readonly ["auth", "token"]; mutationFn: (body?: AuthTokenRequest) => Promise; } ``` ### `cancelActionsMutation` `DELETE /auth/actions/:type/cancel?ids=<comma>` — cancel pending actions. Arguments ride in the query string, so no body is sent. **Constant** ```typescript (client: on, fetcher?: Fetcher) => { mutationKey: ResourceKey; invalidates: ResourceKey[]; mutationFn: (payload: CancelActionsPayload) => Promise; } ``` ### `containerPoolStatsQuery` `GET /auth/pools/stats/containers` — per-container override counts. **Function** ```typescript (client: QueryClient, fetcher: Fetcher = runtimeFetcher(client)) => { queryKey: readonly ["auth", "pools", "stats", "containers"]; queryFn: ({ signal }?: QueryFnContext) => Promise { queryKey: readonly ["auth", "global", "data", GlobalDataParams]; quer… ``` ### `deleteThingCommentMutation` TanStack Mutation factory for `DELETE /auth/thing/comment` — remove an existing device comment (`body.id` identifies it; the schema still requires the full body on delete). **Constant** ```typescript (client: on, fetcher?: Fetcher) => { mutationKey: ResourceKey; invalidates: ResourceKey[]; mutationFn: (payload: ThingCommentBody) => Promise; } ``` ### `editThingCommentMutation` TanStack Mutation factory for `PUT /auth/thing/comment` — edit an existing device comment (`body.id` identifies it). **Constant** ```typescript (client: on, fetcher?: Fetcher) => { mutationKey: ResourceKey; invalidates: ResourceKey[]; mutationFn: (payload: ThingCommentBody) => Promise; } ``` ### `extDataQuery` TanStack Query factory for `GET /auth/ext-data`. Generic in the response row type so adapters can pin the result to a typed envelope (see `minerpoolStatsQuery` for the canonical narrowing). `query` is a JSON-stringified provider-specific s… **Function** ```typescript (client: QueryClient, params: ExtDataParams, fetcher: Fetcher = runtimeFetcher(client)) => { queryKey: readonly ["auth", "ext-data", ExtDataParams]; queryFn: ({ signal }?: QueryFnContext) => Promise<… ``` ### `featureConfigQuery` TanStack Query factory for `GET /auth/featureConfig` — deployment feature flags, including the multi-site mode switch. Note the camelCase path: there is no `/auth/feature-config` route (a kebab-case request falls through to the SPA fallbac… **Function** ```typescript (client: QueryClient, fetcher: Fetcher = runtimeFetcher(client)) => { queryKey: readonly ["auth", "featureConfig"]; queryFn: ({ signal }?: QueryFnContext) => Promise; } ``` ### `globalConfigQuery` TanStack Query factory for `GET /auth/global-config` — the global system config document. Shape is deployment-specific (not yet captured live), so callers narrow via the generic. **Function** ```typescript (client: QueryClient, fetcher: Fetcher = runtimeFetcher(client)) => { queryKey: readonly ["auth", "global-config"]; queryFn: ({ signal }?: QueryFnContext) => Promise; } ``` ### `globalDataQuery` TanStack Query factory for `GET /auth/global/data`. Generic in the row type — see `containerSettingsQuery` for the canonical narrowing. **Function** ```typescript (client: QueryClient, params: GlobalDataParams, fetcher: Fetcher = runtimeFetcher(client)) => { queryKey: readonly ["auth", "global", "data", GlobalDataParams]; queryFn: ({ signal }?: QueryFnContext)… ``` ### `historyLogQuery` TanStack Query factory for `GET /auth/history-log`. `logType` is required (`'alerts' | 'info'`). **Function** ```typescript (client: QueryClient, params: HistoryLogParams, fetcher: Fetcher = runtimeFetcher(client)) => { queryKey: readonly ["auth", "history-log", HistoryLogParams]; queryFn: ({ signal }?: QueryFnContext) =>… ``` ### `listRacksQuery` TanStack Query factory for `GET /auth/list-racks`. `type` (worker type, e.g. `miner` / `container`) is required — the backend 400s with `ERR_TYPE_INVALID` without it. Response is the per-Kernel nested envelope. **Function** ```typescript (client: QueryClient, params: ListRacksParams, fetcher: Fetcher = runtimeFetcher(client)) => { queryKey: readonly ["auth", "list-racks", ListRacksParams]; queryFn: ({ signal }?: QueryFnContext) => Pr… ``` ### `listThingsQuery` TanStack Query factory for `GET /auth/list-things`. `query` and `fields` are Mongo-style selectors passed as already-stringified JSON. **Function** ```typescript (client: QueryClient, params: ListThingsParams = {}, fetcher: Fetcher = runtimeFetcher(client)) => { queryKey: readonly ["auth", "list-things", ListThingsParams]; queryFn: ({ signal }?: QueryFnContex… ``` ### `liveActionsQuery` `GET /auth/actions?queries=…` — polls all action types in a single request using the multi-type query format. Returns the typed response map `{ voting, ready, executing, done }`. **Function** ```typescript (client: QueryClient, queries: ActionTypeQuery[] = [ { type: 'voting', opts: { reverse: true, limit: LIVE_ACTIONS_LIMIT } }, { type: 'ready', opts: { reverse: true, limit: LIVE_ACTIONS_LIMIT } }, { t… ``` ### `minerpoolStatsQuery` Convenience wrapper around `extDataQuery` pinned to `type=minerpool` and `query={"key":"stats"}`. Returns the canonical `MinerpoolExtDataEntry[][]` envelope so the pool counts hook can `_head(_head(...))` without casts. **Function** ```typescript (client: QueryClient, fetcher: Fetcher = runtimeFetcher(client)) => { queryKey: readonly ["auth", "ext-data", ExtDataParams]; queryFn: ({ signal }?: QueryFnContext) => Promise { queryKey: readonly ["auth", "miners", MinersParams]; queryFn: ({ signal }?: QueryFnContext) => Promise… ``` ### `pduLayoutQuery` TanStack Query factory for `GET /auth/pdu-layout` — the static PDU socket grid for a container type. The backend sources it from the container worker's `pduGridLayout` config keyed by the exact type string, and 400s with `ERR_PDU_LAYOUT_NO… **Function** ```typescript (client: QueryClient, params: PduLayoutParams, fetcher: Fetcher = runtimeFetcher(client)) => { queryKey: readonly ["auth", "pdu-layout", PduLayoutParams]; queryFn: ({ signal }?: QueryFnContext) => Pr… ``` ### `poolBalanceHistoryQuery` `GET /auth/pools/:pool/balance-history` — per-pool revenue/hashrate history for the chart view. **Function** ```typescript (client: QueryClient, pool: string, params: PoolBalanceHistoryParams = {}, fetcher: Fetcher = runtimeFetcher(client)) => { queryKey: readonly ["auth", "pools", string, "balance-history", PoolBalanceH… ``` ### `poolConfigForDeviceQuery` `GET /auth/pools/config/:minerId` — pool config + override count for a single device/miner. **Function** ```typescript (client: QueryClient, minerId: string, fetcher: Fetcher = runtimeFetcher(client)) => { queryKey: readonly ["auth", "pools", "config", string]; queryFn: ({ signal }?: QueryFnContext) => Promise { queryKey: readonly ["auth", "configs", "pool"]; queryFn: ({ signal }?: QueryFnContext) => Promise; } ``` ### `poolsQuery` `GET /auth/pools` — aggregated pools (hashrate / workers / balance / revenue). Feeds the Dashboard pool panel. **Function** ```typescript (client: QueryClient, fetcher: Fetcher = runtimeFetcher(client)) => { queryKey: readonly ["auth", "pools"]; queryFn: ({ signal }?: QueryFnContext) => Promise; } ``` ### `siteQuery` TanStack Query factory for `GET /auth/site` — the configured site label. **Function** ```typescript (client: QueryClient, fetcher: Fetcher = runtimeFetcher(client)) => { queryKey: readonly ["auth", "site"]; queryFn: ({ signal }?: QueryFnContext) => Promise; } ``` ### `siteStatusLiveQuery` `GET /auth/site/status/live?overwriteCache=true` — composite live site-status snapshot (hashrate / power / efficiency / miner, alert & pool counts). Polled on a short interval by `useSiteStatusLive`; `overwriteCache` bypasses the server-si… **Function** ```typescript (client: QueryClient, fetcher: Fetcher = runtimeFetcher(client)) => { queryKey: readonly ["auth", "site", "status", "live"]; queryFn: ({ signal }?: QueryFnContext) => Promise; } ``` ### `submitActionMutation` `POST /auth/actions/voting` — submit a single staged action. The backend exposes a fixed `voting` path, so the client-only `type` field is stripped from the body; the remaining fields (`query`, `action`, `params`, `rackType`, …) form the r… **Constant** ```typescript (client: on, fetcher?: Fetcher) => { mutationKey: ResourceKey; invalidates: ResourceKey[]; mutationFn: (payload: VotingActionPayload) => Promise; } ``` ### `submitBatchActionMutation` `POST /auth/actions/voting/batch` — submit a batch of staged actions in one request. Expects the `SubmitBatchActionsPayload` body (`{ batchActionsPayload, batchActionUID, suffix? }`). **Constant** ```typescript (client: on, fetcher?: Fetcher) => { mutationKey: ResourceKey; invalidates: ResourceKey[]; mutationFn: (payload: SubmitBatchActionsPayload) => Promise; } ``` ### `tailLogMultiQuery` TanStack Query factory for `GET /auth/tail-log/multi` — the batched variant of tail-log (`keys` is a comma-separated list of `stat-*` keys). Returns the same per-worker nested envelope as `tailLogQuery`, one series per requested key. **Function** ```typescript (client: QueryClient, params: TailLogMultiParams, fetcher: Fetcher = runtimeFetcher(client)) => { queryKey: readonly ["auth", "tail-log", "multi", TailLogMultiParams]; queryFn: ({ signal }?: QueryFnC… ``` ### `tailLogQuery` TanStack Query factory for `GET /auth/tail-log`. Returns the raw nested response shape (`Array<Array<TailLogEntry>>`) — callers unwrap with `_head(response)` (or a typed `select` projection). **Function** ```typescript (client: QueryClient, params: TailLogParams, fetcher: Fetcher = runtimeFetcher(client)) => { queryKey: readonly ["auth", "tail-log", TailLogParams]; queryFn: ({ signal }?: QueryFnContext) => Promise<… ``` ### `thingConfigQuery` TanStack Query factory for `GET /auth/thing-config` — a thing type's config document (Settings tab). Both params are required by the backend schema. Response shape is worker-specific, so callers narrow via the generic. **Function** ```typescript (client: QueryClient, params: ThingConfigParams, fetcher: Fetcher = runtimeFetcher(client)) => { queryKey: readonly ["auth", "thing-config", ThingConfigParams]; queryFn: ({ signal }?: QueryFnContext)… ``` ### `userInfoQuery` `GET /auth/userinfo` — current authenticated user's profile. Used to resolve the caller's email for partitioning live actions into "mine vs others". **Function** ```typescript (client: QueryClient, fetcher: Fetcher = runtimeFetcher(client)) => { queryKey: readonly ["auth", "userinfo"]; queryFn: ({ signal }?: QueryFnContext) => Promise; } ``` ### `voteActionMutation` `PUT /auth/actions/voting/:id/vote` — approve or reject a pending action. **Constant** ```typescript (client: on, fetcher?: Fetcher) => { mutationKey: ResourceKey; invalidates: ResourceKey[]; mutationFn: (payload: VoteActionPayload) => Promise; } ``` # Core utils (/reference/ui/utilities/utils) Core utility functions for formatting, validation, and data processing. ## Package `@tetherto/mdk-ui-foundation` ## Utilities @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `getLatestSample` Tiny projection helper used by the dashboard's header-stat hooks. **Function** ```typescript (entries: readonly T[] | null | undefined) => T | undefined ``` # Workers (/reference/worker) Workers are device protocol adapters for MDK. Each Worker wraps a specific API, such as a hardware vendor's API, and exposes it through the MDK Protocol, allowing Kernel to discover, query, and command it without knowing anything about the underlying hardware or business logic. ## Worker categories Workers are organized by categories, for example: | Directory | Description | |------------------------------------|----------------------------------------------------| | [`miners/`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/README.md) | Bitcoin ASIC miners — Antminer, Avalon | | [`containers/`](https://github.com/tetherto/mdk/blob/main/backend/workers/containers/README.md) | Mining container orchestration — Antspace, Bitdeer | | [`minerpools/`](https://github.com/tetherto/mdk/blob/main/backend/workers/minerpools/README.md) | Pool API integrations — Ocean, F2Pool | | [`power-meter/`](https://github.com/tetherto/mdk/blob/main/backend/workers/power-meter/README.md) | Power metering — ABB, SATEC, Schneider | | [`temperature/`](https://github.com/tetherto/mdk/blob/main/backend/workers/temperature/README.md) | Temperature/humidity sensors — Seneca | ## How Workers fit into MDK ```text Kernel │ │ Hyperswarm HRPC (MDK Protocol envelopes) ▼ `WorkerRuntime` ──┐ │ hosts Worker Plugin ────┘ │ │ Vendor protocol (TCP, HTTP, Modbus, Serial, …) ▼ Physical Hardware ``` Workers never initiate communication to Kernel. Kernel obtains each Worker's RPC public key through [DHT, local-directory, or same-process discovery](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/architecture.md#discovery-model), then initiates all MDK Protocol calls to the Worker over HRPC. ## Worker architecture Each Worker has: - **A [Worker Plugin](#1-worker-plugin)**, e.g. [`antminer/plugin/index.js`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/plugin/index.js). A plain object `{ contract, dir, connect, disconnect? }` — no base class, no subclassing - **A [`WorkerRuntime`](#2-workerruntime)**, the shared runtime that hosts the plugin's devices and exposes them through the MDK Protocol over HRPC - **A [`mdk-contract.json`](#3-mdk-contractjson)**, e.g. the [Antminer contract](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/plugin/mdk-contract.json), the engineering source of truth. Declares every telemetry field (name, unit, type) and every command (name, params) - **A [mock server](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/mock/server.js)**, a local HTTP server with canned responses for hardware-free development ### 1. Worker plugin The plugin is the object `WorkerRuntime` is constructed with — the contract, the plugin's own directory, and a `connect` function that turns one device's config into the `device` object every handler sees. There is no base class and no subclassing; a plugin package can be built and tested with zero dependency on `WorkerRuntime`. Every telemetry/command handler is invoked as `(ctx, params)`, where `ctx = { deviceId, device, config, services }`. ```text miners/antminer/ plugin/ index.js # the Worker Plugin: { contract, dir, connect, disconnect } mdk-contract.json boot.js # startAntminerWorker(opts) — constructs WorkerRuntime lib/antminer.js # the device driver plugin.connect() returns ``` ### 2. `WorkerRuntime` [`WorkerRuntime`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/lib/worker-runtime.js) hosts every device behind one HRPC channel to Kernel. It: - Starts a Hyperswarm RPC server and responds to every MDK Protocol action - Provides the RPC public key (`getPublicKey()`) that the host process registers or publishes according to the selected discovery mode - Dispatches incoming MDK Protocol actions to the plugin's per-device handlers, wrapping results into the protocol envelope itself - Persists the DHT/RPC keypair in a process-owned store when one is supplied (stable identity across restarts) ### 3. mdk-contract.json Each Worker package ships an [`mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/mdk-contract.schema.json) that declares its full capabilities: - **metadata** — provider, device family, brand, supported models - **capabilities.telemetry** — metric fields with types, units, and descriptions - **capabilities.commands** — available commands with parameters, constraints, and AI workflow examples - **capabilities.health** — supported states, alert types, troubleshooting rules - **capabilities.errors** — error code → description mapping The contract is the source of truth for a Worker's programmatic capabilities **and** its AI context. MDK deliberately merges formal validation and semantic guidance into one file rather than splitting them across a schema and a prose document that drift apart. Three fields carry that double load: - `description` is both the human-readable UI label and the rule an AI agent reasons about, so it states edge cases rather than just naming the field — *"Outlet temperature > 85C requires intervention"*, not *"Outlet temperature"* - `constraints` governs orchestration limits, publishing the bounds a caller is expected to respect - `troubleshooting` pairs if/then recovery behaviours with the payload they evaluate Kernel fetches this contract once via `capability.request` and caches it, and validates commands against it. A plugin can query the same capabilities to derive available operations dynamically. (Agent MCP tools are not derived from the contract — they come from a plugin's `mcp-plugin.json` or the Gateway plugin's `mdk-plugin.json`.) ## Start a Worker Each Worker package ships its own boot function that constructs `WorkerRuntime` internally — there is no single generic `startWorker()` entry point: ```js const { getKernel } = require('@tetherto/mdk-core') 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: { serialNum: 'AM-001' }, opts: { address: '192.168.1.10', port: 80, username: 'root', password: 'root' } }] }) await kernel.registerWorker(worker.runtime.getPublicKey()) ``` `seedDevices` only seeds a fresh, empty `storeDir`; add a device to an already-running Worker with the `registerThing` command instead (see each package's own `USAGE.md`, e.g. [`miners/antminer/USAGE.md`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/USAGE.md)). The `registerWorker()` call above is the same-process shape. Separate processes on one machine, or Workers on other hosts, publish the key to a shared directory or join a DHT topic instead: the [discovery model](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/architecture.md#discovery-model) compares the three, and [Test a new Worker](/guides/workers/test-a-worker) has a runnable host script for each. ## Implement a new Worker 1. Read the [full build walkthrough](/guides/workers/build-a-worker) — it covers the current model: a package directory of [`mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/mdk-contract.schema.json) + handler files hosted by [`WorkerRuntimeV2`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/lib/worker-runtime-v2.js). 2. Use [`demo-worker`](https://github.com/tetherto/mdk/blob/main/backend/workers/samples/demo-worker/package.json) as the package-directory template. 3. Author `mdk-contract.json` following [`mdk-contract.schema.json`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk-worker/mdk-contract.schema.json). 4. The Worker instance boots, connects to devices, and publishes or registers its RPC public key through the selected discovery mode — Kernel handles the rest. The miner Workers in [Worker architecture](#worker-architecture) (Antminer, Avalon) are v1 examples, not recommended templates for a new plugin; they still construct `WorkerRuntime` v1 directly via the `{ contract, dir, connect, disconnect? }` shape, which remains supported. `WorkerRuntimeV2` is the model for new hardware. ## Testing Needs npm 11 [(< 12)](/reference/environment); `npm install` from the repo root installs every package. Each Worker package has its own `mock/server.js` that simulates the hardware API. Run tests from the package root: ```bash cd backend/workers/miners/antminer && npm test cd backend/workers/miners/avalon && npm test ``` ## Run mock devices Boot one or more device mocks locally — no hardware — with the Workers-level runner. Entries are **comma-delimited**; the first token of each entry is the device **type** (case-insensitive) — or, for a type-less device such as `ocean`/`f2pool`, its name. Flags are **dash-free** so npm forwards them with no `--`: a bare number is the port, and `key=value` sets any other flag: ```bash npm run mock m56s, ocean npm run mock b23 4009 host=127.0.0.1 mockControlPort=5009, pm180 4008 ``` Run `npm run mock` with no arguments to list every device and its types. A single Worker package can also run just its own mock on its default port, e.g. `cd miners/whatsminer && npm run mock m56s`. ## Next steps - Build a [minimal dashboard on top of a Worker](/tutorials/build-a-dashboard) - Understand the [install pattern any Worker follows](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/install-pattern.md) - Build a full [Worker for new hardware](/guides/workers/build-a-worker) # Code of conduct (/support/community/code-of-conduct) ## Our commitment MDK is committed to fostering an open, professional, and respectful community. We welcome contributors of all backgrounds and experience levels. Participation in the MDK community should be harassment-free and inclusive for everyone. --- ## Expected behavior All participants in the MDK community are expected to: - Be respectful and constructive in communication - Provide helpful and professional feedback - Assume good intent - Focus on what is best for the project - Accept constructive criticism gracefully --- ## Unacceptable behavior The following behaviors are not tolerated: - Harassment, discrimination, or hateful conduct - Personal attacks or insulting language - Public or private harassment - Trolling, intimidation, or deliberate disruption - Publishing private information without consent - Any conduct that would be considered unprofessional in a workplace setting --- ## Scope This code of conduct applies to: - GitHub repositories - Issues and pull requests - Discussions and community channels - Official MDK communication platforms - Any other space officially associated with the MDK project --- ## Enforcement The Community Manager is responsible for enforcing this code of conduct. **Community Manager:** Gio\ **Lead Maintainer:** Hemant T If you experience or witness unacceptable behavior, report it privately to the Community Manager. Reports are handled confidentially and reviewed in coordination with the MDK core team. --- ## Enforcement guidelines The MDK core team may take any action deemed appropriate, including: - Warning the participant - Temporarily restricting access - Permanently banning a participant from the community - Removing content that violates this code of conduct Decisions regarding enforcement are final. --- ## Amendments This code of conduct may be updated from time to time by the MDK core team to reflect evolving community needs. # Contributing (/support/community/contributing) Thank you for your interest in contributing to [MDK](https://github.com/tetherto/mdk/). This document outlines the contribution workflow for the MDK repository, from setting up your development environment to submitting pull requests and participating in releases. ## Security If you discover a security vulnerability, do not report it in a public issue. Please follow the private disclosure instructions in [SECURITY.md](https://github.com/tetherto/mdk/blob/main/SECURITY.md). ## Monorepo structure MDK is a monorepo with separate backend and frontend workspaces: - Backend: - `backend/core/`: Backend services, container modules, and integration/unit tests (npm-based) - [`backend/workers/`](/reference/worker): Protocol-translator worker packages (miners, miner-pools, power-meter, temperature, containers), per-worker mock servers, and per-worker tests (npm-based) - Frontend: `ui/`: Frontend packages, demo app, and shared UI foundation (npm + Turbo-based) Choose the backend or frontend workflow that matches the area you are contributing to. ### Root configuration must be domain-aware The repo top level is a fixed set of domains (`ui/`, `backend/`, `docs/`, `examples/`) plus tooling and repo-meta files. Shared root config (today just `.gitignore`) is read across all of them, so every pattern must be written so it cannot silently match another domain's source: - **Anchor anything that targets one domain's build or runtime output.** Use `/name/` for the repo root or `domain/**/name/` for a subtree. A bare `status` / `store` / `tmp` / `Checklist*` matches a file or directory of that name *anywhere*, including UI source. That is exactly what caused a prior root ignore regression, where bare `status` / `store` swallowed `ui/packages/ui-foundation/src/store/`. - **Keep per-domain ignores in that domain's own `.gitignore`** (`ui/.gitignore`, the per-package backend `.gitignore`s), not the root. Things like `dist`, `.turbo`, and `build` belong to a domain. - **Lint/format/type config is domain-owned, not shared at the root.** `ui/` ships its own `eslint.config.mjs` / `tsconfig.base.json` / `.prettierrc`; backend uses `standard`. Do not add a root-level eslint/tsconfig/prettier that would apply across domains. - **A genuinely shared convention is fine if it applies identically to every domain** - e.g. committing `config/*.json.example` while ignoring the generated `config/*.json`. Note it as shared so the intent is clear. ## Get started ### Prerequisites Before contributing, ensure you have the following installed: - **Node.js** (version >=24) - **Git** (latest stable version) - **npm 11 [(< 12)](/reference/environment)** ### Licensing MDK is released under the [**Apache License 2.0**](https://github.com/tetherto/mdk/blob/main/LICENSE). By contributing, you agree that: - You retain copyright over your contributions - You grant a perpetual, worldwide, royalty-free license for their use - Contributions are provided **"AS IS"**, without warranty ## Development environment setup
1. Fork and clone 1. Fork [the repository](https://github.com/tetherto/mdk.git) on GitHub. 2. Clone your fork locally and navigate into the project directory: ```bash git clone https://github.com/username/mdk.git cd mdk ``` 1. Add the upstream remote: ```bash git remote add upstream https://github.com/tetherto/mdk.git ```
2. Stay in sync Keep your fork in sync with the main repository. For example: ```bash git fetch upstream git merge --ff-only upstream/main # fails loudly if main has diverged ```
### Backend contribution setup Use this workflow when contributing to backend code under `backend/core/`. ```bash npm install ``` `backend/core/*` packages, including `backend/core/agent`, are members of the root npm workspace: a single `npm install` at the repo root hoists and links them all. #### Common commands `npm run lint` and `npm test` at the repo root also run the UI workspace (`lint:ui`/`test:ui`), which needs Turbo installed via `npm run setup`. For a backend-only install (just `npm install`), scope commands to the backend workspaces instead: ```bash # Lint backend code npm run lint --workspaces --if-present # Run backend test suite (lint + unit + integration + package tests) npm test --workspaces --if-present ``` ### Frontend contribution setup Use this workflow when contributing to frontend code under `ui/`. ```bash cd ui npm install ``` #### Common commands ```bash # Build packages npm run build # Run dev mode (all packages + demo) npm run dev # Lint and type-check npm run lint npm run typecheck # Run tests npm test ``` ## Pull request workflow ### Conventional types MDK uses Conventional Commits-style types for both branch names and PR titles. | Type | Use for | |---|---| | `feat` | New features | | `fix` | Bug fixes | | `docs` | Documentation changes | | `refactor` | Code refactoring without behavior change | | `test` | Test additions or changes | | `chore` | Tooling, dependencies, repo maintenance | | `perf` | Performance improvements | | `style` | Formatting only (no logic change) | | `ci` | CI configuration changes | | `build` | Build system or external dependency changes | ### Branch naming convention Create branches using the following pattern: ```bash {type}/{short-description} ``` Where `{type}` is one of the [conventional types](#conventional-types). #### Branch naming examples ```bash # New feature git checkout -b feat/mdk-new-device # Bug fix git checkout -b fix/timeout-handling ``` ### Commit message template The repository ships a commit message template at [`.gitmessage`](https://github.com/tetherto/mdk/blob/main/.gitmessage) that pre-fills the commit editor with the expected format (type, summary, body, and Asana/Related PR references). Enable it once per clone: ```bash git config commit.template .gitmessage ``` This is a local convenience only — it is not enforced, and the [conventional types](#conventional-types) above remain the source of truth for commit, branch, and PR naming. ### Pull request steps 1. Sync your local main with upstream `main`. 2. Create a branch from local `main`. 3. Make your code changes. 4. Write or update tests. 5. Run linting and tests locally in the workspaces you changed: - `core`: `npm run lint && npm test` - `ui`: `npm run lint && npm test` (and `npm run typecheck` for TypeScript changes) 6. Commit changes with meaningful messages. 7. Push your branch and open a Pull Request targeting the upstream `main`. ### PR checklist Before submitting your PR, ensure that: - [ ] Code builds locally (`npm run build` for `ui` changes) - [ ] Tests pass in affected workspaces (`npm test`) - [ ] Linting passes (`npm run lint`) - [ ] Type-check passes for frontend TypeScript changes (`npm run typecheck`) - [ ] New features include tests - [ ] Public behavior or APIs changes have a [`docs-needed` issue](https://github.com/tetherto/mdk/issues/new?template=docs-needed.yml) linked to the PR - [ ] Generated pages affected by the change are regenerated, using the command named in that file's `DO NOT EDIT` header (a Worker contract, a plugin manifest, or devkit component source each rewrite a different file) The `docs-freshness` workflow also checks this on a PR that touches a Worker contract, a plugin manifest, devkit component source, or one of the generated pages themselves — it warns rather than blocks when a page is stale, so regenerating is still on you, not something CI does for you. It does fail the run if a generator itself breaks. ### PR title format Use the following convention: ```bash {type}({scope}): {description} ``` Where `{type}` is one of the [conventional types](#conventional-types) and `{scope}` is the affected area, for example `miner` or `ui`. Examples: - `feat(miner): add Antminer S21 support` - `fix(timeout): resolve action timeout handling` - `docs(api): update stats documentation` ## PR review All pull requests go through the following review steps: 1. **Automated checks**: Linting and tests must pass. 2. **Code review**: At least 2 maintainer approvals are required. 3. **Feedback resolution**: All requested changes must be addressed. 4. **Squash and merge**: Maintainers squash commits to keep history clean. ### Workflow diagram ```mermaid flowchart TB subgraph contributor [Contributor] start((Start)) --> createBranch[Create branch from main] createBranch --> test[Run tests] test --> testGw{Tests pass?} testGw -->|No| fix[Fix issues] fix --> test testGw -->|Yes| createPR[Create PR] createPR --> review address[Address feedback] --> pushFixes[Push fixes] pushFixes --> test end subgraph reviewer [Reviewer / Maintainer] review[Code review] reviewGw{Approved?} review --> reviewGw reviewGw -->|Request changes| address reviewGw -->|Rejected| cancel[Close PR] reviewGw -->|Approved| merge[Merge to main] merge --> tagGw{Tag release?} tagGw -->|No| endNoTag((End)) tagGw -->|Yes| tag[Tag version] tag --> deploy[Deploy] deploy --> deployGw{Deploy success?} deployGw -->|Yes| endSuccess((End)) deployGw -->|No| rollback[Rollback] rollback --> fix end ``` ## Code standards MDK uses **StandardJS** style to keep the codebase consistent and easy to review across repositories. Key rules: - 2-space indentation - No semicolons - Single quotes for strings - Space after keywords (`if`, `for`, `while`) - No unused variables ## Versioning and tagging MDK follows **Semantic Versioning**: - **MAJOR** (`1.x.x`): breaking changes - **MINOR** (`x.1.x`): new backward-compatible features - **PATCH** (`x.x.1`): backward-compatible bug fixes When the MDK team provides a release, they are cut from a `release/` branch, verified in CI, promoted to the public repo, then tagged. For the full release process and checklist, see [RELEASING.md](https://github.com/tetherto/mdk/blob/main/RELEASING.md). Happy contributing, and thanks for helping improve MDK! 🚀 # Governance (/support/community/governance) This document describes how the MDK project is governed and how decisions are made. MDK is an open-source project. While the code is publicly available and community contributions are welcome, final decision-making authority rests with the MDK core team. --- ## Project roles ### Users Anyone who uses MDK and provides feedback, bug reports, or feature requests. ### Contributors Community members who contribute code, documentation, tests, or other improvements via pull requests or issues. Contributors do not have merge access. --- ### Maintainers Maintainers are responsible for reviewing pull requests, maintaining code quality, and ensuring alignment with the project roadmap. Maintainers may be appointed by the Lead Maintainer. The following GitHub IDs currently hold maintainer status for MDK: - arif-dewi - eugeneglova - robdll - eskawl - boris91 - habrahamyanbf - efr-nox - rob-aslanian - mukama - paragmore - tekwani Only the GitHub accounts listed above have maintainer privileges within the MDK repositories. Maintainer status is granted based on demonstrated technical expertise, sustained contribution, and alignment with project goals. --- ### MDK core team The MDK Core Team consists of all active developers, the Project Manager, and the Community Manager. The Core Team is responsible for the overall health, sustainability, and long-term success of the project. While specific authorities are defined for the Lead Maintainer (technical) and the Community Manager (strategic), the Core Team operates as the primary collaborative decision-making body. ### Lead Maintainer **Hemant T** is the Lead Maintainer and Technical Lead for MDK. The Lead Maintainer has final authority over: - Pull request approval and merging - Architecture and design decisions - Release planning and versioning - Accepting or rejecting features - Appointing or removing maintainers If consensus cannot be reached among maintainers, the Lead Maintainer makes the final decision. --- ### Community Manager **Gio** is the Community Manager for MDK. The Community Manager is responsible for: - Managing community communication channels - Moderating discussions and enforcing the code of conduct - Supporting contributors during onboarding - Acting as a bridge between the community and the MDK core team - Defining, maintaining, and communicating the project vision and long-term direction - Leading roadmap prioritization and strategic planning --- ## Decision-making process - Community members may propose changes via issues or pull requests - Maintainers review contributions for quality, security, and alignment with MDK goals - The Lead Maintainer has final approval authority on all technical decisions - Strategic, roadmap, or breaking changes are determined by the MDK core team - The Community Manager has final decision-making authority over all strategic and directional matters, including project vision, roadmap prioritization, major feature introductions, partnerships, and significant pivots - In cases where strategic direction and technical considerations intersect, the Community Manager determines the final strategic outcome, while the Lead Maintainer determines the final technical implementation approach. --- ## Contribution review process - All contributions must follow `CONTRIBUTING.md` - Pull requests require review by a maintainer - Final approval and merge is performed by the Lead Maintainer - The MDK team reserves the right to decline contributions that do not align with the project direction --- ## Inactivity and removal Maintainers who become inactive for an extended period or violate project policies may be removed by the Lead Maintainer. --- ## Code of conduct All participants are expected to follow the project's code of conduct. Violations are handled by the Community Manager in coordination with the MDK core team. See [code of conduct](/support/community/code-of-conduct) for details. --- ## Changes to governance This governance model may evolve over time. Any changes are proposed and approved by the MDK core team. # MDK Repositories (/support/resources/repositories) The MDK monorepo makes the frontend and backend components publicly accessible: - [https://github.com/tetherto/mdk](https://github.com/tetherto/mdk) *MDK is developed by Tether and released under the [Apache 2.0 license](/support/community/contributing#licensing).* # Roadmap (/support/resources/roadmap) MDK follows a **two-week release cadence** to keep progress visible, collect feedback early, and progressively harden the platform — from a rudimentary end-to-end foundation toward a production-ready release. ## Release principles - **Release every two weeks** to keep momentum and feedback loops short - **Use four maturity phases** to mark clear readiness jumps - **Start with a working end-to-end developer experience**, then harden progressively - **Reach production readiness progressively**, not by a single large release - **Follow standard [semantic versioning](https://semver.org)** — the `0.x` line means MDK is still in development and not intended for production ## Versioning and naming strategy MDK uses standard [semantic versioning](https://semver.org) (`MAJOR.MINOR.PATCH`): - A **new version ships every two weeks**, bumping the **minor**: `0.2.0` → `0.3.0` → `0.4.0` → … - **Patch** releases (`0.x.1`) go out only when a fix is needed between the scheduled releases, e.g. 0.2.1 - While MDK is in the **`0.x` line**, interfaces may change and the platform is **not intended for production** — this is the standard semver signal, and we use it deliberately so developers can read it literally - **`1.0.0`** marks the first **production-ready** release Maturity is tracked by **phase**, not by version number — each phase spans several bi-weekly releases. The four phases describe how ready MDK is at each stage: | Phase | Production-readiness | |---|---| | **Foundation** | Experimental. End-to-end but rudimentary. Not stable. | | **Lab testing** | Complete enough to build against end-to-end, in a lab. Not stable, not for production. | | **On site testing** | Stable enough to test at real sites under real operating conditions, closely monitored. Not yet production-ready. | | **Production Ready** | Ready for production deployment, with stable interfaces and compatibility guarantees. | ## 2026 2026 moves MDK through **four phases**: - **May:** [Foundation](#foundation) - **July:** [Lab testing](#lab-testing) - **October:** [On site testing](#on-site-testing) - **December:** [Production Ready](#production-ready) Between phases, MDK ships a new release **every two weeks**, starting from the Foundation release at the end of May. The diagram below shows only the phase milestones; the bi-weekly releases land in the windows between them. ```mermaid graph TB subgraph foundation [Foundation] p1([May 2026

Foundation
Public, end-to-end but rudimentary]) it1[[Jun–Jul 2026
New release every 2 weeks]] end subgraph lab [Lab testing] p2([Jul 2026
Lab testing
Complete end-to-end, lab only]) it2[[Aug–Oct 2026
New release every 2 weeks]] end subgraph onsite [On site testing] p3([Oct 2026
On site testing
Testing at real sites]) it3[[Nov–Dec 2026
New release every 2 weeks]] end subgraph prod [Production Ready] p4([Dec 2026
Production Ready
Production-ready baseline]) end p1 --> it1 --> p2 --> it2 --> p3 --> it3 --> p4 style foundation fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A style lab fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A style onsite fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A style prod fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A ``` ## Release plan Phase descriptions below focus on the **level of maturity and production-readiness** at each stage. The exact feature scope of each two-week release is decided as we go and is not fixed by this roadmap. ### Foundation *Released end of May 2026.* The first public release is about **visibility, direction, and feedback**. It is intentionally rudimentary: developers can clone it, run an end-to-end example, and get a concrete feel for how MDK fits together. Interfaces are expected to change. The goal is to show where MDK is heading and invite feedback from developers, partners, and early contributors — not to support real workloads. ### Lab testing *Target: end of July 2026.* At this stage MDK is **complete enough to build against end to end**. Developers can run a full workflow and experiment with MDK in their own labs. It is **not stable** — breaking changes are still expected between releases — and it is **not ready for production**. The goal is to validate the end-to-end developer workflow and surface integration gaps before MDK is exposed to real operational conditions. ### On site testing *Target: end of October 2026.* This is the first stage intended for **testing at real sites**. MDK should be stable enough to run on site under real operating conditions, while still being monitored closely. The goal is to validate performance, robustness, deployment workflows, and operational fit in real scenarios. This is not yet a stability commitment — interfaces may still change before production readiness. ### Production Ready *Target: end of December 2026.* This is the first **production-ready** release (`1.0.0`). By this point MDK offers a solid baseline for production deployment, with stable core interfaces, validated workflows, and documentation that supports adoption by operators, integrators, and developers building on top of the platform. From here, standard semver compatibility guarantees apply. # Tutorials (/tutorials) ## New to MDK? See it run first These are not a sequence. Run the finished site to see the whole stack, then build your own version of it when you want your own data on your own routes. } title={Run a mining site end to end} href="/tutorials/run-a-site" description={ Run the full example: Workers, mock hardware, a Gateway API, an MCP server, and a live dashboard from one command } /> } title={Build a minimal single-page dashboard} href="/tutorials/build-a-dashboard" description={ Build from an empty directory: one Worker, one Gateway route, and one React page } /> } title={Build a dashboard with an agent} href="/tutorials/ui/react/build-any-dashboard-with-an-agent" description={ Generate it instead: wire Cursor or Claude once, then build from plain-language prompts } /> The Gateway serves only the routes its plugins provide, so getting Worker telemetry to a browser means mounting your own endpoint. Building a dashboard covers that end to end, and [building a Gateway plugin](/guides/gateway/plugins) is the reference for writing your own. ## Next steps - Learn more about the high-level [architecture](/concepts/architecture): runtime stack and deployment modes - Install and wire the [React packages](/guides/ui/install): the provider, hooks, and theming - Integrate your own hardware by [building a third-party Worker](/guides/workers/build-a-worker) - Run a site from the [deployment guides](/guides/deployment) - [Contribute](/support/community/contributing) # Build a minimal single-page dashboard (/tutorials/build-a-dashboard) If **Kernel**, **Worker**, or **Gateway** are unfamiliar terms, read [the architecture overview](/concepts/architecture) first. This tutorial also assumes you've skimmed [Build a third-party Worker](/guides/workers/build-a-worker): the one Worker used here is [`backend/workers/samples/demo-worker`](https://github.com/tetherto/mdk/blob/main/backend/workers/samples/demo-worker/package.json), the same zero-dependency reference Worker that tutorial is built around. This guide builds the smallest version of [the Starter site example](https://github.com/tetherto/mdk/tree/main/examples/mvp-site) (`examples/mvp-site`): **one Worker, one Gateway route, one React page**. It teaches the same end-to-end shape, including its UI layer, `@tetherto/mdk-react-devkit` components driven by `@tetherto/mdk-react-adapter` hooks. However, it does so without that example's family-specific adapters, persistence, history, commands, multi-page router, or chart aggregation. This tutorial hand-assembles every piece to teach the wiring. To scaffold a real dashboard app against a stack you already have running, use `mdk create dashboard` and `mdk run dashboard` instead. ## Overview This tutorial builds the smallest version of the full MDK stack: **one Worker, one Gateway route, one React page**. What you'll have at the end: ```text examples/minimal-dashboard/ package.json start.js # boots Kernel + the one Worker + Gateway, one process plugins/ dashboard/ mdk-plugin.json # one route: GET /overview controllers/ overview.js # lists every Worker's devices + live telemetry ui/ package.json tsconfig.json vite.config.ts index.html src/ main.tsx # + render OverviewPage OverviewPage.tsx # polls /overview, renders devkit components ``` No router, no charts, no hand-rolled CSS, just one page built from three `@tetherto/mdk-react-devkit` primitives (`LabeledCard`, `DataTable`, `Badge`) and one `@tetherto/mdk-react-adapter` hook (`useQuery`), the same building blocks `examples/mvp-site/ui` uses, minus the parts (router, sidebar, line charts, domain panels) this single-page, single-Worker dashboard doesn't need. ## Prerequisites - Node.js `>=24` - This repo checked out, with the backend and UI dependencies installed once from the repository root: ```bash npm install npm run setup:ui npm run build:ui ``` Running `npm run setup` from the Starter site example (`examples/mvp-site`) is also supported, but it is broader: it installs these backend and UI dependencies plus the Starter site example and UI dependencies and builds the UI packages. - Commands below assume you create a new `examples/minimal-dashboard/` directory alongside `examples/mvp-site/` ### Pick the Worker Use [`backend/workers/samples/demo-worker`](https://github.com/tetherto/mdk/blob/main/backend/workers/samples/demo-worker/package.json) as-is: it needs no worker-infra services (provisioning stores, alert templates), just `WorkerRuntime` and its own bundled mock device. Nothing in the steps below is specific to it, though: swap in `startWhatsminerWorker`, `startAntminerWorker`, or your own Worker from [Build a third-party Worker](/guides/workers/build-a-worker) and everything past the next step is unchanged. ### Boot Kernel and Worker in the same process The simplest of the three discovery modes ([full trade-offs here](/guides/deployment)): one Node process owns both the Kernel and the Worker, so there's no key file or DHT topic to manage. `examples/minimal-dashboard/start.js`: ```js 'use strict' const path = require('path') const { getKernel, waitForDiscovery } = require('../../backend/core/mdk') const { startDemoWorker } = require('../backend/demo-worker-caller') const demoMock = require('../../backend/workers/samples/demo-worker/mock/server') const ROOT = path.join(__dirname, '.mdk-data') const MOCK_PORT = 9101 const HTTP_PORT = Number(process.env.MDK_HTTP_PORT) || 3000 function onceListening (mock) { if (mock.server.listening) return Promise.resolve() return new Promise((resolve) => mock.server.once('listening', resolve)) } async function main () { // the one fake device this dashboard will show, swap for real hardware later const mock = demoMock.createServer({ host: '127.0.0.1', port: MOCK_PORT, serial: 'WM3-0001' }) await onceListening(mock) // Kernel + the one Worker, same process const kernel = await getKernel({ root: ROOT }) const worker = await startDemoWorker({ workerId: 'demo-worker-1', storeDir: path.join(ROOT, 'demo-worker-store'), seedDevices: [{ id: 'demo-0', opts: { host: '127.0.0.1', port: MOCK_PORT } }] }) await kernel.registerWorker(worker.runtime.getPublicKey()) await waitForDiscovery(kernel, { minWorkers: 1 }) console.log('worker registered: %s', worker.deviceIds.join(', ')) } module.exports = { main, ROOT, HTTP_PORT } if (require.main === module) main().catch((err) => { console.error(err); process.exit(1) }) ``` The demo Worker package exports no module of its own: it is an `mdk-contract.json` plus `src/` handler files, loaded from its own directory and instantiated per device by the Worker Runtime. The separate [`demo-worker-caller`](https://github.com/tetherto/mdk/blob/main/examples/backend/demo-worker-caller/index.js) used here owns that runtime, device configuration, persistence, sampling, and shutdown. `getKernel({ root: ROOT })` with no `topic`/`discovery` option defaults to DHT discovery with a fresh random topic. This example then registers the Worker's public key directly because both objects are in the same process. See the [discovery model](/reference/worker) for the DHT, Local, and Same-process options to use in other deployments. ### Write the one Gateway plugin route A [Gateway plugin](/guides/gateway/plugins) is a directory with a manifest and a controller. This one has a single read-only route that lists every registered Worker's devices and pulls each one's default `metrics` telemetry bundle, with no per-device-family branching, because there's only one family here. #### 3.1 Write the plugin manifest `examples/minimal-dashboard/plugins/dashboard/mdk-plugin.json`: ```json { "name": "@your-org/mdk-plugin-dashboard", "version": "0.1.0", "description": "Minimal dashboard plugin: one route that lists every registered device and its live telemetry.", "routes": [ { "id": "dashboard.overview", "handler": "./controllers/overview.js", "http": { "method": "GET", "path": "/overview" }, "description": "Live snapshot of every device across every registered Worker.", "safety": "read-only" } ] } ``` #### 3.2 Write the controller `examples/minimal-dashboard/plugins/dashboard/lib/client.js` builds the plugin's client once: ```js 'use strict' const { config } = require('@tetherto/mdk-gateway/plugin') const { createMdkClient } = require('@tetherto/mdk-client') module.exports = createMdkClient(config) ``` `examples/minimal-dashboard/plugins/dashboard/controllers/overview.js`: ```js 'use strict' const mdkClient = require('../lib/client') module.exports = async function overview (req) { const { workers } = await mdkClient.listWorkers() const devices = await Promise.all( workers.flatMap((w) => (w.deviceIds || []).map(async (deviceId) => { const tel = await mdkClient.pullTelemetry(deviceId, 'metrics') return { deviceId, workerId: w.workerId, workerState: w.state, ...tel.metrics } })) ) return { ts: Date.now(), devices } } ``` A controller is `async (req) => value`: return a plain object, the Gateway serializes it to JSON itself; you never touch `res`. `mdkClient` is the same client used everywhere else in MDK, with no knowledge of the underlying MDK Protocol envelope required. With more than one Worker family mixed in (miners, powermeters, sensors, ...) you'd branch by `deviceFamily` instead of spreading `tel.metrics` blindly. [The Starter site's overview controller](https://github.com/tetherto/mdk/blob/main/examples/mvp-site/backend/gateway-plugins/site/controllers/overview.js) shows that pattern once you outgrow this one. ### Serve the plugin and static page from the same Gateway Add `startGateway` to `start.js`, mounting the plugin via `extraPluginDirs` and the built UI (Step 5's `ui/dist`) via `common.staticRootPath`. This is the complete canonical file; its boot order is mock, Kernel, Worker, registration, readiness, then Gateway: ```js 'use strict' const path = require('path') const { getKernel, startGateway, waitForDiscovery } = require('../../backend/core/mdk') const { startDemoWorker } = require('../backend/demo-worker-caller') const demoMock = require('../../backend/workers/samples/demo-worker/mock/server') const ROOT = path.join(__dirname, '.mdk-data') const MOCK_PORT = 9101 const HTTP_PORT = Number(process.env.MDK_HTTP_PORT) || 3000 function onceListening (mock) { if (mock.server.listening) return Promise.resolve() return new Promise((resolve) => mock.server.once('listening', resolve)) } async function main () { // Start the mock device before its Worker tries to connect. const mock = demoMock.createServer({ host: '127.0.0.1', port: MOCK_PORT, serial: 'WM3-0001' }) await onceListening(mock) // Start Kernel, then the Worker runtime that hosts the demo Worker plugin. const kernel = await getKernel({ root: ROOT }) const worker = await startDemoWorker({ workerId: 'demo-worker-1', storeDir: path.join(ROOT, 'demo-worker-store'), seedDevices: [{ id: 'demo-0', opts: { host: '127.0.0.1', port: MOCK_PORT } }] }) // Register the Worker and wait until it is ready before accepting HTTP traffic. await kernel.registerWorker(worker.runtime.getPublicKey()) await waitForDiscovery(kernel, { minWorkers: 1 }) await startGateway({ kernel, port: HTTP_PORT, root: path.join(ROOT, 'gateway'), tmpdir: path.join(ROOT, 'gateway'), extraPluginDirs: [path.join(__dirname, 'plugins', 'dashboard')], common: { staticRootPath: path.join(__dirname, 'ui', 'dist') } }) console.log('worker registered: %s', worker.deviceIds.join(', ')) console.log(`dashboard up: http://localhost:${HTTP_PORT}/`) } module.exports = { main, ROOT, HTTP_PORT } if (require.main === module) main().catch((err) => { console.error(err); process.exit(1) }) ``` This route accepts any caller, because the Gateway applies no authentication of its own and this controller adds none. Keep the Gateway off untrusted networks while you work. A production deployment needs TLS termination, network policy, and [authentication and authorization inside each controller](/guides/gateway/plugins#auth-and-permissions); write routes additionally need input validation, rate limits, and auditing. Serve the built page from `common.staticRootPath`, not a separate server on its own port. Gateway plugin controllers only receive `(req)`, never the underlying reply object, so a controller has no way to set `Access-Control-Allow-Origin`, and Gateway has no built-in CORS support. Building the UI (Step 5) and serving `ui/dist` from `staticRootPath` keeps `fetch('/overview')` same-origin with zero CORS configuration. This is also why `examples/mvp-site`'s Vite *dev* server proxies `/site/*` to the Gateway port instead of calling it cross-origin. Step 5's `vite.config.ts` proxies `/overview` the same way for hot-reload development. ### Write the single-page UI with MDK devkit components Instead of hand-rolled HTML, the page is a small React + Vite app built from the same packages `examples/mvp-site/ui` uses, so `@tetherto/mdk-react-adapter` for the provider and data hook, `@tetherto/mdk-react-devkit` for the components, scaled down to what one route needs: no router (one page), no charts (no history endpoint here), no sidebar. Three primitives do the job: `LabeledCard` for the section container, `DataTable` for the sortable device grid, and `Badge` to color-code `workerState`. `examples/minimal-dashboard/ui/package.json`: ```json { "name": "@your-org/mdk-minimal-dashboard-ui", "type": "module", "version": "0.1.0", "private": true, "scripts": { "dev": "vite", "build": "tsc --noEmit && vite build" }, "dependencies": { "@tetherto/mdk-react-adapter": "file:../../../ui/packages/react-adapter", "@tetherto/mdk-react-devkit": "file:../../../ui/packages/react-devkit", "react": "^19.2.0", "react-dom": "^19.2.0" }, "devDependencies": { "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^4.3.4", "typescript": "^5.7.3", "vite": "^6.3.5" } } ``` `examples/minimal-dashboard/ui/tsconfig.json`: ```json { "compilerOptions": { "target": "ES2022", "jsx": "react-jsx", "lib": ["ES2022", "DOM", "DOM.Iterable"], "types": ["vite/client"], "module": "ESNext", "moduleResolution": "bundler", "strict": true, "skipLibCheck": true, "noEmit": true }, "include": ["src/**/*", "vite.config.ts"], "exclude": ["node_modules", "dist"] } ``` `examples/minimal-dashboard/ui/vite.config.ts`: the dev-only proxy so `fetch('/overview')` stays same-origin against the Gateway port, the same pattern [`examples/mvp-site/ui/vite.config.ts`](https://github.com/tetherto/mdk/blob/main/examples/mvp-site/ui/vite.config.ts) uses for `/site/*`: ```ts declare const process: { env: Record } const apiPort = process.env.VITE_API_PORT || '3000' plugins: [react()], server: { port: Number(process.env.MDK_UI_PORT) || 3041, proxy: { '/overview': `http://localhost:${apiPort}` } } }) ``` `examples/minimal-dashboard/ui/index.html`: ```html MDK dashboard
``` `examples/minimal-dashboard/ui/src/main.tsx`: `` wires the TanStack Query client `useQuery` needs and resolves the API base URL, exactly as it does in [`examples/mvp-site/ui/src/main.tsx`](https://github.com/tetherto/mdk/blob/main/examples/mvp-site/ui/src/main.tsx): ```tsx const rootElement = document.getElementById('root') if (!rootElement) throw new Error('ERR_ROOT_ELEMENT_MISSING') // Same-origin: the Vite dev proxy forwards /overview to the gateway; // in production the Gateway serves this build from staticRootPath. ReactDOM.createRoot(rootElement).render( ) ``` With no `auth` prop, `MdkProvider` defaults to `gatewayRedirectAuth()` (the bundled mining Gateway's OAuth-redirect flow) minus a redirect target — fine for a read-only route with no `"auth": true` requirement, like this one. Pass `auth={noAuth()}` instead for a backend that needs no session at all, or `auth={gatewayRedirectAuth({ oauthBaseUrl })}` to enable sign-in. The [react-adapter auth presets](https://github.com/tetherto/mdk/blob/main/ui/packages/react-adapter/README.md#authentication) cover what each one does and its limits. Create `ui/src/OverviewPage.tsx` under your new `examples/minimal-dashboard/`: `useQuery` polls the route from Step 3 the same way [`examples/mvp-site/ui/src/SitePage.tsx`](https://github.com/tetherto/mdk/blob/main/examples/mvp-site/ui/src/SitePage.tsx) polls `/site/overview`; `DataTable` and `Badge` replace that page's hand-rolled markup: ```tsx type Device = { deviceId: string workerId: string workerState: string hashrate_rt?: number power?: number temperature?: number } type Overview = { ts: number; devices: Device[] } function get(base: string, path: string): Promise { return fetch(`${base}${path}`).then((res) => { if (!res.ok) throw new Error(`HTTP ${res.status}`) return res.json() as Promise }) } function stateBadgeStatus(state: string): 'success' | 'error' | 'default' { if (state === 'ready') return 'success' if (state === 'offline') return 'error' return 'default' } const columns: DataTableColumnDef[] = [ { accessorKey: 'deviceId', header: 'Device' }, { accessorKey: 'workerId', header: 'Worker' }, { id: 'workerState', header: 'Worker state', cell: ({ row }) => { const state = (row.original.workerState || 'unknown').toLowerCase() return } }, { accessorKey: 'hashrate_rt', header: 'Hashrate (TH/s)', cell: ({ getValue }) => (Number(getValue()) || 0).toFixed(2) }, { accessorKey: 'power', header: 'Power (W)', cell: ({ getValue }) => (Number(getValue()) || 0).toFixed(0) }, { accessorKey: 'temperature', header: 'Temp (°C)', cell: ({ getValue }) => (Number(getValue()) || 0).toFixed(1) } ] const { apiBaseUrl } = useMdkContext() const overview = useQuery({ queryKey: ['dashboard-overview'], queryFn: () => get(apiBaseUrl, '/overview'), refetchInterval: 3000 }) return (
data={overview.data?.devices ?? []} columns={columns} getRowId={(row) => row.deviceId} loading={overview.isLoading} enablePagination={false} />
) } ``` A controller is `async (req) => value`; a page is `useQuery` + devkit components, and neither one touches the other's plumbing. `OverviewPage` never imports `mdkClient`, and `overview.js` never imports React.
### Run it #### 6.1 Add package.json `examples/minimal-dashboard/package.json`: ```json { "name": "@your-org/mdk-minimal-dashboard", "version": "0.1.0", "private": true, "scripts": { "build": "npm --prefix ui run build", "start": "node start.js" } } ``` #### 6.2 Start the dashboard Install each package's own dependencies, build the UI once, then boot the backend: ```bash cd examples/minimal-dashboard npm install npm --prefix ui install npm run build npm run start ``` Open `http://localhost:3000/`, where the page polls `/overview` every 3 seconds and shows the one `demo-0` device reporting live telemetry from its mock. Confirm the API directly with: ```bash curl -s http://localhost:3000/overview ``` You should see JSON shaped like `{ "ts": ..., "devices": [{ "deviceId": "demo-0", "workerId": "demo-worker-1", "workerState": "READY", "hashrate_rt": ..., "power": ..., "temperature": ... }] }`. ### Hot-reload UI development Vite does **not** replace the Gateway. Without hot reload you open `:3000` (Gateway serves the built `ui/dist`). With hot reload you still need the Gateway for `/overview`, and Vite only serves the React app and proxies that path. Keep **both** terminals running: **Terminal 1, Gateway** (do not stop this): ```bash cd examples/minimal-dashboard npm run start ``` Wait for `dashboard up: http://localhost:3000/`. **Terminal 2, Vite**: ```bash cd examples/minimal-dashboard VITE_API_PORT=3000 npm --prefix ui run dev ``` Open **`http://localhost:3041/`** (the Vite port), not `:3000`. Step 5's proxy forwards `/overview` to the Gateway on `VITE_API_PORT`. If Terminal 1 is down, Vite logs `http proxy error: /overview` / `ECONNREFUSED` and the table stays empty.
## Next steps - **More Workers, zero controller changes**: `overview.js` already loops over every registered Worker generically. Register a second Worker the same way (Step 2's `startDemoWorker` + `startWhatsminerWorker`, etc., both followed by `kernel.registerWorker(...)`) and it appears in `/overview` for free. - **A write route**: Add a second manifest entry (`POST /devices/{deviceId}/command`) calling `mdkClient.sendCommand(deviceId, 'setPowerMode', { mode })`, following the `command.js` example in [Gateway plugins](/guides/gateway/plugins), and a `Button` in `OverviewPage.tsx` that `POST`s to it. Before deploying any physical write command, require narrowly scoped authorization, validate its payload and target state, apply rate limits, and record an audit trail. - **More pages, charts, history**: Add `react-router` and a second route/page, or graduate to the domain layer (`@tetherto/mdk-react-devkit/domain`'s `LineChartCard`, `MetricCard`, header stats bar) once the Gateway plugin grows a history endpoint to feed them. Compare [`examples/mvp-site/ui/src/DashboardPage.tsx`](https://github.com/tetherto/mdk/blob/main/examples/mvp-site/ui/src/DashboardPage.tsx) and [`examples/mvp-site/backend/gateway-plugins/site/controllers/history.js`](https://github.com/tetherto/mdk/blob/main/examples/mvp-site/backend/gateway-plugins/site/controllers/history.js) for that shape. - **Separate processes or hosts**: Replace the direct registration in Step 2 with Local discovery for processes on one machine or DHT discovery for separate hosts, as described in the [discovery model](/reference/worker). ## Troubleshooting | Symptom | Cause | | --- | --- | | `/overview` returns `{ devices: [] }` | `kernel.registerWorker(...)` wasn't awaited, or `startGateway` was called before `waitForDiscovery` resolved | | `/overview` returns `500` / `Cannot read properties of null` | Controller spread `tel.metrics` when `pullTelemetry` returned `null`: use `(tel && tel.metrics) \|\| {}` as in Step 3 | | `pullTelemetry` throws / device shows zeros | The Worker's `connect()` couldn't reach the mock at boot: confirm the mock's `listening` event fired before `startDemoWorker` seeded it (Step 2's `onceListening`) | | Vite shows `http proxy error: /overview` / `ECONNREFUSED`, page at `:3041` has no data | Gateway is not listening on the proxy target: keep `npm run start` running in another terminal, and set `VITE_API_PORT` to that Gateway port (default `3000`). Open `:3041`, not `:3000` | | Browser `fetch('/overview')` fails from Vite with CORS / wrong host when Gateway is up | See the CORS note in Step 4: the Vite proxy must target the Gateway (`VITE_API_PORT`); do not call a different origin from the page | | `ERR_PLUGIN_HANDLER_NOT_FOUND: routes.dashboard.overview: ./controllers/overview.js` on Gateway boot | `extraPluginDirs` must point at the directory *containing* `mdk-plugin.json`, not the controller file itself | | Gateway boots but the page 404s | `common.staticRootPath` must be an absolute path (`path.join(__dirname, 'ui', 'dist')`) pointing at a *built* UI (`npm run build` in Step 6), not a relative string or the unbuilt `ui/src` | | `Cannot find module '@tetherto/mdk-react-devkit'` when building `ui/` | Run the Prerequisites' `npm run setup:ui && npm run build:ui` from the repo root first: the UI packages ship pre-built `dist/` output that `ui/`'s `package.json` depends on via `file:` links | # Run a mining site end to end (/tutorials/run-a-site) If Kernel, Gateway, Worker, manager, or thing are unfamiliar, [terminology](/reference/glossary) defines them. ## Overview This tutorial runs the [Starter site example](https://github.com/tetherto/mdk/tree/main/examples/mvp-site) end to end: a Whatsminer worker, an Ocean pool worker, and a SATEC powermeter worker, each backed by mock hardware that speaks the real wire protocol, a Gateway HTTP API, and a React dashboard, all supervised by PM2. What you'll have at the end: - Mock miners, a mock pool, and a mock powermeter, each driven by the real Worker driver code against a localhost mock instead of hardware - A Gateway API on `:3000` serving `/site/overview`, `/site/history`, `/site/miners/:id/command`, and `/site/miners/:id/pools` - A React dashboard on `:3000` with Dashboard, Containers, Monitoring, Pools, and Control pages - One MCP surface on `:3101` exposing the site as tools for AI agents, served by a standalone MCP process: a hand-authored, agent-contract tool set alongside the site Gateway plugin's own routes, read off the same plugin dir the Gateway loads Every component above runs as its own PM2-supervised OS process, discovering the Kernel over a shared local directory rather than a DHT. ## Prerequisites - [Node.js](https://nodejs.org/) >=24 (LTS) - npm 11 [(< 12)](/reference/environment) - PM2 (`npm install -g pm2`) ### Install the example #### 1.1 Clone the repo ```bash git clone git@github.com:tetherto/mdk.git cd mdk ``` #### 1.2 Run setup ```bash cd examples/mvp-site npm run setup npm run setup:config ``` `setup` installs `backend/core`, [`backend/workers`](/reference/worker), the UI workspace devkit packages, and this example's own dependencies. `setup:config` copies the committed `*.json.example` config files into place without overwriting any that already exist. The script walks several workspaces; first run takes 1-2 minutes. ### Start the site ```bash npm start ``` `start.js` generates `deploy/ecosystem.config.js` from `config/site.deploy.json`, starts the PM2 apps, then exits — PM2 itself keeps the processes running in the background. ```bash pm2 list # mocks, mocks-ocean, mocks-satec, kernel, worker, worker-ocean, worker-satec, gateway, mcp ``` Wait for every app to show `online`, then open `http://localhost:3000/` in a browser. The dashboard shows live hashrate, power, and per-device status. PM2 showing every app `online` means the processes started, not that the site has fully settled. The Ocean pool worker ticks its mock fetch/save cycle every 10 seconds, and Kernel's local-discovery scan runs every 4 seconds, so `/site/overview` can report `"pools": 0` for 15-20 seconds after `pm2 list` goes green. Retry the command below if the first response comes back short. Verify via the API: ```bash curl -s http://localhost:3000/site/overview | jq '{miners: (.miners|length), pools: (.pools|length), powermeters: (.powermeters|length)}' ``` Expected output: ```text { "miners": 5, "pools": 1, "powermeters": 1 } ``` `config/devices.json` seeds five miners and one powermeter by default. Add or remove entries there to resize the fleet; [configuring devices](https://github.com/tetherto/mdk/blob/main/examples/mvp-site/README.md#configure-devices) describes the format. ### (Optional) Explore the UI pages The Dashboard, Containers, Monitoring, Pools, and Control pages are all served from `ui/dist` via the Gateway on the same `:3000` port. The Pools page reads the Ocean pool worker's stats; the Monitoring page charts the SATEC meter's power series over time. See [UI pages](https://github.com/tetherto/mdk/blob/main/examples/mvp-site/README.md#ui-pages) for what each one shows. ### (Optional) Connect an AI agent over MCP Agents and connection modes vary; apply the method for your agent. For Claude CLI: `cd examples/mvp-site` `claude` Accept `Use this MCP server` Then your agent can query and act on the site's devices. The example exposes MCP as a single surface on `:3101`, served by a standalone MCP process and listed in `.mcp.json.example`. It carries two tool sources: - A hand-authored tool set with agent-contract metadata — `backend/mcp-plugins/site/mcp-plugin.json` — giving an agent summary-first, closed-vocabulary tools (`summarize_site`, `count_devices`, `list_devices`, `get_device`, `rank_devices`, `act_device`) instead of raw route exports - The site Gateway plugin's own `/site/*` routes, read off the same plugin dir the Gateway loads and converted into tools Point an MCP client at that URL and it can query the fleet or, with the agent-contract tools, act on it with operator approval. This local site runs an empty Kernel allowlist; a hardened Kernel restricts the MCP connection through [Kernel's caller allowlist](https://github.com/tetherto/mdk/blob/main/examples/mvp-site/README.md#troubleshooting). ### Stop the site ```bash npm run stop:pm2 ``` This stops and removes every PM2-managed process for this site: mocks, Workers, Kernel, Gateway, and the MCP server. The PM2 daemon itself (`pm2 list` still shows a `God Daemon` process) stays running in the background after this — that's expected, since PM2 manages processes across every project on the machine, not just this one. If it ever becomes unresponsive, see [stale PM2 processes](https://github.com/tetherto/mdk/blob/main/examples/mvp-site/README.md#stale-pm2-processes-after-system-crash) for `pm2 kill`, which stops PM2 itself, not just this site. ## What just happened 1. **Setup** installed `backend/core`, [`backend/workers`](/reference/worker), the MDK UI devkit, and this example, then `setup:config` seeded its local config from the committed `*.example` files. 2. **Mock hardware**: PM2's `mocks`, `mocks-ocean`, and `mocks-satec` roles each started a mock device server — a Whatsminer miner, an Ocean pool, and a SATEC powermeter — speaking the real wire protocol, so the Worker drivers run their true connect, collect, and command paths against them. 3. **Kernel**: the `kernel` role started the orchestration layer in local-discovery mode, watching a shared directory for Workers to publish their RPC keys to. 4. **Workers**: the `worker`, `worker-ocean`, and `worker-satec` roles each dispatched to that family's boot function (`startWhatsminerWorker`, `startOceanPoolWorker`, `startSatecWorker`) to construct a `WorkerRuntime`, seed its devices from `config/devices.json`, and publish its RPC key for the Kernel to discover. 5. **Gateway**: the `gateway` role mounted the site plugin declared by `backend/gateway-plugins/site/mdk-plugin.json` and the built UI from `ui/dist`, then opened the HTTP server on `:3000`. The plugin aggregates data across the three Workers through `mdkClient`. 6. **MCP**: the `mcp` role started the standalone MCP process on `:3101`, serving both tool sources — the hand-authored agent-contract tool set and the site Gateway plugin's own routes, derived from the same plugin dir the Gateway loads. ## Resetting state State (Kernel key, Worker seeds, device registry) persists in `.site-data/`. If you change `config/devices.json` after the first run, [reset it](https://github.com/tetherto/mdk/blob/main/examples/mvp-site/README.md#reset-state) before restarting — seed devices are only registered once, on an empty store. ## Next steps - Fix a failed boot, a port clash, or stale PM2 processes with the example's troubleshooting section - Register real hardware instead of mocks by [configuring devices](https://github.com/tetherto/mdk/blob/main/examples/mvp-site/README.md#configure-devices) - Build the same shape from an empty directory with one Worker and one route: [build a minimal single-page dashboard](/tutorials/build-a-dashboard) - Serve your own data by [adding custom plugins to the Gateway HTTP API](/guides/gateway/plugins) - Integrate your own hardware by [building a third-party Worker](/guides/workers/build-a-worker) - Run the same PM2-supervised model as a production deployment, or across hosts with DHT discovery, with the [supervised-services deployment guide](/guides/deployment/run-all-workers-site) - Go beyond querying the site's MCP tools by hand: connect [the conversational operator agent](/guides/agent), which calls the same tools and gates writes behind human approval - See the full 11-worker fleet — three miner families, containers, sensors, and two pools — in [`examples/full-site`](https://github.com/tetherto/mdk/tree/main/examples/full-site) - Browse [every runnable example in one place](https://github.com/tetherto/mdk/blob/main/examples/backend/README.md) # Build a dashboard with an agent (/tutorials/ui/react/build-any-dashboard-with-an-agent) This tutorial walks a **realistic agent session** end to end, using the same flow every MDK agent session uses: [put `mdk` on your PATH](/guides/cli), [install the skill suite once](/guides/ui/agent-skills), then state what you want in plain language. ## Overview MDK's React UI Devkit's library of presentational and composable building blocks can be used wherever suits you. Mining is one of many disciplines that benefits from charts, tables, tabs, and stat cards. Your telemetry; your dash. You bring *your* labels, *your* mock data, and *your* layout. The first thing we built was [a mining dashboard](/tutorials/build-a-dashboard). What will you build? IoT fleet backend reporting, workout metrics motivation app, weather stats? Your data; your choice. You'll ask for a **statistics tutorial page** so students can explore distributions, trends, and raw grades, and see every: - Lookup the agent makes against the [skill suite](/guides/ui/agent-skills) - Resulting page code - Run instructions We use mock JSON in the browser. No Kernel, Worker, pool API, or fleet backend is required. Presentational imports from `@tetherto/mdk-react-devkit/core` are enough for a highly visual, shippable UI. ## What you'll learn 1. **Domain is yours.** Component contracts describe shape and behavior (chart datasets, table columns), not industry vocabulary. 2. **The agent path is unchanged.** Install the CLI → install the skills → plain-language intent → local registry lookup → build → typecheck. 3. **Visual density is supported.** Bar charts, line trends, sortable tables, and stat cards compose into a dashboard that *looks* like a product, not a wireframe. The demo app is **Stats Lab**: a fictional intro statistics course where Teaching Assistants review quiz score histograms, weekly class averages, and per-student grades. ## Prerequisites - `mdk` on your PATH: [Install the CLI](/guides/cli) - The MDK React packages installed and wired: [Install and wire the React packages](/guides/ui/install)
This walkthrough uses `apps/stats-lab`, same pattern as [Install and wire the React packages](/guides/ui/install).
## Agent session walkthrough ### Install the skill suite (once per project) `cd` to your app or monorepo project root. Do not run this from the CLI checkout: ```bash mdk skill add --client cursor ``` This installs the MDK Developer Skill suite into `.cursor/skills/` (use `--client claude` for `.claude/skills/`, or omit `--client` for both), so the session knows MDK's conventions and carries the component registry. Full flag list: [Agent skills](/guides/ui/agent-skills#install). ### State a non-mining intent Paste a prompt that names the domain explicitly so the agent does not reach for hashrate widgets: > Build a **statistics tutorial dashboard** for students in `apps/stats-lab`. Include: > > - A **histogram** of final exam scores (bar chart buckets 50–59 through 90–100) > - A **line chart** of weekly class average over six weeks > - A **sortable table** of students with midterm, final, and section > - Three **summary stat cards**: mean final, median final, enrollment count > > Use mock data in the repo. Import only from `@tetherto/mdk-react-devkit/core` and `@tetherto/mdk-react-devkit/foundation` where > needed. No mining APIs. The agent's job is the same whatever the domain: discover exports, scaffold, and verify compile. ### Lookups the agent makes Behind the prompt, the `mdk-ui-component` skill activates and a well-behaved session reads its bundled `references/ui-registry.json` — a deterministic file lookup, no model or network calls. A representative pass: The intent names four visuals, so the agent resolves each against `indexes.componentsByCategory` in the registry: | Intent | Category | Candidates | |--------|----------|------------| | Histogram of exam scores | `charts` | `BarChart` | | Weekly average trend | `charts` | `LineChart` | | Sortable student list | `tables` | `DataTable` | | Mean, median, enrollment | `cards` | `SingleStatCard` | Because we asked for mock data only, the agent skips the adapter hooks like `useDevices` that a live-data page would bind. For each shortlisted component the agent opens its `indexes.componentsByName` entry and copies the prop list verbatim — names, types, and which are required. That is where real shapes like `LineChartData`'s millisecond `x` values and `DataTableColumnDef` accessors come from, instead of an invented API. Each entry also points at the component's `USAGE.md` and its runnable `*.example.tsx`, both shipped in the devkit, for the cases where the prop table alone is not enough. The skill's layer model puts mock datasets and labels in the page, and the visuals in panels that take props. Nothing is scaffolded from a template — the agent writes the page against the registry's contracts, then verifies: ```bash npm run typecheck ``` Type-checking against the real package barrels catches a wrong prop name or shape immediately; the app build in the local run verifies the final Vite project. Full skill reference: [Agent skills](/guides/ui/agent-skills). ### Review the generated page After the agent edits `App.tsx`, a Stats Lab dashboard might look like this: ```tsx BarChart, LineChart, DataTable, Tabs, TabsList, TabsTrigger, TabsContent, } from '@tetherto/mdk-react-devkit/core' type Student = { id: string name: string section: 'A' | 'B' midterm: number final: number } const students: Student[] = [ { id: '1', name: 'Alex Kim', section: 'A', midterm: 82, final: 88 }, { id: '2', name: 'Jordan Lee', section: 'B', midterm: 74, final: 79 }, { id: '3', name: 'Sam Rivera', section: 'A', midterm: 91, final: 94 }, { id: '4', name: 'Taylor Ng', section: 'B', midterm: 68, final: 72 }, { id: '5', name: 'Casey Park', section: 'A', midterm: 85, final: 90 }, ] const weekStart = (weekIndex: number): number => new Date(2025, 0, 6 + weekIndex * 7).valueOf() const StatsLab = (): React.JSX.Element => { const [sorting, setSorting] = useState([]) const finals = students.map((s) => s.final) const meanFinal = finals.reduce((a, b) => a + b, 0) / finals.length const sortedFinals = [...finals].sort((a, b) => a - b) const medianFinal = sortedFinals[Math.floor(sortedFinals.length / 2)] const histogram = useMemo( () => ({ labels: ['50–59', '60–69', '70–79', '80–89', '90–100'], datasets: [ { label: 'Students', data: [1, 2, 4, 6, 3], backgroundColor: '#6366f1', }, ], }), [], ) const weeklyAverage = useMemo( () => ({ datasets: [ { label: 'Class average', borderColor: '#22c55e', data: [71, 74, 76, 79, 81, 83].map((y, i) => ({ x: weekStart(i), y, })), }, ], }), [], ) const columns: DataTableColumnDef[] = [ { accessorKey: 'name', header: 'Student' }, { accessorKey: 'section', header: 'Section' }, { accessorKey: 'midterm', header: 'Midterm' }, { accessorKey: 'final', header: 'Final' }, ] return (

Stats Lab

Intro statistics — interpret distributions and trends

Charts Roster

Final exam distribution

Weekly class average

) } ```
Nothing in this file references pools, workers, or TH/s. The same components appear on mining pages because the **data model is generic**.
## Run Stats Lab locally Follow the same monorepo workflow as [installing and wiring the React packages](/guides/ui/install). If you already did that, skip to **Run the app** with `stats-lab` as the workspace name. ### Clone and build the UI monorepo ```bash git clone https://github.com/tetherto/mdk.git cd mdk ``` ```bash git clone git@github.com:tetherto/mdk.git cd mdk ``` ```bash npm install npm run build ``` ### Scaffold `apps/stats-lab` ```bash cd apps npm create vite@latest stats-lab -- --template react-ts cd stats-lab ``` Add workspace dependencies to `package.json`: ```jsonc "@tetherto/mdk-react-devkit": "*", "@tetherto/mdk-react-adapter": "*", "@tetherto/mdk-ui-foundation": "*", ``` Install from the monorepo root: ```bash cd ../.. npm install ``` ### Wrap with `MdkProvider` In `apps/stats-lab/src/main.tsx`, mirror [Wrap your app in MdkProvider](/guides/ui/install#wrap-your-app-in-mdkprovider): ```tsx // … ``` Mock data does not call the API; the provider satisfies components that expect React context. ### Run the app From the monorepo root: ```bash npm -w stats-lab run build ``` Then start Vite: ```bash npm -w stats-lab run dev ``` Or from the app folder: ```bash cd apps/stats-lab npm run dev ``` Open the URL Vite prints (typically `http://localhost:5173`). You should see **Stats Lab** with histogram, trend line, stat cards, and a sortable roster tab. ### Optional: compare with the MDK demo app Same commands as in the [install guide](/guides/ui/install): ```bash npm run dev:catalog ``` Open [http://localhost:5173/mdk](http://localhost:5173/mdk) to browse mining-oriented examples, then contrast with Stats Lab: **same primitives, different story**. ## Why the agent stays accurate The registry lists real exports with their prop types and usage docs, so the agent can't invent props that don't exist. `npm run typecheck` remains the final verification against the real component APIs, and the app build catches the rest. ## Next steps - [Install and wire the React packages](/guides/ui/install): the provider, hooks, and theming your generated pages rely on, and the manual path if you'd rather not use an agent - [Agent skills](/guides/ui/agent-skills): the skill suite behind the lookups above - [Chart components](/reference/ui/components/charts): `BarChart`, `LineChart`, and related data shapes - [Data display](/reference/ui/components/display): `DataTable` sorting and pagination