# 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={ One command brings up Workers, mock hardware, a Gateway API, and a live dashboard } /> I'm an AI agent |{' '} I am building with one } subtitle="Optimize your development workflow by bridging the gap between large language models and high-performance mining infrastructure." > } title={Build dashboards with your AI agent} href="/tutorials/ui/react/build-any-dashboard-with-an-agent" description={ Wire your LLM to MDK with the UI CLI, then build from plain-language prompts } /> } title={Full docs in one file} href="/llms-full.txt" description={ Every page of these docs in one plain-text file—open in the browser to copy or save } /> # About MDK (/concepts) ## Introducing MDK MDK, the Mining Development Kit, is an [open-source platform](/support/community/contributing#licensing) that delivers a modern, transparent, and modular infrastructure for Bitcoin mining operations. MDK enables Bitcoin mining operations to start small, scale smoothly, and remain in full control, without lock-in, rewrites, or hidden complexity. ## The problem The Bitcoin mining industry has long been constrained by closed systems, proprietary tooling, and vendor lock-in. MDK changes that. ## The solution MDK delivers a modular mining stack that empowers operators and developers to build, monitor, control, and scale mining operations with full ownership: from a single device to gigawatt-scale facilities — without architectural rewrites. MDK ships three packages: 1. [Orchestration kernel (Kernel)](#the-orchestration-kernel). 2. [Universal SDK](#the-universal-sdk). 3. [MDK App Toolkit](#mdk-app-toolkit). All three communicate through the **MDK protocol**. Clients — browsers and [AI agents](#ai-ready-with-unified-intelligence) alike — reach the kernel exclusively through the Gateway, the secure entry point your team builds with the SDK. Tying everything together is a **single contract per device type**: the same `mdk-contract.json` serves the UI (data labels), the orchestrator (validation rules), and AI agents (reasoning context). One file, three audiences, no drift. ### The orchestration kernel Kernel, the Orchestration Kernel, is distributed as [`@tetherto/mdk-kernel`](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md). It's the central coordination engine of MDK and serves as a controller: it knows which devices are online, routes commands to the right place, monitors health, and collects performance data. `@tetherto/mdk-kernel` communicates with devices through a standardized language called the **MDK Protocol**, a common set of messages that every device in the system understands, regardless of manufacturer or model. Adding a new device type never impacts `@tetherto/mdk-kernel` thanks to the Worker, a device-specific translator that sits between the kernel and your hardware: it speaks the MDK Protocol upward, and the device's native API downward. The kernel is **pull-only**, **device-agnostic**, and **self-healing**. Learn more about the [internal modules, recovery flows, and protocol specs](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md#architecture) that back those guarantees. ### The universal SDK `@tetherto/mdk-client` is the universal SDK, a connection library that applications use to talk to `@tetherto/mdk-kernel`. It serves as a universal adapter: handling all the connection details so developers can focus on building their application. - **Multi-language support**: available for Node.js, Python, Go, and more; use whatever language your team prefers - **Automatic connection handling**: manages reconnection, retries, and transport selection behind the scenes - **No lock-in**: developers bring their own stack and connect via the SDK. No framework requirements. ### MDK App Toolkit For teams that want to ship fast, the **MDK App Toolkit** 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` for React today), and a production-tested React UI Kit (`@tetherto/mdk-react-devkit`) for dashboards. - **Backend tools**: a plug-and-play library that drops into Fastify or Express to handle command proxying and request-level caching, with hooks for custom routes and aggregations. - **Plugins**: drop-in modules that pair a frontend tools widget with a backend tools route, so third parties can ship whole features without forking the Gateway. Using [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) without the Gateway is technically possible but not supported by this monorepo — most applications build on the Gateway. ## Who MDK is for MDK is built for everyone involved in mining Bitcoin: - **Mining operators**: monitor and control fleets with real-time dashboards. Get fleet-wide summaries (total hashrate, power usage, temperature alerts) across all your sites. - **Hardware manufacturers**: integrate new devices by building a Worker and writing one `mdk-contract.json`. No involvement from MDK maintainers needed. - **Software developers**: build custom mining applications in any language, or leverage the MDK App Toolkit's frontend and backend tools for rapid development. - **AI/Automation teams**: [connect intelligent agents](#ai-ready-with-unified-intelligence) that can monitor, diagnose, and act on device issues autonomously ## Architecture overview `@tetherto/mdk-kernel` is [the kernel](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/README.md). [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) is the protocol connector every caller uses to reach it. Above those two layers, the supported development path builds in two levels: - **Gateway**: the Gateway 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 through the standalone [`@tetherto/mdk-mcp`](https://github.com/tetherto/mdk/blob/main/backend/core/mcp/README.md) package - **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. For the full layer-by-layer view with transports and discovery flows, see the [MDK stack](/concepts/architecture#the-round-trip) on the Architecture page. ## AI-ready with unified intelligence MDK is designed from the ground up for AI-driven operations. 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`) contains: - **Safety rules**: for example, "Outlet temperature > 85°C requires immediate intervention" - **Operational constraints**: limits on command frequency, power thresholds, cooling requirements - **Troubleshooting guides**: if/then recovery steps that AI agents can follow autonomously This means an AI agent connecting to MDK doesn't need a separate knowledge base or custom prompts per device. The intelligence travels with the device; the same contract that validates commands and generates dashboards also determines how AI reasons about that hardware. ## What you can build - Operational dashboards (hashrate, power, temperature) - Multisite fleet management with centralized oversight - Alerts and notifications for critical device events - Overheating detection and automated remediation - AI-driven autonomous monitoring and control - Custom analytics and reporting pipelines - White-labeled hosted mining platforms - Third-party device integrations and plugins ## Scaling MDK [scales](/concepts/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. A single Gateway connects to all of them, giving you one view across your entire operation. - **Site isolation**: `@tetherto/mdk-kernel` instances are fully independent. A problem at one site has zero impact on any other. ## Next steps Learn more about: - [Architecture](/concepts/architecture) - MDK App Toolkit - Connecting intelligent agents # Architecture (/concepts/architecture) ## How MDK works MDK is built around ownership, not layers. Three tiers, each owned differently: - **Kernel is invariant.** One small coordination layer every deployment runs unchanged: it routes validated commands to whichever Worker owns a device, and pulls telemetry back. It has no plugin system and no per-deployment configuration of its routing behavior. - **Extensions are yours.** Worker plugins wrap a device family; Gateway plugins add HTTP routes, aggregation, and auth. Both are code you write and own, isolated from the kernel and from each other. - **The UI devkit is optional.** Nothing above the Gateway is required: a deployment can dispatch commands and pull telemetry with only `@tetherto/mdk-client` and no Gateway or UI at all. ## 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 K["Kernel"] subgraph wk ["Worker"] WP["Worker plugin\n(extension point)"] end Devices[("Devices")] Agent --> GP Dashboard --> GP GP <-->|"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 calls a Gateway plugin's route. That plugin builds its own `@tetherto/mdk-client` and dispatches a command through it. Kernel 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 plugin** teaches Kernel about one device family; a **Gateway plugin** teaches the Gateway a new route. Kernel itself never changes. ## The three tiers **Gateway**: 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) and the only tier where *user*-level authentication, aggregation, and business logic live. Kernel's own allowlist, when configured, gates which *connections* it accepts: a transport-level check, not a user identity. **Kernel**: 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. **Workers**: 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. ## 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; telemetry is never pushed to a caller or streamed: it is always a pull, whether triggered on demand by a caller or by Kernel's own scheduled cadence, and read back through the same round trip described above. A dropped connection is the client's problem to recover from: the next call through `@tetherto/mdk-client` reconnects; Kernel does not buffer or replay what it couldn't deliver while the 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. See [the storage model](/concepts/the-storage-model) for the full picture, including what Kernel's write-command log guarantees on restart. ## 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 [what an app is](/concepts/whats-an-app) in MDK terms - Understand [scalability](/concepts/scalability): parallel Workers, parallel Kernels, and what's measured today ## 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 [what an app is](/concepts/whats-an-app) in MDK terms - Understand [scalability](/concepts/scalability): parallel Workers, parallel Kernels, and what's measured today # Scalability (/concepts/scalability) ## The axes Three independent numbers describe how big an MDK deployment is: - **Devices per Worker plugin**: how many devices one Worker instance 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**: how many Kernel instances one Gateway (and, above it, one AI agent) connects to at once. This page is about *how many* Workers and Kernels a deployment runs, not *how those processes are packaged* on a host (one process versus many machines). That's a [deployment topology](/guides/deployment) choice, made independently of the numbers on this page. ## Single-kernel versus multi-kernel The one topology distinction this page owns: does your deployment run **one Kernel serving a site**, or **several Kernels, each serving its own site, behind one Gateway**? - **One Kernel** is the default and the right choice until you have a concrete reason to split: a single Kernel process routes commands and telemetry for every Worker registered to it, with no per-Worker partitioning. - **Several Kernels**, one per physical site, is the shape for multi-site operations (for example, a Texas site and an Iceland site). Each Kernel is fully isolated: Kernel instances do not federate registries, share queues, or synchronize state with each other. A crash at one site has zero effect on any other. A single Gateway (and, above it, an AI agent) connects to all of them and merges results in its own controller code: that aggregation is Gateway-layer work, not something Kernel does for you. ## When to add a Kernel Add a second Kernel when you're adding a second physically- or organizationally-distinct site, not because one Kernel is running out of capacity for a single site's device count: Kernel is not currently known to bottleneck at realistic single-site device counts (see the benchmark table below for what's actually been measured). Splitting Kernels for a single site buys you nothing: you'd gain isolation you don't need and lose the single registry that makes routing simple. ## What serializing Workers and Kernels means 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. 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 See [the storage model](/concepts/the-storage-model) for the full picture. In short: each Kernel keeps its own separate store: 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 normally. - A Kernel crash is recovered from its own command write-ahead log on restart (`recover()` sweeps non-terminal command states); it does not need to reconstruct device state, since it never owned it. - In a multi-Kernel deployment, one site's Kernel going down has no effect on any other site's Kernel: there is no shared state to become inconsistent. ## Benchmarks pending A real benchmark harness exists (`backend/tests/benchmark/`) and can measure device counts, telemetry throughput, and command latency at a given topology, but no baseline numbers are committed yet. This table reserves the shape for when they are: | Topology | Devices | Telemetry throughput | Command latency (p50/p99) | | --- | --- | --- | --- | | Single Kernel, single Worker | *pending* | *pending* | *pending* | | Single Kernel, N Workers | *pending* | *pending* | *pending* | | Multi-Kernel (per-site) | *pending* | *pending* | *pending* | ## 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 ## Next steps - Understand [the storage model](/concepts/the-storage-model) - Choose a [deployment topology](/guides/deployment) - Understand [architecture](/concepts/architecture) # The integration model (/concepts/the-integration-model) ## Two extension points, named after what they extend | | Worker plugin | Gateway plugin | | --- | --- | --- | | Extends | The Worker tier | The Gateway tier | | Declares itself with | `mdk-contract.json` | `mdk-plugin.json` | | Read by | Kernel (routing, validation), the UI (labels), agents (reasoning context) | 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` | The naming is literal: a **Worker plugin** is a plugin for a Worker, a **Gateway plugin** is a plugin for the Gateway. Neither extends Kernel: Kernel has no plugin system, by design (see [Architecture](/concepts/architecture)). ## The contract: one file, three audiences A Worker plugin's `mdk-contract.json` is read by three different consumers, none of which see a different copy: - **Kernel** reads `capabilities.telemetry[]`/`commands[]` to validate that a command a caller sends is one the Worker actually declared, and to route by device family for write-permission checks (`miner:w`, `container:w`). - **The UI** reads the same telemetry/command names and units to label a value without hard-coding a device's vocabulary. - **An AI agent** reads the same contract at runtime to derive its tool set: a new device family means new agent tools with no change to the agent's own code (see the MCP server and runtime tool derivation). One file, no drift between what the kernel enforces, what the UI shows, and what an agent can reason about. ## `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. ## Contract versioning today There is no enforced compatibility mechanism between contract versions today. A Worker plugin's `mdk-contract.json` has no version field of its own: the package's `package.json` semver is the only version signal, and nothing in Kernel or the Gateway checks it against a caller's expectations. In practice this means: a contract's shape is whatever the currently-loaded plugin declares, and there's no compatibility gate protecting a caller written against an older shape. If a contract's telemetry or command names change, that's a breaking change for anything built against the old names, with no automated warning today. ## 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 ## Next steps - [Build a Worker plugin](/guides/workers/build-a-worker) for a new device family - Understand [the storage model](/concepts/the-storage-model) - Understand [architecture](/concepts/architecture) # The storage model (/concepts/the-storage-model) ## Where data lives | Data | Lives where | Engine | | --- | --- | --- | | Live device state | The device itself | N/A: Kernel and Workers hold no independent copy of "truth" | | 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 (see [The integration model](/concepts/the-integration-model) for what a Worker plugin controls) | | Credentials and per-device config | Wherever the Worker plugin author put them | Worker-defined | Kernel is intentionally not the place telemetry lives: its own store code says so directly: "Telemetry storage is intentionally NOT here... Workers own their own telemetry storage." Kernel's Hyperbee holds three things only: the Worker/device registry, published capabilities, and the write-command log. ## 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 never overrides what a Worker reports. - **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 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, not a time-series database or a cloud store MDK's storage choices favor **local-first, zero-external-dependency operation** over the query flexibility 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 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: see [Scalability](/concepts/scalability) for what that costs as a fleet grows. ## 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. ## Under growth and 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 (see [Scalability](/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 ## Next steps - Understand [the integration model](/concepts/the-integration-model) - Understand [architecture](/concepts/architecture) - Understand [scalability](/concepts/scalability) # 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 ["Reused: you don't write this"] 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 plugin(s)"] GP["Gateway plugin(s)"] UI["UI or headless consumer"] end Spec --> Kernel Spec --> Gateway WP --> Kernel GP --> Gateway UI --> Gateway Gateway --> Devkit 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 plugin(s)**: yours if you're integrating a new device family; reused if you picked one that already ships (an Antminer worker, a demo worker). - **Gateway plugin(s)**: 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 - [Build an app](/tutorials/build-a-dashboard) from an empty directory ## Next steps - Understand [the integration model](/concepts/the-integration-model) - Understand [architecture](/concepts/architecture) - [Build an app](/tutorials/build-a-dashboard) # Guides (/guides) } title="Run an MDK site" href="/guides/deployment" description="Choose a deployment topology and run a production or single-process MDK site" /> } title="Gateway" href="/guides/gateway" description="Run, configure, and extend the MDK Gateway" /> } title="Miner Workers" href="/guides/miners" description="Connect Antminer, Avalon, or Whatsminer hardware to an MDK stack" /> } title="UI" href="/guides/ui" description="Compose reporting layouts and wire the React UI Devkit into your app" /> # 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 covers the concepts; these guides cover running it. If Gateway, Kernel, or plugin are unfamiliar, read [terminology](/reference/glossary) first. ## Choose a guide | Goal | Guide | | --- | --- | | Run the agent as a standalone CLI, for local development or evaluation | `backend/core/agent/README.md` | | Deploy the agent behind the Gateway as a chat API for an operator UI | [Deploy the agent behind the Gateway](/guides/agent/gateway-deployment) | | Expose a plugin's own routes to the agent as tools | [Expose data to the agent](/guides/agent/expose-data) | ## Next steps - Understand the agent as a stack component - Understand the Gateway as a development surface # 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 automatically when mounted with `autoGenerateMcp: true` — no separate MCP manifest to author and keep in sync with the plugin's own routes. ## Prerequisites - The [Gateway is running](/guides/gateway/run) - A [Gateway plugin](/guides/gateway/plugins) already mounted via `extraPluginDirs` ### Auto-generate tools from a plugin Pass `{ dir, autoGenerateMcp: true }` instead of a plain path to also expose a plugin's HTTP routes as MCP tools: ```js await startGateway({ kernel, port: 3000, extraPluginDirs: [ { dir: path.join(__dirname, 'plugins/custom-metrics'), autoGenerateMcp: true } ], mcp: { port: 3100 } }) ``` 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 and live `mdkClient` connection serve both interfaces. The Gateway starts one in-process MCP server (Streamable HTTP, default port `opts.port + 100`) covering every auto-generated tool across all mounted plugins. ### Write tools by hand instead A plugin that needs a different tool granularity, richer descriptions, or direct `mdkClient` calls can still author an `mcp-plugin.json` by hand and run it with a standalone [MCP server](https://github.com/tetherto/mdk/blob/main/examples/full-site/docs/mcp-server.md). ## 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 # Deploy the agent behind the Gateway (/guides/agent/gateway-deployment) ## Overview `@tetherto/mdk-plugin-agent` mounts `@tetherto/mdk-agent` behind the Gateway as a chat API. It is not a separate product to adopt: enabling the plugin brings session, message, and approval routes with it, and every write the agent proposes pauses for an operator's decision. The agent itself still reaches fleet data the way any AI agent does, over the standalone MCP server; this plugin only gives a human operator a chat surface to talk to it through. This is one of two ways to run the agent: for the standalone CLI path, or to compare the two, start from [the agent guide chooser](/guides/agent). ## Prerequisites - The [Gateway is running](/guides/gateway/run) - The plugin is selected during `mdk onboard`, or mounted directly through `extraPluginDirs` - `config.agent` is populated with a model provider, an MCP server url, and an approval timeout - An MCP tool server is reachable, so the agent has fleet tools to call ### Mount the plugin #### 1.1 Select it during onboarding `mdk onboard` lists `mdk-plugin-agent` in its Gateway plugin catalog. Its entry carries a real `repoPath` (`backend/plugins/agent`), not a stub, so selecting it installs a working plugin rather than a placeholder. #### 1.2 Or mount it directly Pass its directory through `extraPluginDirs`, 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`: ```js const path = require('path') const { startGateway } = require('@tetherto/mdk/backend/core/mdk') await startGateway({ kernel, port: 3000, extraPluginDirs: [ { dir: path.join(__dirname, ''), // backend/plugins/agent in this checkout config: { agent: { // 'qvac' is the only implemented provider kind, required even for a non-QVAC endpoint; // 'external' mode just wraps any OpenAI-compatible chat-completions endpoint at baseURL provider: { kind: 'qvac', mode: 'external', model: 'qwen3-4b', baseURL: 'http://127.0.0.1:11500/v1' }, mcp: { url: 'http://127.0.0.1:3008/mcp' }, approvalTimeoutMs: 120000 } } } ] }) ``` 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` block answers `503 ERR_AGENT_UNAVAILABLE` instead of failing to load. ### Create a session and send a message Use the port `startGateway({ port })` was given: `3000` in the snippet above, `3007` if this is mounted alongside the full-site example. ```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. ## Next steps - Read the agent plugin's route reference: session, message, and approval routes, plus the manifest's `setup` fields - Understand the underlying agent: 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 # Install the CLI (/guides/cli/install) `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 - [git](https://git-scm.com/) `@tetherto/mdk-cli` isn't published to npm yet — install from a tagged source checkout. ## Install ```bash git clone --branch v0.7.0 https://github.com/tetherto/mdk.git && cd mdk/packages npm install cd cli && npm run build && npm link ``` ## Verify ```bash mdk --version # 0.7.0 ``` ## Command groups | 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` | See the full flag reference in the CLI's command surface. ## Update ```bash git pull && npm install && npm run build ``` ## Uninstall ```bash npm rm -g @tetherto/mdk-cli ``` ## Next [Try the demo](/tutorials/run-a-site) to see a stack running end to end. # 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#containers). 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 >=24 (LTS) - npm >=11 - 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/backend/core/mdk') 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-texas-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/backend/core/client') const client = createMdkClient({ hrpc: { key: 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/backend/core/mdk') 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/backend/core/client') const client = createMdkClient({ hrpc: { key: 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. ## Choose a guide - [Single-process](/guides/deployment/run-single-process-site) — run Kernel, Gateway, and Workers in one Node.js process - [Supervised services](/guides/deployment/run-all-workers-site) — run a multi-Worker site as separate, PM2-supervised processes, from one machine up to a cross-host deployment ## Next steps - Understand the trade-offs before you choose your deployment topology - Browse the [functions](https://github.com/tetherto/mdk/blob/main/backend/core/mdk/README.md) that wire together the Kernel, device Workers, and the Gateway HTTP # Run the supported Worker fleet with mock devices (/guides/deployment/run-all-workers-site) This page directs you to the correct location for the prerequisites, run command, smoke test, and troubleshooting. ## Overview Use this example when you want 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` 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 before building a site config — [Run a miner Worker](/guides/miners) - 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, device Workers, and the Gateway 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 through the standalone [`@tetherto/mdk-mcp`](https://github.com/tetherto/mdk/blob/main/backend/core/mcp/README.md) package instead of the Gateway's HTTP surface. If Gateway, Kernel, or plugin are unfamiliar, read [terminology](/reference/glossary) first. For the full developer model — extension, data access, auth design — read the Gateway concept page. ## Choose a guide | Goal | Guide | | --- | --- | | Start the Gateway for the first time | [Run the Gateway](/guides/gateway/run) | | Use built-in plugins or build your own | [Gateway plugins](/guides/gateway/plugins) | | Stop Kernel, Gateway, and Workers cleanly | [Tear down MDK services](/guides/gateway/teardown) | | Operator in the loop: submit and approve write actions | [Submit and approve write actions](/guides/gateway/write-actions) | ## Next steps - Understand the Gateway as a development surface - 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 # Gateway plugins (/guides/gateway/plugins) ## Overview The Gateway exposes HTTP routes through a declarative plugin system. Each plugin is a directory containing an [`mdk-plugin.json`](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md#manifest-format) manifest and one or more controller files. MDK ships a set of default plugins that load automatically; you can mount additional plugins for your own site logic. 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) ## Default plugins MDK ships plugins that load automatically on Gateway startup: - The `telemetry` plugin serves site metrics (hashrate, consumption, efficiency, temperature, and more) - The `site-hashrate` plugin serves aggregated site hashrate history - The `site-monitor` plugin serves site configuration, feature flags, and live per-device hashrate 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 mounting it via `extraPluginDirs` 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. The [plugin reference](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md) lists every route each of these plugins serves, with its method, generated from the plugin's `mdk-plugin.json`. Plugins you mount yourself are documented by their own manifests. ### Mount a plugin Pass an `extraPluginDirs` array to `startGateway()` to load additional plugins at boot alongside the default plugins: ```js const { startGateway } = require('@tetherto/mdk/backend/core/mdk') await startGateway({ kernel, port: 3000, extraPluginDirs: [ path.join(__dirname, 'plugins/custom-metrics'), path.join(__dirname, 'plugins/alerts') ] }) ``` Each entry must be an absolute path to a directory containing an [`mdk-plugin.json`](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md#manifest-format). The plugin loader validates the manifest and all handler files at startup — missing files or invalid manifests throw immediately before the server comes up. [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 an `http` block with a `method` and `path`. 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 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 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 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 | ### 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:
Migrate from the `services` parameter (pre-0.7) A controller used to take `(req, services)`, a `services` object the Gateway passed to every plugin. | Before | After | | --- | --- | | `module.exports = (req, services) => …` | `module.exports = (req) => …` | | `services.conf` | `config` from `require('@tetherto/mdk-gateway/plugin')` | | `services.mdkClient` | The plugin builds its own from `config.kernelKey` / `config.kernelBootstrap` | | `services.dataProxy` | Removed with the data proxy | | `services.authLib` | Removed in 0.6.0 | Drop the second handler parameter, read `config` from the context module, and build your own MDK client for Kernel access — the bundled `site-monitor`, `site-hashrate`, and `telemetry` plugins each ship a `lib/client.js` showing the pattern.
`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` 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) ``` [`site-monitor/controllers/hashrate.js`](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/site-monitor/controllers/hashrate.js) is the shipping example this pattern is copied from. There is no separate Gateway-side store for historical or aggregated data, either. Fan `pullWorkerTelemetry` 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 })) ) ``` The [default telemetry controllers](https://github.com/tetherto/mdk/tree/main/backend/core/plugins/telemetry/controllers) and [`telemetry/lib/site-data.js`](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/telemetry/lib/site-data.js) show a worked, production version of this fan-out (aliasing, error tolerance per Worker, and the aggregation shapes each route returns). ### Send a command `sendCommand` dispatches via the Kernel to the Worker that owns the device — the command must be declared in the Worker's `mdk-contract.json`. `controllers/command.js` above already shows the pattern; the client's own reference documents the full return shape (`commandId`, `status`, `result`, `error`). ### Caching Add a `"cache"` array of dot-path strings to a route to enable request-level caching, bypassed with `?overwriteCache=true`. [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` 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 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 new Error('ERR_UNAUTHORIZED') const { permissions } = validateToken(token) if (!permissions.includes('miner:w')) throw new Error('ERR_FORBIDDEN') // Your route logic } ``` A controller cannot choose its status code. It receives `(req)` and never the Fastify reply, so a returned value goes out as `200` and a thrown `ERR_`-prefixed error becomes `400 Bad Request` carrying that message. `ERR_UNAUTHORIZED` reaches the client as `400`, not `401`. A route that needs true status control belongs in [raw Fastify routes](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/README.md#raw-fastify-routes) instead. ### 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 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 - Understand [how Workers declare their data](/guides/workers/build-a-worker) via `mdk-contract.json` — what `mdkClient` reads and `sendCommand` 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. ## Prerequisites - Node.js >=24 (LTS) - npm >=11 - 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/backend/core/mdk') 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/backend/core/mdk') const kernel = await getKernel() console.log('Kernel listener key:', kernel.getPublicKey().toString('hex')) ``` Share that hex string with the Gateway host. #### 2.2 Start the Gateway with `kernelKey` ```js const { startGateway } = require('@tetherto/mdk/backend/core/mdk') 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 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 - 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 - 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/backend/core/mdk') const { startWhatsminerWorker } = require('@tetherto/mdk-worker-whatsminer') const kernel = await getKernel() const { runtime, stop } = await startWhatsminerWorker({ workerId: 'whatsminer-rack-1', model: 'm56s', storeDir: './data/whatsminer' }) 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 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/backend/core/mdk') 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/backend/core/mdk') onShutdown(async () => { await db.close() await logger.flush() }, { forceMs: 5000 }) ``` See [`onShutdown` API reference](https://github.com/tetherto/mdk/blob/main/backend/core/mdk/README.md#onshutdowncleanupfn-opts--handler). ## What just happened 1. **Automatic chain**: `getKernel()` 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` 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/backend/core/mdk') 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 — 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#miners). For example, you may: - [Run an Antminer Worker](/guides/miners/run-antminer-worker) - [Run a Whatsminer Worker](/guides/miners/run-whatsminer-worker) - [Run an Avalon Worker](/guides/miners/run-avalon-worker) ## Prerequisites Every guide assumes: - Node.js >=24 (LTS) - npm >=11 - 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/backend/core/mdk') const { startAntminerWorker } = require('@tetherto/mdk-worker-antminer') const kernel = await getKernel() const worker = await startAntminerWorker({ workerId: 'antminer-rack-1', model: 's21', storeDir: './store/antminer-rack-1', seedDevices: [{ info: { container: 'site-1', serialNum: 'AM-001' }, opts: { address: '192.168.1.20', port: 80, username: 'root', password: 'root' } }] }) await kernel.registerWorker(worker.runtime.getPublicKey()) ``` Make sure each miner's IP is reachable from the machine or container running the Worker before registering. Commands act on physical hardware — prioritize thermal safety. `seedDevices` only seeds a fresh, empty `storeDir` — once persisted, the device set survives restarts on its own. To add a device to an already-running fleet, send the `registerThing` command to the live Worker instead: ```js const { createMdkClient } = require('@tetherto/mdk/backend/core/client') const client = createMdkClient({ hrpc: { key: kernel.getPublicKey() } }) await client.connect() await client.sendWorkerCommand('antminer-rack-1', null, 'registerThing', { id: 'AM-002', info: { container: 'site-1', serialNum: 'AM-002' }, opts: { address: '192.168.1.21', port: 80, username: 'root', password: 'root' } }) ``` `registerThing` persists the device config immediately, but the running Worker does not pick it up until it is stopped and restarted (`await worker.stop()`, then call `startAntminerWorker` again with the same `storeDir` and no `seedDevices`) — there is no hot-add. Before running in a deployment, generate the Worker config (`common.json` for Worker identity, `base.thing.json` for device defaults and per-model alert thresholds): ```bash cd backend/workers/miners/antminer ./setup-config.sh ``` For the full `seedDevices`/`registerThing` option reference and the mock `createServer` options, see the Worker's [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/USAGE.md) and the shared [install pattern](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/install-pattern.md).
## Troubleshooting The development example on this page is `examples/backend/miners/antminer/index.js`. A working run prints the Kernel HRPC key and one line per registered device, then stays running until Ctrl+C. If it does not print those values, or if a mock port is already in use, follow [miner troubleshooting](/guides/miners/troubleshooting). ## Next steps - Decide how to run the Worker service — [Deployment topologies](/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` - No API username or password. The Avalon CGMiner API is unauthenticated ### Development
Run against a mock To support development, this repo ships a runnable example that boots a mock A1346, starts a Kernel and Gateway, and starts the Worker (`startAvalonWorker`) against it: ```bash node examples/backend/miners/avalon/index.js ``` It prints the Kernel HRPC key and the registered device ID, then stays running until Ctrl+C. For details on the boot options and mock, see [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/avalon/USAGE.md).
### Connect a miner #### 2.1 Confirm the model Avalon ships one model family today, `a1346` — confirm this against the [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/avalon/USAGE.md) as new models are added. #### 2.2 Register your miner Avalon devices use the native CGMiner TCP API on port 4028, which is unauthenticated (no username or password). Add this code to the Node.js service or script that runs the MDK Worker in your deployment. The snippet shows the minimum boot call seeding one Avalon device; replace the example IP address with your miner's value: ```js const { getKernel } = require('@tetherto/mdk/backend/core/mdk') const { startAvalonWorker } = require('@tetherto/mdk-worker-avalon') const kernel = await getKernel() const worker = await startAvalonWorker({ workerId: 'avalon-rack-1', model: 'a1346', storeDir: './store/avalon-rack-1', seedDevices: [{ info: { container: 'site-1', serialNum: 'AV-001' }, opts: { address: '192.168.1.30', port: 4028 } }] }) await kernel.registerWorker(worker.runtime.getPublicKey()) ``` Make sure each miner's IP is reachable from the machine or container running the Worker before registering. Commands act on physical hardware — prioritize thermal safety. `seedDevices` only seeds a fresh, empty `storeDir` — once persisted, the device set survives restarts on its own. To add a device to an already-running fleet, send the `registerThing` command to the live Worker instead: ```js const { createMdkClient } = require('@tetherto/mdk/backend/core/client') const client = createMdkClient({ hrpc: { key: kernel.getPublicKey() } }) await client.connect() await client.sendWorkerCommand('avalon-rack-1', null, 'registerThing', { id: 'AV-002', info: { container: 'site-1', serialNum: 'AV-002' }, opts: { address: '192.168.1.31', port: 4028 } }) ``` `registerThing` persists the device config immediately, but the running Worker does not pick it up until it is stopped and restarted (`await worker.stop()`, then call `startAvalonWorker` again with the same `storeDir` and no `seedDevices`) — there is no hot-add. Before running in a deployment, generate the Worker config (`common.json` for Worker identity, `base.thing.json` for device defaults and per-model alert thresholds): ```bash cd backend/workers/miners/avalon ./setup-config.sh ``` For the full `seedDevices`/`registerThing` option reference and the mock `createServer` options, see the Worker's [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/avalon/USAGE.md) and the shared [install pattern](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/install-pattern.md).
## Troubleshooting The development example on this page is `examples/backend/miners/avalon/index.js`. A working run prints `Kernel HRPC key:` and `Device:`, then stays running until Ctrl+C. If the example does not print both values, or if its mock port is already in use, follow [miner troubleshooting](/guides/miners/troubleshooting). ## Next steps - Decide how to run the Worker service — [Deployment topologies](/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 This page details how to run the MicroBT Whatsminer Worker. Select the development (mock) or real-device path. ## Prerequisites Review the [common deployment prerequisites](/guides/miners#prerequisites) before you start. Deployment-specific requirements: - A Node.js service or script in your deployment that runs the MDK Worker and registers devices - A supported Whatsminer device reachable from the machine or container running the Worker - The miner API reachable over encrypted TCP: port `4028` for API v2 (the default) or `4433` for API v3; the Worker auto-detects the version from the port, or probes both if given a different port - The Whatsminer API password. The Worker negotiates a session token from it; there is no separate username ### Development
Run against a mock To support development, this repo ships a runnable example that boots a mock M56S Whatsminer, starts a Kernel, and starts the Worker (`startWhatsminerWorker`) against it: ```bash node examples/backend/miners/whatsminer/index.js ``` It prints the Kernel HRPC key and the registered device ID, then stays running until Ctrl+C. To try another model, run that model's mock directly (`npm run mock ` from `backend/workers/miners/whatsminer`, or see [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/whatsminer/USAGE.md)) and adapt the `model` option in your own boot script.
### Connect a miner #### 2.1 Pick your model Use the Whatsminer Worker's [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/whatsminer/USAGE.md) to confirm the `model` value and mock `type` for your device. This guide uses `m56s`; replace it with the value for your miner. #### 2.2 Register your miner Whatsminer devices use an encrypted TCP API, port `4028` for API v2 (the default) or `4433` for API v3, with token-based authentication; the Worker negotiates a session token from the device password (there is no separate username) and auto-detects the API version from the port. Add this code to the Node.js service or script that runs the MDK Worker in your deployment. The snippet shows the minimum boot call seeding one Whatsminer device; replace the example IP address and password with your miner's values: ```js const { getKernel } = require('@tetherto/mdk/backend/core/mdk') const { startWhatsminerWorker } = require('@tetherto/mdk-worker-whatsminer') const kernel = await getKernel() const worker = await startWhatsminerWorker({ workerId: 'whatsminer-rack-1', model: 'm56s', storeDir: './store/whatsminer-rack-1', seedDevices: [{ info: { container: 'site-1', serialNum: 'WM-001' }, opts: { address: '192.168.1.10', port: 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/backend/core/client') const client = createMdkClient({ hrpc: { key: kernel.getPublicKey() } }) await client.connect() await client.sendWorkerCommand('whatsminer-rack-1', null, 'registerThing', { id: 'WM-002', info: { container: 'site-1', serialNum: 'WM-002' }, opts: { address: '192.168.1.11', port: 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 `startWhatsminerWorker` again with the same `storeDir` and no `seedDevices`) — there is no hot-add. Before running in a deployment, generate the Worker config (`common.json` for Worker identity, `base.thing.json` for device defaults and per-model alert thresholds): ```bash cd backend/workers/miners/whatsminer ./setup-config.sh ``` For the full `seedDevices`/`registerThing` option reference, the mock `createServer` options, and the per-model alert blocks, see the Worker's [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/whatsminer/USAGE.md) and the shared [install pattern](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/install-pattern.md).
## Troubleshooting The development example on this page uses `examples/backend/miners/whatsminer/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/whatsminer/plugin/mdk-contract.json) # Troubleshoot miner Workers (/guides/miners/troubleshooting) ## Overview This page covers the mock/development examples used by the Antminer, Whatsminer, and Avalon miner guides. The examples start a bundled mock miner, start 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) - Whatsminer: [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/whatsminer/USAGE.md#runnable-examples) - Avalon: [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/avalon/USAGE.md#runnable-example) 2. Open the matching `examples/run-*.js` file. 3. Look for the `createServer({ port: ... })` call. The cross-worker manifest also records the expected mock type and default port for each variant: [workers manifest](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/workers-manifest.yaml). ### Real miners Real devices use their native APIs: - Antminer: HTTP, usually port `80`, with digest-auth credentials. - Whatsminer: encrypted TCP, 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. ## 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 # UI guides (/guides/ui) } title="React" href="/guides/ui/react" description="Compose reporting layouts using MDK React foundation components" /> } title="Core (headless)" href="/guides/ui/use-ui-foundation-headlessly" description="Use MDK UI Foundation headlessly, without the React adapter" /> # 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 # UI CLI reference (/guides/ui/ui-cli) The **UI CLI** (`mdk-ui`, package `@tetherto/mdk-ui-cli`) is the command surface your AI agent uses to build with MDK. You usually never run it yourself, your agent does, after you [wire your IDE](/tutorials/ui/react/build-any-dashboard-with-an-agent). This page documents the commands for when you want to drive or inspect the tooling by hand. Every command runs locally and prints JSON by default. Add `--format table` for human-readable output. There are no network or model calls: each command is a lookup against files MDK ships. ## At a glance | Bucket | Section | What it covers | |--------|---------|----------------| | Set up | [Set up a project](#set-up-a-project) | Wire your IDE with `init` | | Discover | [Discover what to use](#discover-what-to-use) | `suggest`, `hooks`, `stores`, `find` | | Read | [Read a component contract](#read-a-component-contract) | `docs`, `example` | | Recipes | [Follow a recipe](#follow-a-recipe) | `blueprints`, `blueprint` | | Scaffold | [Scaffold and verify](#scaffold-and-verify) | `add page`, `check`, `sync` | | Inspect | [Inspect the UI CLI itself](#inspect-the-ui-cli-itself) | `--json-help` | ## All commands | Command | Summary | |---------|---------| | [`init`](#init) | Bootstrap `.mdk/context.md` and IDE rules | | [`suggest`](#suggest) | Ranked shortlist from free-text intent | | [`hooks`](#hooks) | List adapter hooks (optional `--category`) | | [`stores`](#stores) | List Zustand stores and query helpers | | [`find`](#find) | Filter components by domain and capability | | [`docs`](#docs) | Print a component's `USAGE.md` | | [`example`](#example) | Print a runnable `*.example.tsx` | | [`blueprints`](#blueprints) | List curated intent-to-component recipes | | [`blueprint`](#blueprint) | Show one recipe in detail | | [`add page`](#add-page) | Scaffold a page with chosen components | | [`check`](#check) | Type-check a file against real APIs | | [`sync`](#sync) | Refresh `.mdk/context.md` | | [`--json-help`](#json-help) | Machine-readable CLI surface | ## The agent's decision flow Given an intent, the deterministic path a session follows is: ```mermaid flowchart TD intent["Plain-language intent"] suggest["mdk-ui suggest"] state{"State or hooks needed?"} hooks["mdk-ui hooks / stores"] blueprints["mdk-ui blueprints"] match{"Matching blueprint?"} blueprint["mdk-ui blueprint"] find["mdk-ui find"] docs["mdk-ui docs / example"] add["mdk-ui add page"] check["mdk-ui check"] intent --> suggest suggest --> state state -->|yes| hooks state -->|no| blueprints hooks --> add blueprints --> match match -->|yes| blueprint match -->|no| find blueprint --> docs find --> docs docs --> add add --> check ``` ## Set up a project ### init Bootstraps the current project with an agent-context file and an IDE rule so every AI session is wired automatically: ```bash npx @tetherto/mdk-ui-cli init --ide cursor # .mdk/context.md + .cursor/rules/mdk.mdc npx @tetherto/mdk-ui-cli init --ide claude # .mdk/context.md + CLAUDE.md ``` ## Discover what to use ### suggest Turns free text into a ranked shortlist across components, hooks, blueprints, and stores: ```bash mdk-ui suggest "show hashrate for a pool" ``` ### hooks Lists every hook exported from `@tetherto/mdk-react-adapter`, grouped by category (`store`, `utility`, `permission`, `ui`, `external`): ```bash mdk-ui hooks --format table # all adapter hooks mdk-ui hooks --category store --format table # store-binding hooks only ``` ### stores Describes the Zustand stores and TanStack Query helpers from `@tetherto/mdk-ui-foundation`: ```bash mdk-ui stores --format table # stores and query helpers mdk-ui stores --category devices --format table ``` ### find Filters the component library by domain and capability: ```bash mdk-ui find --domain mining-operations --capability hashrate-monitoring ``` ## Read a component contract ### docs Prints a component's usage notes: ```bash mdk-ui docs LineChartCard ``` ### example Prints a runnable example: ```bash mdk-ui example LineChartCard ``` ## Follow a recipe Blueprints are curated recipes that map a high-level intent to a concrete set of components and hooks. ### blueprints Lists available recipes: ```bash mdk-ui blueprints ``` ### blueprint Shows one recipe in detail: ```bash mdk-ui blueprint device-management ``` ## Scaffold and verify ### add page Scaffolds a page with the components you name: ```bash mdk-ui add page Dashboard --component LineChartCard ``` ### check Confirms a file compiles against the real component APIs: ```bash mdk-ui check src/pages/Dashboard.tsx ``` ### sync Keeps the `.mdk/context.md` agent-context file current as MDK updates: ```bash mdk-ui sync ``` ## Inspect the UI CLI itself ### json-help `--json-help` prints the full command surface, useful for meta-tooling that wants to discover commands without running them: ```bash mdk-ui --json-help ``` ## Next steps - [Build dashboards with your AI agent](/tutorials/ui/react/build-any-dashboard-with-an-agent): the two-step flow most developers use - [UI Devkit](/reference/ui): the component library these commands draw from - [MDK repositories](/support/resources/repositories): source for the UI CLI and the agent-ready contract # Use UI Foundation headlessly (/guides/ui/use-ui-foundation-headlessly) @tetherto/mdk-ui-foundation [`@tetherto/mdk-ui-foundation`](/reference/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 will 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) - 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 ### Scaffold the package Create your own repo (or a directory inside your existing one) with a `package.json`. Pick your own npm scope (as an external Worker provider, you will publish under 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/backend/core && ./install-packages.sh) ``` 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. `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 will fail. #### 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) — 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 - 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 with MDK (/guides/workers/test-a-worker) This guide is for users of third-party worker packages or such partners who have integrated their own hardware, firmware, or data feed with MDK by shipping a [Worker plugin package](/guides/workers/build-a-worker). ## Overview Worker packages are the contract between the hardware and the Kernel, before relying on such a contract you will want to test its integration. To seed devices and register with Kernel, host the 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) - 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). It does **not** auto-install the nested package's own dependencies — this repo's install is a federated set of scripts, not a single root dependency graph — so run its installer once after adding it: ```bash (cd node_modules/@tetherto/mdk/backend/core && ./install-packages.sh) ``` The same deep-path pattern also gets you `getKernel`, `startGateway`, and `waitForDiscovery` from `require('@tetherto/mdk/backend/core/mdk')`, used in Step 3 below. ### Write the host module `host/index.js`, modeled on [`examples/backend/demo-worker-caller/index.js`](https://github.com/tetherto/mdk/blob/main/examples/backend/demo-worker-caller/index.js): ```js "use strict"; const 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({ hrpc: { key: kernel.getPublicKey() } }); await client.connect(); const telemetry = await withTimeout( client.pullTelemetry("vendor-0", "metrics"), 8000, "ERR_TELEMETRY_TIMEOUT", ); if (typeof telemetry.metrics?.hashrate_rt !== "number") { throw new Error("ERR_TELEMETRY_INVALID"); } console.log(`READY ${ready.workerId}: ${ready.deviceIds.join(", ")}`); console.log(`hashrate_rt=${telemetry.metrics.hashrate_rt}`); } finally { if (client) await client.close(); if (kernel) await shutdown(kernel); if (worker) await worker.stop(); if (mock) mock.exit(); } } main().catch((err) => { console.error(err); process.exitCode = 1; }); ``` Expected output: ```text READY vendor-demo: vendor-0 hashrate_rt=200 ``` The timeout wrapper bounds the client's wait but cannot cancel the current HRPC request. Always close the client during shutdown. Device-protocol cancellation is separately owned by the device client from Step 2. ### Use DHT discovery across processes or hosts For DHT discovery, generate and securely distribute one 32-byte hex topic, start the Worker first with `kernelTopic`, then start Kernel with the same `topic`. Do **not** also call `registerWorker()`: ```js "use strict"; const crypto = require("crypto"); const os = require("os"); const path = require("path"); const { getKernel, waitForDiscovery, shutdown, } = require("@tetherto/mdk/backend/core/mdk"); const { startVendorWorker } = require("./index"); const ROOT = path.join(os.tmpdir(), `vendor-worker-dht-${process.pid}`); async function main() { const topic = process.env.MDK_TOPIC || crypto.randomBytes(32).toString("hex"); let worker; let kernel; try { worker = await startVendorWorker({ workerId: "vendor-demo", kernelTopic: topic, seedDevices: [ { id: "vendor-0", opts: { host: "10.0.0.20", port: 9001 } }, ], }); kernel = await getKernel({ root: ROOT, topic }); const workers = await waitForDiscovery(kernel, { minWorkers: 1, timeoutMs: 45000, }); const ready = workers.find( (w) => w.workerId === "vendor-demo" && w.state === "READY", ); if (!ready) throw new Error("ERR_WORKER_NOT_READY"); console.log(`READY ${ready.workerId}: ${ready.deviceIds.join(", ")}`); } finally { if (kernel) await shutdown(kernel); if (worker) await worker.stop(); } } main().catch((err) => { console.error(err); process.exitCode = 1; }); ``` For separate production processes, each process must install signal handlers and close every handle it owns. DHT topics enable rendezvous; they are not authentication secrets or command-authorization tokens. See the discovery model for DHT, Local, and Same-process trade-offs. ## 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 legacy services](https://github.com/tetherto/mdk/blob/main/docs/reference/maintainers/worker-runtime-legacy-services.md)) that persist new device configs to the store. Those writes only take effect once the host is stopped and restarted with the provisioned set — there is no hot-add. It is off by default; every shipped miner Worker in this monorepo sets it to `true` in its boot function. ### Startup and discovery | Symptom | Diagnostic and remediation | | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `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 or command omitted its target device ID | | `ERR_UNKNOWN_QUERY_TYPE: ` | Use `metrics` or the exact `name` of a telemetry entry; the entry's return `type` is not its channel name | | `ERR_UNKNOWN_COMMAND: ` | Use the exact declared command name and confirm its handler loaded | | `ERR_UNKNOWN_ACTION: ` | Use a public MDK client helper instead of constructing protocol actions manually | | Command returns `status: 'FAILED'` | Read the stable `ERR_*` value, check validation/cooldown/device logs, and do not retry a timed-out physical write until its actual device state is known | 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 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 # Glossary (/reference/glossary) This page provides explanations for terms that new users may not be familiar with. - [Stack](#stack-and-hardware-terms) - [HRPC](#hyperswarm-rpc) ## Stack and hardware terms This section explains the terms you need to familiarize yourself with, using an Antminer rack as an example. | Term | What it is | Lives at | | --- | --- | --- | | **Kernel** (Orchestration Kernel) | The pull-only kernel that owns the device registry, routes commands, and aggregates telemetry. | [`backend/core/kernel/index.js`](https://github.com/tetherto/mdk/blob/main/backend/core/kernel/index.js) | | **Gateway** | The developer-owned entry point between non-Node clients (UI, AI agents) and Kernel. Mandatory whenever a non-Node consumer reaches the kernel; not used in the in-process Antminer-rack example below. | [`backend/core/gateway/`](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/worker.js) | | **Worker** | A device-family translator. Speaks the MDK Protocol upward to Kernel and the vendor's native API downward to one device family (one miner brand, one container type, one pool API). | [`backend/workers/docs/install-pattern.md`](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/install-pattern.md) | | **Manager class** | The JavaScript class a Worker exports, one per supported device model. Instances drive a single rack of devices. | e.g. `AM_S19XP`, `AM_S21` in [`backend/workers/miners/antminer/index.js`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/index.js) | | **Thing** | One registered device instance. Created by calling `manager.registerThing({ info, opts })`. Identified by a generated `deviceId`. | runtime, in `manager.mem.things` | | **MCP** (Model Context Protocol) | The protocol AI agents use to discover and call tools. MDK's server is a standalone package — a separate process from the Gateway, not a Gateway plugin. | [`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({ hrpc: { key } }) ``` The Noise handshake that HRPC performs on every connection authenticates by key, so Kernel's allowlist works identically whether the caller is on the same machine or a remote host. This is consistent with the broader Holepunch ecosystem philosophy — everything is a peer addressed by public key. When the peer is on the same machine it routes locally over the local network interface; the application code sees no difference. ## Next steps - You are ready to run the example in [Run a mining site end to end](/tutorials/run-a-site) - Learn more about: - Multi-process discovery across machines: Worker discovery - Gateway implementation details, including HTTP routing and plugin registration: [`backend/core/gateway/worker.js`](https://github.com/tetherto/mdk/blob/main/backend/core/gateway/worker.js) - Building your own Worker for a new device family: see [`backend/workers/docs/install-pattern.md`](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/install-pattern.md) - Per-device contract details (telemetry units, command shapes, error codes): those live in each Worker's `mdk-contract.json`, e.g. [`backend/workers/miners/antminer/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): maps `deviceId` to `workerId` to RPC channel, 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 stateless proxy that routes telemetry queries to the Worker that owns the data - [`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: the architectural narrative behind this module split - Understand 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 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) ## Overview MDK integrates field hardware through Workers. Each Worker declares what it supports in its `mdk-contract.json`, and that contract is the single source of truth for coverage. Use this page to discover what Workers are supported. ## What MDK supports - **Miners**: For example, Bitmain Antminer, MicroBT Whatsminer - **Containers**: For example, Bitmain Antspace, Bitdeer - **Power meters**: For example, ABB, Satec - **Sensors**: For example, Seneca - **Mining pools**: Protocol integrations such as Ocean, F2Pool For the exact model lists, Worker packages, and per-Worker docs, see the generated catalogue: - [Full supported-hardware catalogue](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/supported-hardware.md) — generated from every `backend/workers/**/mdk-contract.json` ## Next steps - New to the moving parts? Read [terminology](/reference/glossary) (Kernel, Worker, manager, thing, mock) - Decide how to run the Worker service — [Deployment topologies](/guides/deployment) - Run a miner Worker — [Run a miner Worker](/guides/miners) # UI Reference (/reference/ui) Complete API reference for the MDK UI packages: `@tetherto/mdk-react-devkit`, `@tetherto/mdk-react-adapter`, and `@tetherto/mdk-ui-foundation`. ## Quick Links | Section | Description | Count | |---------|-------------|-------| | [Components](/reference/ui/components) | React components for building UIs | 286 components | | [Hooks](/reference/ui/hooks) | React hooks for state and data | 106 hooks | | [Query Helpers](/reference/ui/query-helpers) | TanStack Query helpers for data fetching | 43 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 | 149 utilities | ## Package Overview ### `@tetherto/mdk-react-devkit` The main UI component library. Provides: - Production-ready React components - Component-specific hooks - TypeScript types for all components ```tsx ``` ### `@tetherto/mdk-react-adapter` React bindings for the foundation layer. Provides: - Zustand store access hooks - Authentication hooks - Permission hooks - Data fetching hooks ```tsx ``` ### `@tetherto/mdk-ui-foundation` Framework-agnostic foundation layer. Provides: - Zustand stores - TanStack Query helpers - Utility functions - TypeScript types ```tsx ``` ## Getting Started 1. [Install the packages](/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` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string \| undefined` | | - | - | | `loading` | `boolean \| undefined` | | - | - | | `disabled` | `boolean \| undefined` | | - | - | | `className` | `string \| undefined` | | - | - | | `variant` | `TActionButtonVariant \| undefined` | | - | - | | `confirmation` | `ActionButtonConfirmation` | ✓ | - | - | | `mode` | `"dialog" \| "popover" \| undefined` | | - | Confirmation mode: popover (inline) or dialog (modal). Default: popover | ### `Button` Primary action button. Supports loading state with spinner, icon placement, variants, sizes, and full-width layout. Forwards refs and all native button attributes. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `loading` | `boolean` | | - | Show a spinner instead of the content and disable the button. | | `fullWidth` | `boolean` | | - | Make the button stretch to fill its container. | | `icon` | `React.ReactNode` | | - | Icon node rendered alongside `children`. | | `variant` | `ButtonVariant` | | - | Visual variant (e.g. `primary`, `secondary`, `ghost`). | | `contentClassName` | `string` | | - | Class names applied to the inner content wrapper. | | `iconPosition` | `ButtonIconPosition` | | - | Icon placement relative to children. | | `size` | `ComponentSize` | | - | Size token (`sm`, `md`, `lg`). | ### `StatsExport` Dropdown button that triggers asynchronous CSV or JSON export. Shows a spinner while the corresponding handler is awaited. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `showLabel` | `boolean \| undefined` | | - | - | | `disabled` | `boolean \| undefined` | | - | - | | `onCsvExport` | `() => Promise` | ✓ | - | - | | `onJsonExport` | `() => Promise` | ✓ | - | - | # Auth (/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` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `children` | `React.ReactNode` | ✓ | - | Rendered when a token is present. | | `fallback` | `React.ReactNode` | ✓ | - | Rendered when no token is present — typically ``. | | `rememberPath` | `boolean \| undefined` | | - | When true (default), the current location is persisted to sessionStorage before rendering the fallback so the sign-in flow can return there. | ### `SignInGoogleButton` One-click Google OAuth sign-in trigger. Defaults to a full-page redirect to `${oauthBaseUrl}/oauth/google`, mirroring the production MOS flow. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `icon` | `React.ReactNode` | ✓ | - | Icon node rendered alongside `children`. | | `loading` | `boolean \| undefined` | ✓ | - | Show a spinner instead of the content and disable the button. | | `variant` | `ButtonVariant \| undefined` | ✓ | - | Visual variant (e.g. `primary`, `secondary`, `ghost`). | | `size` | `ComponentSize \| undefined` | ✓ | - | Size token (`sm`, `md`, `lg`). | | `fullWidth` | `boolean \| undefined` | ✓ | - | Make the button stretch to fill its container. | | `contentClassName` | `string \| undefined` | ✓ | - | Class names applied to the inner content wrapper. | | `iconPosition` | `ButtonIconPosition \| undefined` | ✓ | - | Icon placement relative to children. | | `oauthBaseUrl` | `string` | ✓ | - | Base URL of the OAuth backend (no trailing slash). Click navigates to `${oauthBaseUrl}/oauth/google`. | | `label` | `string \| undefined` | | - | Override the visible button label. | | `onClick` | `(() => void) \| undefined` | | - | Override the click behaviour entirely. When set, `oauthBaseUrl` is ignored. | # Branding (/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` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `size` | `MdkWordmarkSize \| undefined` | | - | Visual size of the wordmark. `sm` ≈ 24px tall, `md` ≈ 32px, `lg` ≈ 64px. | | `className` | `string \| undefined` | | - | Optional class hook on the outer ``. | | `title` | `string \| undefined` | | - | Accessible label. Defaults to "MDK". | # Card (/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` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string` | | - | - | | `isLoading` | `boolean` | | - | - | | `className` | `string` | | - | - | | `skeletonRows` | `number` | | - | - | | `emptyMessage` | `string` | | - | - | | `items` | `TIncidentRowProps[]` | | - | - | | `onItemClick` | `(id: string) => void` | | - | - | ### `CabinetDetailCard` Read-only LV cabinet detail: powermeter readings, the root plus per-position temperature readings (severity-coloured, with an offline marker), and the active-warnings timeline. Presentational — shape the rows with [`useCabinetDetail`](/reference/ui/hooks/cards/#usecabinetdetail). `advanced` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `title` | `string` | ✓ | - | Cabinet display title (`LV Cabinet 1` / transformer title). | | `powerMeters` | `CabinetReadingRow[]` | ✓ | - | Non-root powermeter reading rows. | | `rootTempSensor` | `CabinetReadingRow \| undefined` | | - | The cabinet-root temperature reading, when present. | | `tempSensors` | `CabinetReadingRow[]` | ✓ | - | Non-root temperature sensor reading rows. | | `alarmsDataItems` | `TimelineItemData[]` | ✓ | - | Active-warnings timeline items. | | `onNavigate` | `((path: string) => void) \| undefined` | | - | Router navigate used by warning rows to deep-link into the alert. | | `isLoading` | `boolean \| undefined` | | - | Shows a spinner while the cabinet snapshot is loading. | ### `MetricCard` Compact card displaying a labelled metric value with optional highlight and transparency states. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string` | ✓ | - | - | | `unit` | `string` | ✓ | - | - | | `value` | `string \| number \| null` | ✓ | - | - | | `bgColor` | `string \| undefined` | ✓ | - | - | | `className` | `string \| undefined` | ✓ | - | - | | `noMinWidth` | `boolean \| undefined` | ✓ | - | - | | `isValueMedium` | `boolean \| undefined` | ✓ | - | - | | `isHighlighted` | `boolean \| undefined` | ✓ | - | - | | `showDashForZero` | `boolean \| undefined` | ✓ | - | - | | `isTransparentColor` | `boolean \| undefined` | ✓ | - | - | ### `MiningPoolsPanel` Dashboard card that lists configured mining pools — one row per pool, with revenue, hash rate, and an optional "Show details" action. Pure presentation: the row data + click handler come from props, shaped upstream by `usePoolRows` (or any caller producing `MiningPoolRow`s). `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string` | | - | Override the card title — defaults to `Mining Pools`. | | `hideHeader` | `boolean` | | - | Hide the title row entirely. | | `isLoading` | `boolean` | | - | Loading state — renders skeleton rows. | | `skeletonRows` | `number` | | - | Number of skeleton rows to show while loading. | | `emptyMessage` | `string` | | - | Message shown when `rows` is empty. | | `rows` | `MiningPoolRow[]` | | - | Pool rows, in display order. | | `onShowDetails` | `(row: MiningPoolRow) => void` | | - | Called when the user clicks the per-row "Show details" button. | | `className` | `string` | | - | Extra className for the root. | ### `PoolDetailsCard` Compact key/value card for displaying pool metadata (URL, fee, worker count, etc.). Empty list renders a "No data available" placeholder. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string \| undefined` | ✓ | - | - | | `underline` | `boolean \| undefined` | ✓ | - | - | | `className` | `string \| undefined` | ✓ | - | - | | `details` | `PoolDetailItem[]` | ✓ | - | - | ### `PoolDetailsPopover` [**`Button`**](/reference/ui/components/actions/#button)-triggered popover that displays a pool's key/value details (URL, fee, worker count, status, …) inside a Radix `Dialog`. Wraps [`PoolDetailsCard`](/reference/ui/components/cards/#pooldetailscard) so the read-out matches the embedded card variant. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `title` | `string \| undefined` | ✓ | - | - | | `description` | `string \| undefined` | ✓ | - | - | | `disabled` | `boolean \| undefined` | ✓ | - | - | | `className` | `string \| undefined` | ✓ | - | - | | `triggerLabel` | `string \| undefined` | ✓ | - | - | | `details` | `PoolDetailItem[]` | ✓ | - | - | # Chart (/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. #### Notes - `minMaxAvg` and `timeRange` are only rendered when the chart is not loading or empty. #### Example ```tsx /** * Runnable example for ChartContainer. */ const RANGE_OPTIONS = [ { label: '1H', value: '1h' }, { label: '24H', value: '24h' }, { label: '7D', value: '7d' }, ] const LEGEND_DATA = [ { label: 'Pool A', color: '#59E8E8' }, { label: 'Pool B', color: '#FF9500' }, ] const MockChart = () => (
Chart content
) const [range, setRange] = useState('24h') return (
) } ``` #### Related API - [**`useChartDataCheck`**](/reference/ui/hooks/charts/#usechartdatacheck) @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `ActualEbitdaCard` Stat card summarising the realised EBITDA for the selected reporting window vs the prior period. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `value` | `number` | ✓ | - | - | ### `AreaChart` Presentational Chart.js area chart (Line with fill). Data must be provided via props; this component does no fetching of its own. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `data` | `ChartData<"line", (number \| Point \| null)[], unknown>` | ✓ | - | Chart data - required, provided by parent | | `options` | `_DeepPartialObject & ElementChartOptions<"line"> & PluginChartOptions<"line"> & DatasetChartOptions<"line"> & ScaleChartOptions<"line"> & LineControllerChartOptions> \| undefined` | | - | Chart.js options - merged with defaults | | `tooltip` | `ChartTooltipConfig \| undefined` | | - | Custom HTML tooltip configuration. When provided, replaces the default Chart.js tooltip. | | `height` | `number \| undefined` | | - | Chart height in pixels | | `className` | `string \| undefined` | | - | - | ### `AverageDowntimeChart` Stacked bar chart of average downtime (curtailment vs operational issues). Wraps [`ChartContainer`](/reference/ui/components/charts/#chartcontainer) and [`BarChart`](/reference/ui/components/charts/#barchart); pass period labels and rate arrays via `data`. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `title` | `string` | | - | - | | `unit` | `string` | | - | - | | `height` | `number` | | - | - | | `barWidth` | `number` | | - | - | | `className` | `string` | | - | - | | `isLoading` | `boolean` | | - | - | | `emptyMessage` | `string` | | - | - | | `data` | `AverageDowntimeChartData` | | - | - | | `yTicksFormatter` | `(value: number) => string` | | - | Formats Y-axis ticks, tooltips, and bar data labels (values are 0–1 rates). | | `showDataLabels` | `boolean` | | - | - | ### `AvgAllInCostChart` Avg All-in [**`Cost`**](/reference/ui/components/dashboards/#cost) - revenue vs cost ($/MWh) bar chart over time. Renders the OSS `SiteEnergyVsCostChart`. The revenue/cost time-series isn't carried by the cost-summary response, so consumers feed it through as a separate prop (the OSS app sources it from `useAvgAllInPowerCostData`). `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `data` | `readonly AvgAllInCostDataPoint[] \| undefined` | | - | - | | `dateRange` | `FinancialDateRange \| null` | ✓ | - | - | | `isLoading` | `boolean \| undefined` | | - | - | ### `BarChart` Presentational Chart.js bar chart. Data must be pre-aggregated; use grouped or stacked categories via `datasets`. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `data` | `any` | ✓ | - | Chart data - required, provided by parent. Use `as any` for mixed bar+line datasets. | | `options` | `_DeepPartialObject & ElementChartOptions<"bar"> & PluginChartOptions<"bar"> & DatasetChartOptions<"bar"> & ScaleChartOptions<"bar"> & BarControllerChartOptions> \| undefined` | | - | Chart.js options - merged with defaults | | `isStacked` | `boolean \| undefined` | | - | Stack bars on top of each other | | `isHorizontal` | `boolean \| undefined` | | - | Render bars horizontally (indexAxis: 'y') | | `formatYLabel` | `((value: number) => string) \| undefined` | | - | Format Y-axis tick labels | | `showLegend` | `boolean \| undefined` | | - | Show built-in Chart.js legend (default: true) | | `legendPosition` | `Position \| undefined` | | - | Position of the legend (default: 'top') | | `legendAlign` | `FlexAlign \| undefined` | | - | Alignment of the legend labels within their position (default: 'start') | | `showDataLabels` | `boolean \| undefined` | | - | Show values above each bar | | `formatDataLabel` | `((value: number) => string) \| undefined` | | - | Format data label values (default: round to nearest integer) | | `tooltip` | `ChartTooltipConfig \| undefined` | | - | Custom HTML tooltip configuration. When provided, replaces the default Chart.js tooltip. | | `height` | `number \| undefined` | | - | Chart height in pixels | | `className` | `string \| undefined` | | - | - | ### `BitcoinPriceCard` Stat card showing the BTC reference price used by the reporting view with currency and timestamp. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `value` | `number` | ✓ | - | - | ### `BitcoinProducedCard` Stat card summarising the bitcoin produced during the reporting window with delta to prior period. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `value` | `number` | ✓ | - | - | ### `BitcoinProducedChart` Time-series chart of bitcoin produced per day across the selected reporting window. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `chartData` | `BarChartDataResult` | ✓ | - | - | | `isLoading` | `boolean \| undefined` | | - | - | | `hasAllZeros` | `boolean \| undefined` | | - | - | | `height` | `number \| undefined` | | - | - | ### `BitcoinProductionCostCard` Stat card showing the average cost in USD to produce one bitcoin during the reporting window. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `value` | `number` | ✓ | - | - | ### `ChartContainer` Standard chrome for charts: title, optional highlighted value, legend with toggle, range selector (radio cards), loading / empty states, and a footer for min/max/avg stats. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `title` | `string \| undefined` | | - | - | | `titleExtra` | `React.ReactNode` | | - | Optional node rendered immediately after the title text (e.g. an info tooltip). Only shown when `title` is set and `header` is not. Additive - omit it and the title renders exactly as before. | | `header` | `React.ReactNode` | | - | - | | `headerAction` | `React.ReactNode` | | - | Optional action rendered on the right side of the header row (e.g. an expand/fullscreen toggle). Sits alongside the range selector when both are present. Purely additive - omit it and the header renders exactly as before. | | `legendData` | `LegendItem[] \| undefined` | | - | - | | `highlightedValue` | `HighlightedValueProps \| undefined` | | - | - | | `rangeSelector` | `RangeSelectorProps \| undefined` | | - | - | | `loading` | `boolean \| undefined` | | - | - | | `empty` | `boolean \| undefined` | | - | - | | `emptyMessage` | `string \| undefined` | | - | - | | `minMaxAvg` | `Partial<{ min: string; max: string; avg: string; }> \| undefined` | | - | - | | `timeRange` | `string \| undefined` | | - | - | | `footer` | `React.ReactNode` | | - | - | | `footerClassName` | `string \| undefined` | | - | - | | `className` | `string \| undefined` | | - | - | | `children` | `React.ReactNode` | ✓ | - | - | | `onToggleDataset` | `((index: number) => void) \| undefined` | | - | - | ### `ChartExpandAction` Expand / collapse toggle rendered in a dashboard chart card's header. Swaps between a maximize and a minimize glyph based on `isExpanded`. `advanced` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `isExpanded` | `boolean` | ✓ | - | Whether the parent chart is currently expanded to full width. | | `onToggle` | `VoidFunction \| undefined` | | - | Toggles the expanded state. | ### `ChartStatsFooter` [**`ChartStatsFooter`**](/reference/ui/components/charts/#chartstatsfooter) - Displays Min/Max/Avg values and optional stats grid below a chart `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `minMaxAvg` | `Partial<{ min: string; max: string; avg: string; }>` | | - | Min/Max/Avg values row | | `stats` | `ChartStatsFooterItem[]` | | - | Additional stats displayed in a columnar grid | | `statsPerColumn` | `number` | | - | Number of stat items per column (default: 1) | | `secondaryLabel` | `SecondaryLabel` | | - | Secondary label displayed below stats | | `className` | `string` | | - | Custom class name | ### `CostCharts` Convenience wrapper that renders the three cost-page charts in declaration order. Pages that need bespoke layouts (e.g. [`CostContent`](/reference/ui/components/dashboards/#costcontent)'s 2x2 [**`Mosaic`**](/reference/ui/components/layout/#mosaic)) compose the individual chart components directly instead. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `costLog` | `readonly CostTimeSeriesEntry[]` | ✓ | - | - | | `btcPriceLog` | `readonly BtcPriceTimeSeriesEntry[]` | ✓ | - | - | | `totals` | `CostSummaryMonetaryTotals \| null` | ✓ | - | - | | `dateRange` | `FinancialDateRange \| null` | ✓ | - | - | | `avgAllInCostData` | `readonly AvgAllInCostDataPoint[] \| undefined` | | - | - | | `isLoading` | `boolean \| undefined` | | - | - | ### `DetailLegend` [**`DetailLegend`**](/reference/ui/components/charts/#detaillegend) - Enhanced chart legend with current values and percentage change indicators `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `items` | `DetailLegendItem[]` | ✓ | - | Legend items to display | | `onToggle` | `((label: string, index: number) => void) \| undefined` | | - | Callback when a legend item is toggled | | `className` | `string \| undefined` | | - | Custom class name | ### `DoughnutChart` [**`DoughnutChart`**](/reference/ui/components/charts/#doughnutchart) – Presentational Chart.js doughnut chart with custom HTML legend matching the MDK design. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `data` | `DoughnutChartDataset[]` | ✓ | - | Array of labelled slices | | `unit` | `string \| undefined` | | - | Unit suffix shown in tooltips | | `options` | `_DeepPartialObject & ElementChartOptions<"doughnut"> & PluginChartOptions<"doughnut"> & DatasetChartOptions<"doughnut"> & ScaleChartOptions<"doughnut"> & DoughnutControllerChartOptions> \| undefined` | | - | Chart.js options – merged with defaults | | `cutout` | `string \| undefined` | | - | Doughnut cutout percentage (default: '75%') | | `borderWidth` | `number \| undefined` | | - | Border width between segments (default: 4) | | `height` | `number \| undefined` | | - | Chart height in pixels | | `legendPosition` | `Position \| undefined` | | - | Where to place the legend relative to the chart (default: 'top') | | `tooltip` | `ChartTooltipConfig \| undefined` | | - | Custom HTML tooltip configuration. When provided, replaces the default doughnut tooltip (which shows label, value with unit, and percentage). Use `valueFormatter` to replicate the percentage display if needed. | | `formatValue` | `((value: number) => string) \| undefined` | | - | Formats slice values in the built-in legend and default tooltip (default: raw number). | | `className` | `string \| undefined` | | - | - | ### `Ebitda` Top-level EBITDA section of the reporting view — pulls together metric cards, charts, and tables. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `metrics` | `EbitdaDisplayMetrics \| null` | ✓ | - | - | | `ebitdaChartInput` | `ToBarChartDataInput \| null` | ✓ | - | - | | `btcProducedChartInput` | `ToBarChartDataInput \| null` | ✓ | - | - | | `hasBtcProducedAllZeros` | `boolean` | ✓ | - | - | | `showEbitdaBarChart` | `boolean` | ✓ | - | - | | `currentBTCPrice` | `number` | ✓ | - | - | | `datePicker` | `React.ReactElement>` | ✓ | - | - | | `isLoading` | `boolean \| undefined` | | - | - | | `errors` | `string[] \| undefined` | | - | - | | `hasDateSelection` | `boolean` | ✓ | - | When false, show the "select a period" hint instead of empty data. | | `setCostHref` | `string \| undefined` | | - | Optional URL for the "Set Monthly [**`Cost`**](/reference/ui/components/dashboards/#cost)" control (hidden when omitted). | ### `EbitdaCharts` Chart panel inside the EBITDA section visualising revenue, cost, and EBITDA over time. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `showEbitdaBarChart` | `boolean` | ✓ | - | - | | `ebitdaChartData` | `BarChartDataResult` | ✓ | - | - | | `btcDisplayData` | `BarChartDataResult` | ✓ | - | - | | `isLoading` | `boolean` | ✓ | - | - | | `hasBtcProducedAllZeros` | `boolean` | ✓ | - | - | ### `EbitdaHodlCard` Stat card projecting EBITDA assuming all produced bitcoin is held instead of sold. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `value` | `number` | ✓ | - | - | | `currentBTCPrice` | `number` | ✓ | - | - | ### `EbitdaMetrics` Row of summary metric cards across the top of the EBITDA section (actual, hodl, selling, cost). `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `metrics` | `EbitdaDisplayMetrics` | ✓ | - | - | | `currentBTCPrice` | `number` | ✓ | - | - | ### `EbitdaSellingCard` Stat card projecting EBITDA assuming all produced bitcoin is sold at the daily reference price. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `value` | `number` | ✓ | - | - | ### `EnergyBalance` Full energy balance view with tabbed revenue and cost sections, charts, and metric cards. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `viewModel` | `EnergyBalanceViewModel` | ✓ | - | - | | `onTabChange` | `(tab: EnergyBalanceTab) => void` | ✓ | - | - | | `onRevenueDisplayModeChange` | `(mode: DisplayMode) => void` | ✓ | - | - | | `onCostDisplayModeChange` | `(mode: DisplayMode) => void` | ✓ | - | - | | `isDemoMode` | `boolean \| undefined` | | - | - | | `timeframeControls` | `React.ReactNode` | | - | Slot for timeframe / date-range controls rendered by the host app. | | `setCostHref` | `string \| undefined` | | - | Optional URL for the "Set Monthly [**`Cost`**](/reference/ui/components/dashboards/#cost)" control (hidden when omitted). | ### `EnergyBalanceCostCharts` Layout container for the energy cost tab charts: revenue-vs-cost bar chart and power line chart. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `costChartData` | `BarChartDataResult` | ✓ | - | - | | `btcUnit` | `string \| null` | ✓ | - | - | | `powerChartInput` | `ThresholdLineChartInput` | ✓ | - | - | | `displayMode` | `DisplayMode` | ✓ | - | - | | `barLabelFormatter` | `(v: number) => string` | ✓ | - | - | | `onDisplayModeChange` | `(mode: DisplayMode) => void` | ✓ | - | - | | `showCostBarChart` | `boolean` | ✓ | - | Show the revenue-vs-cost bar chart only for non-daily periods. | | `periodType` | `PeriodType` | ✓ | - | - | ### `EnergyBalanceCostMetrics` Grid of stat cards summarising energy cost metrics for the selected period. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `metrics` | `EnergyCostMetrics` | ✓ | - | - | ### `EnergyBalancePowerChart` Line chart visualising power consumption against threshold for the energy balance view. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `height` | `number \| undefined` | | - | - | | `fillHeight` | `boolean \| undefined` | | - | - | | `periodType` | `PeriodType` | ✓ | - | - | | `chartInput` | `ThresholdLineChartInput` | ✓ | - | - | ### `EnergyBalanceRevenueCharts` [**`Mosaic`**](/reference/ui/components/layout/#mosaic) layout of revenue, downtime, and power charts for the energy balance revenue tab. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `revenueChartData` | `BarChartDataResult` | ✓ | - | - | | `averageDowntimeData` | `AverageDowntimeChartData` | ✓ | - | - | | `powerChartInput` | `ThresholdLineChartInput` | ✓ | - | - | | `displayMode` | `DisplayMode` | ✓ | - | - | | `barLabelFormatter` | `(v: number) => string` | ✓ | - | - | | `onDisplayModeChange` | `(mode: DisplayMode) => void` | ✓ | - | - | | `periodType` | `PeriodType` | ✓ | - | - | | `revenueMetrics` | `EnergyRevenueMetrics` | ✓ | - | - | ### `EnergyBalanceRevenueMetrics` Grid of stat cards summarising energy revenue metrics for the selected period. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `metrics` | `EnergyRevenueMetrics` | ✓ | - | - | ### `EnergyCostChart` Bar chart comparing site revenue vs cost per MWh, with USD/BTC currency toggle. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `chartData` | `BarChartDataResult` | ✓ | - | - | | `btcUnit` | `string \| null` | ✓ | - | - | | `displayMode` | `DisplayMode` | ✓ | - | - | | `barLabelFormatter` | `(v: number) => string` | ✓ | - | - | | `onDisplayModeChange` | `(mode: DisplayMode) => void` | ✓ | - | - | | `height` | `number \| undefined` | | - | - | ### `EnergyMetricCard` Stat card for a single energy balance metric. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `name` | `string` | ✓ | - | - | | `value` | `number` | ✓ | - | - | | `unit` | `string` | ✓ | - | - | | `fallback` | `string \| undefined` | | - | - | ### `EnergyReportMinerTypeView` Energy report — power consumption grouped by miner model (latest day in range). `advanced` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `isLoading` | `boolean \| undefined` | | - | - | | `containers` | `Container[] \| undefined` | | - | - | | `groupedConsumption` | `MetricsConsumptionGroupedResponse \| undefined` | | - | - | | `onTimeFrameChange` | `((start: Date, end: Date) => void) \| undefined` | | - | - | ### `EnergyReportMinerUnitView` Energy report — power consumption grouped by mining unit / container. `advanced` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `isLoading` | `boolean \| undefined` | | - | - | | `containers` | `Container[] \| undefined` | | - | - | | `groupedConsumption` | `MetricsConsumptionGroupedResponse \| undefined` | | - | - | | `onTimeFrameChange` | `((start: Date, end: Date) => void) \| undefined` | | - | - | ### `EnergyReportSiteView` Energy report site tab — power trend, power-mode table, and per–mining-unit activity cards. `advanced` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `consumptionError` | `unknown` | | - | - | | `tailLogLoading` | `boolean \| undefined` | | - | - | | `containersLoading` | `boolean \| undefined` | | - | - | | `consumptionLoading` | `boolean \| undefined` | | - | - | | `consumptionFetching` | `boolean \| undefined` | | - | - | | `nominalConfigLoading` | `boolean \| undefined` | | - | - | | `containers` | `EnergyReportContainer[] \| undefined` | | - | - | | `tailLog` | `EnergyReportTailLogItem[][] \| undefined` | | - | - | | `nominalPowerAvailabilityMw` | `number \| null \| undefined` | | - | - | | `consumptionLog` | `MetricsConsumptionLogEntry[] \| undefined` | | - | - | | `snapshotLoading` | `boolean \| undefined` | | - | - | | `onRefetchSnapshot` | `VoidFunction \| undefined` | | - | - | | `dateRange` | `EnergyReportDateRange` | ✓ | - | - | | `onDateRangeChange` | `((range: EnergyReportDateRange) => void) \| undefined` | | - | - | ### `EnergyRevenueChart` Bar chart showing site energy revenue per MWh, with USD/BTC currency toggle. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `chartData` | `BarChartDataResult` | ✓ | - | - | | `displayMode` | `DisplayMode` | ✓ | - | - | | `barLabelFormatter` | `(v: number) => string` | ✓ | - | - | | `onDisplayModeChange` | `(mode: DisplayMode) => void` | ✓ | - | - | | `height` | `number \| undefined` | | - | - | ### `GaugeChart` [**`GaugeChart`**](/reference/ui/components/charts/#gaugechart) - Presentational gauge / speedometer chart. Implementation note: this component used to wrap the `react-gauge-chart` NPM package, but that package is published only as CommonJS with a broken `module` field that points at the same CJS file. That made it crash under ESM bundlers that don't add a `__esModule ? .default : module` interop shim (Webpack 4, raw esbuild, certain SSR setups, etc.) with React's "Element type is invalid" error. We replaced it with a pure-SVG internal implementation (see `./gauge-svg.tsx`) so the component is bundler- and runtime-agnostic and has zero third-party runtime dependencies. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `percent` | `number` | ✓ | - | Value between 0 and 1 (e.g. 0.75 = 75%). Values outside the range are clamped. | | `colors` | `string[] \| undefined` | | - | Arc colours in HEX format. | | `arcWidth` | `number \| undefined` | | - | Arc thickness as a fraction of the gauge radius (0–1). | | `nrOfLevels` | `number \| undefined` | | - | Number of arc segments. Ignored when `arcsLength` is provided. | | `arcsLength` | `number[] \| undefined` | | - | Custom arc-segment proportions (auto-normalised), overriding `nrOfLevels`. e.g. `[0.7, 0.3]` for a progress-style gauge whose first arc is the fill. | | `needleColor` | `string \| undefined` | | - | Needle + hub colour. | | `hideNeedle` | `boolean \| undefined` | | - | Hide the needle + hub (e.g. for a progress-style gauge). | | `formatTextValue` | `((percent: number) => string) \| undefined` | | - | Format the centre label from the clamped fraction (0–1). | | `hideText` | `boolean \| undefined` | | - | Hide the percentage text rendered inside the gauge. | | `id` | `string \| undefined` | | - | Stable id used for the gauge's accessibility labels. | | `height` | `string \| number \| undefined` | | - | Chart height in pixels or any CSS length (e.g. `'200px'` or `'50%'`). | | `maxWidth` | `number \| undefined` | | - | Maximum width in pixels. | | `className` | `string \| undefined` | | - | - | ### `Hashrate` Top-level hashrate reporting section - composes the site / miner-type / mining-unit drilldowns into a tabbed shell. Each tab fetches independently because the three views use different `groupBy` axes; the composite stitches them via per-tab prop bags. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `defaultTab` | `HashrateTabValue \| undefined` | | - | Tab selected on first render. Defaults to the Site View. | | `siteView` | `HashrateSiteViewProps \| undefined` | | - | Props forwarded to the Site View tab. | | `minerTypeView` | `HashrateMinerTypeViewProps \| undefined` | | - | Props forwarded to the Miner Type View tab. | | `miningUnitView` | `HashrateMiningUnitViewProps \| undefined` | | - | Props forwarded to the Mining Unit View tab. | ### `HashrateMinerTypeView` [**`Hashrate`**](/reference/ui/components/charts/#hashrate) drilldown grouped by miner model - bar chart of the latest hashrate per miner type, with an optional multi-select filter. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `log` | `HashrateGroupedLog \| undefined` | | - | [**`Hashrate`**](/reference/ui/components/charts/#hashrate) log grouped by miner type (groupBy=miner). | | `isLoading` | `boolean \| undefined` | | - | - | | `dateRange` | `HashrateDateRange \| undefined` | | - | - | | `onDateRangeChange` | `((range: HashrateDateRange) => void) \| undefined` | | - | - | | `onReset` | `VoidFunction \| undefined` | | - | - | ### `HashrateMiningUnitView` [**`Hashrate`**](/reference/ui/components/charts/#hashrate) drilldown grouped by mining unit / container - bar chart of the latest hashrate per container with an optional multi-select filter. Drops BE-leaked rollup keys (`group-N`, `maintenance`) via the utils layer. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `log` | `HashrateGroupedLog \| undefined` | | - | [**`Hashrate`**](/reference/ui/components/charts/#hashrate) log grouped by container / mining unit (groupBy=container). | | `isLoading` | `boolean \| undefined` | | - | - | | `dateRange` | `HashrateDateRange \| undefined` | | - | - | | `onDateRangeChange` | `((range: HashrateDateRange) => void) \| undefined` | | - | - | | `onReset` | `VoidFunction \| undefined` | | - | - | ### `HashrateSiteView` Site-level hashrate trend - aggregates hashrate across the whole site for the selected date range, with an optional miner-type filter that scopes the sum to a subset. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `log` | `HashrateGroupedLog \| undefined` | | - | [**`Hashrate`**](/reference/ui/components/charts/#hashrate) log grouped by miner type. | | `isLoading` | `boolean \| undefined` | | - | Loading state - drives the chart spinner. | | `dateRange` | `HashrateDateRange \| undefined` | | - | Selected date range used by the host to drive the query. | | `onDateRangeChange` | `((range: HashrateDateRange) => void) \| undefined` | | - | Fires when the user picks a new range from the [**`DateRangePicker`**](/reference/ui/components/forms/#daterangepicker). | | `onReset` | `VoidFunction \| undefined` | | - | Optional reset handler shown as a "Reset" button next to the date picker. | ### `Heatmap` [**`Heatmap`**](/reference/ui/components/charts/#heatmap) — a generic grid of value-coloured cells on a low→high gradient. Presentational and domain-agnostic: pass a row-major matrix of cells and an optional `[min, max]` range (auto-derived otherwise). Use `renderCell` to overlay domain content (socket borders, selection, tooltips) without forking the primitive — the grid still owns each cell's background colour. Pair with [`HeatmapLegend`](/reference/ui/components/charts/#heatmaplegend) for a gradient scale. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `data` | `HeatmapCell[][]` | ✓ | - | Rows of cells (row-major). Rows may be ragged. | | `min` | `number \| undefined` | | - | Range floor; auto-derived from the finite values when omitted. | | `max` | `number \| undefined` | | - | Range ceiling; auto-derived from the finite values when omitted. | | `colors` | `readonly string[] \| undefined` | | - | Gradient stops low→high. Defaults to the cold→hot `HEATMAP_GRADIENT`. | | `emptyColor` | `string \| undefined` | | - | Colour used for `null` cells. | | `showValues` | `boolean \| undefined` | | - | Render each cell's value/label as text. | | `renderCell` | `((cell: HeatmapCell, context: HeatmapCellContext) => React.ReactNode) \| undefined` | | - | Override the cell's inner content — e.g. to overlay socket borders, selection, or tooltips for a PDU grid. The primitive still owns the cell's background colour (passed via `context.color`). | | `ariaLabel` | `string \| undefined` | | - | Accessible label for the grid. | | `className` | `string \| undefined` | | - | - | ### `HeatmapLegend` [**`HeatmapLegend`**](/reference/ui/components/charts/#heatmaplegend) — a gradient bar with low/high scale labels, matching the gradient used by [`Heatmap`](/reference/ui/components/charts/#heatmap). `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `min` | `string \| number` | ✓ | - | Value (or pre-formatted label) at the low end of the scale. | | `max` | `string \| number` | ✓ | - | Value (or pre-formatted label) at the high end of the scale. | | `unit` | `string \| undefined` | | - | Unit suffix appended to `min`/`max`. | | `label` | `string \| undefined` | | - | Heading above the gradient bar (e.g. "Temperature"). | | `colors` | `readonly string[] \| undefined` | | - | Gradient stops low→high. Defaults to `HEATMAP_GRADIENT`. | | `className` | `string \| undefined` | | - | - | ### `LineChart` Customisable Chart.js line chart with built-in zoom, tooltip, and legend. Data is passed via props; the component does no fetching. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `chartRef` | `React.MutableRefObject \| undefined` | | - | Mutable ref to hold the LightWeightCharts reference | | `data` | `LineChartData` | ✓ | - | Data of the chart | | `yTicksFormatter` | `((value: number) => string) \| undefined` | | - | Callback to format ticks on y axis. If `priceFormatter` is given. It would be used instead. | | `customLabel` | `string \| undefined` | | - | TODO: Doc | | `priceFormatter` | `((value: number) => string) \| undefined` | | - | Callback to format ticks on y axis. | | `roundPrecision` | `number \| undefined` | | - | The number of decimals to show | | `timeline` | `string \| undefined` | | - | TODO: DOC | | `fixedTimezone` | `string \| undefined` | | - | Applies offset if provided, otherwise timestamps are assumed to already be in local time. Otherwise, use browser's current timezone offset for consistent time display | | `shouldResetZoom` | `boolean \| undefined` | | - | Wether to Reset Zoom | | `skipRound` | `boolean \| undefined` | | - | Prevent rounding of values | | `skipMinWidth` | `boolean \| undefined` | | - | Do not enforce a min width | | `fadedBackground` | `boolean \| undefined` | | - | Use a faded background | | `backgroundColor` | `string \| undefined` | | - | Background color of the chart | | `customDateFormat` | `string \| undefined` | | - | Custom date format | | `verticalLineLabelVisible` | `boolean \| undefined` | | - | Show vertical line at mouse position | | `horizontalLineLabelVisible` | `boolean \| undefined` | | - | Show horizontal line at mouse position | | `showDateInTooltip` | `boolean \| undefined` | | - | Show date line in tooltip | | `disableAutoRange` | `boolean \| undefined` | | - | Disable automatically determining range | | `uniformDistribution` | `boolean \| undefined` | | - | Changes horizontal scale marks generation. With this flag equal to true, marks of the same weight are either all drawn or none are drawn at all. | | `unit` | `string \| undefined` | | - | The unit to display with values | | `beginAtZero` | `boolean \| undefined` | | - | Starts the value axis at 0 | | `showPointMarkers` | `boolean \| undefined` | | - | Show a marker on the line | | `height` | `number \| undefined` | | - | Controls the height of the chart. Default: 240 | ### `LineChartCard` Composable line-chart card with title, timeline range selector, legend (basic or detailed), error boundary, and an optional min/max/avg footer. Accepts either pre-shaped `data` or `rawData` + a `dataAdapter` callback so upstream domain components can keep their data wrangling local. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `data` | `LineChartCardData` | | - | Pre-adapted chart data (use this OR rawData+dataAdapter) | | `rawData` | `unknown` | | - | Raw data to be transformed by dataAdapter | | `dataAdapter` | `(data: unknown) => LineChartCardData` | | - | Adapter to transform rawData into LineChartCardData | | `timelineOptions` | `TimelineOption[]` | | - | Timeline range selector options | | `timeline` | `string` | | - | Controlled timeline value | | `defaultTimeline` | `string` | | - | Default timeline when uncontrolled | | `onTimelineChange` | `(timeline: string) => void` | | - | Callback when timeline changes | | `title` | `string` | | - | Chart title | | `detailLegends` | `boolean` | | - | Show detail legends with current values | | `isLoading` | `boolean` | | - | Loading state | | `shouldResetZoom` | `boolean` | | - | Whether to reset zoom on timeline change (default: true) | | `chartProps` | `Partial` | | - | Pass-through props to the core [**`LineChart`**](/reference/ui/components/charts/#linechart) | | `chartRef` | `React.MutableRefObject` | | - | Ref to the lightweight-charts IChartApi | | `minHeight` | `string \| number` | | - | - | | `className` | `string` | | - | Custom class name | | `headerAction` | `React.ReactNode` | | - | Optional action rendered on the right of the card header (e.g. an expand toggle). Passed straight through to [`ChartContainer`](/reference/ui/components/charts/#chartcontainer). Additive - omit it and the card header is unchanged. | | `titleExtra` | `React.ReactNode` | | - | Optional node rendered next to the title (e.g. an info tooltip). Passed straight through to [`ChartContainer`](/reference/ui/components/charts/#chartcontainer). Additive. | ### `MinMaxAvg` Min / Max / Avg summary row with consistent MDK label and value styling. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `min` | `string \| undefined` | ✓ | - | - | | `max` | `string \| undefined` | ✓ | - | - | | `avg` | `string \| undefined` | ✓ | - | - | | `className` | `string \| undefined` | | - | - | ### `MonthlyEbitdaChart` Bar chart comparing EBITDA across the most recent months for trend visualisation. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `chartData` | `BarChartDataResult` | ✓ | - | - | | `height` | `number \| undefined` | | - | - | ### `OperationalHashrateChart` [**`Hashrate`**](/reference/ui/components/charts/#hashrate) trend card for the operational dashboard. Renders the site hashrate over time (TH/s) with an optional nominal reference line, plus an expand toggle. Purely presentational - pass pre-shaped data from [`useOperationsDashboard`](/reference/ui/hooks/misc/#useoperationsdashboard). `advanced` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `data` | `LineChartCardData` | | - | - | | `isLoading` | `boolean` | | - | - | | `isExpanded` | `boolean` | | - | - | | `onToggleExpand` | `VoidFunction` | | - | - | ### `OperationalMinersStatusChart` Miners-status card for the operational dashboard. Renders a stacked daily breakdown of miner states (online / error / offline / sleep / maintenance) with an expand toggle. Purely presentational - pass pre-shaped data from [`useOperationsDashboard`](/reference/ui/hooks/misc/#useoperationsdashboard). `advanced` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `data` | `MinersStatusChartData` | | - | - | | `isLoading` | `boolean` | | - | - | | `isExpanded` | `boolean` | | - | - | | `onToggleExpand` | `VoidFunction` | | - | - | ### `OperationalPowerConsumptionChart` Power-consumption trend card for the operational dashboard. Renders site power draw over time (MW) with an optional power-availability reference line, plus an expand toggle. Purely presentational - pass pre-shaped data from [`useOperationsDashboard`](/reference/ui/hooks/misc/#useoperationsdashboard). `advanced` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `data` | `LineChartCardData` | | - | - | | `isLoading` | `boolean` | | - | - | | `isExpanded` | `boolean` | | - | - | | `onToggleExpand` | `VoidFunction` | | - | - | ### `OperationalSiteEfficiencyChart` Site-efficiency trend card for the operational dashboard. Renders measured site efficiency over time (W/TH/s) with an optional nominal reference line, an info tooltip, and an expand toggle. Purely presentational - pass pre-shaped data from [`useOperationsDashboard`](/reference/ui/hooks/misc/#useoperationsdashboard). `advanced` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `data` | `LineChartCardData` | | - | - | | `isLoading` | `boolean` | | - | - | | `isExpanded` | `boolean` | | - | - | | `onToggleExpand` | `VoidFunction` | | - | - | ### `OperationsEnergyChart` Doughnut breakdown of Operations vs Energy cost (in USD totals). Mirrors the OSS [`OperationsEnergyCostChart`](/reference/ui/components/charts/#operationsenergycostchart). Returns the empty-state placeholder when both totals are zero (the OSS chart hides both slices in that case). `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `totals` | `CostSummaryMonetaryTotals \| null` | ✓ | - | - | | `isLoading` | `boolean \| undefined` | | - | - | ### `OperationsEnergyCostChart` Doughnut breakdown of Operations vs Energy cost (USD per MWh). Wraps [`ChartContainer`](/reference/ui/components/charts/#chartcontainer) and [`DoughnutChart`](/reference/ui/components/charts/#doughnutchart); pass `operationalCostsUSD` and `energyCostsUSD` via `data`. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `title` | `string` | | - | - | | `unit` | `string` | | - | - | | `height` | `number` | | - | - | | `className` | `string` | | - | - | | `isLoading` | `boolean` | | - | - | | `emptyMessage` | `string` | | - | - | | `data` | `Partial<{ energyCostsUSD: number; operationalCostsUSD: number; }>` | | - | - | ### `PowerModeTimelineChart` Timeline chart for power-mode state changes over time. Wraps [`TimelineChart`](/reference/ui/components/charts/#timelinechart) with mining-specific data shaping. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `data` | `PowerModeTimelineEntry[]` | | - | Initial power-mode entries (each with start/end ts + mode). | | `dataUpdates` | `PowerModeTimelineEntry[]` | | - | Streaming updates appended to the initial data. | | `isLoading` | `boolean` | | - | Show a loading skeleton instead of the chart. | | `timezone` | `string` | | - | IANA timezone string for x-axis tick formatting. | | `title` | `string` | | - | Chart title. | ### `ProductionCostChart` Production cost over time, overlaid with BTC price. Mirrors the OSS `ProductionCostPriceChart` - both series rendered as bars on the same x-axis (bucket labels derived from `dateRange.period`). `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `costLog` | `readonly CostTimeSeriesEntry[]` | ✓ | - | - | | `btcPriceLog` | `readonly BtcPriceTimeSeriesEntry[]` | ✓ | - | - | | `dateRange` | `FinancialDateRange \| null` | ✓ | - | - | | `isLoading` | `boolean \| undefined` | | - | - | ### `RevenueChart` Stacked bar chart displaying monthly revenue per site. Automatically switches between BTC and Sats display based on value scale. Receives pre-fetched data as props — no internal data fetching. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `data` | `RevenueDataItem[]` | | - | - | | `isLoading` | `boolean` | | - | - | | `siteList` | `(string \| SiteItem)[]` | | - | - | | `legendPosition` | `Position` | | - | - | | `legendAlign` | `"center" \| "start" \| "end"` | | - | - | ### `ThresholdLineChart` Line chart with optional horizontal threshold lines. Wraps [`ChartContainer`](/reference/ui/components/charts/#chartcontainer) and [`LineChart`](/reference/ui/components/charts/#linechart); pass `series` and optional `thresholds` via `data`. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `title` | `string` | | - | - | | `unit` | `string` | | - | - | | `height` | `number` | | - | - | | `isTall` | `boolean` | | - | When true, uses a taller default height (360px). | | `className` | `string` | | - | - | | `emptyMessage` | `string` | | - | - | | `isLegendVisible` | `boolean` | | - | - | | `data` | `ThresholdLineChartData` | | - | - | | `yTicksFormatter` | `(value: number) => string` | | - | - | ### `TimelineChart` Discrete-event timeline chart (e.g. miner state over time) with a category legend. Supports streaming updates via `newData`. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `initialData` | `TimelineChartData` | ✓ | - | - | | `newData` | `TimelineChartData \| undefined` | | - | - | | `skipUpdates` | `boolean \| undefined` | | - | - | | `range` | `ChartRange \| undefined` | | - | - | | `axisTitleText` | `AxisTitleText \| undefined` | | - | - | | `isLoading` | `boolean \| undefined` | | - | - | | `title` | `string \| undefined` | | - | - | | `height` | `number \| undefined` | | - | - | # Dashboard (/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` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `counts` | `AlarmsBellButtonCounts \| undefined` | | - | Severity-bucketed alarm counts rendered in the stacked badge. | | `onClick` | `((event: React.MouseEvent) => void) \| undefined` | | - | Click handler — typically opens an alerts panel or routes to /alerts. | | `onSeverityClick` | `((severity: AlarmSeverity, event: React.MouseEvent) => void) \| undefined` | | - | Click handler for an individual severity count. When provided, each badge row becomes its own button so an operator can jump straight to the alerts page filtered by that severity (e.g. `/alerts?severity=critical`). When omitted, the counts render as plain (non-interactive) text. | | `label` | `string \| undefined` | | - | Accessible label. Defaults to "Active alarms". | | `className` | `string \| undefined` | | - | - | ### `DashboardDateRangePicker` Dashboard-friendly wrapper around the core [`DateRangePicker`](/reference/ui/components/forms/#daterangepicker) that speaks `{ start, end }` epoch-millisecond timestamps instead of `Date` objects, so it drops straight into `useDashboardDateRange` from `@tetherto/mdk-react-adapter`. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `value` | `DashboardDateRange` | ✓ | - | Current range as `{ start, end }` epoch-millisecond timestamps. | | `onChange` | `(next: DashboardDateRange) => void` | ✓ | - | Fires with the next `{ start, end }` window when the user applies a range. | | `dateFormat` | `string \| undefined` | | - | Display format. Defaults to `dd/MM/yyyy`. | | `disabled` | `boolean \| undefined` | | - | Disable the trigger. | | `className` | `string \| undefined` | | - | Optional class hook. | ### `ExportButton` Split-button trigger for downloading the current dashboard state. The left half labels the action (`↓ Export`); the right half opens a dropdown with the available formats and invokes `onExport(format)` on selection. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `onExport` | `(format: ExportFormat) => void` | ✓ | - | Fires with the chosen format when the user picks an item. | | `formats` | `readonly ExportFormat[] \| undefined` | | - | Formats to offer in the dropdown — defaults to `['csv', 'json']`. | | `label` | `string \| undefined` | | - | [**`Button`**](/reference/ui/components/actions/#button) label — defaults to `'Export'`. | | `disabled` | `boolean \| undefined` | | - | Disable the button. | | `className` | `string \| undefined` | | - | Optional class hook on the wrapper. | ### `HeaderConsumptionBox` Single-row consumption cell for the dashboard's header strip. The `1.663` style numeric is rendered in orange (the warning token) to match the Mining OS visual treatment. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `icon` | `React.ReactNode` | | - | - | | `valueMw` | `number \| undefined` | | - | Current site-level power consumption, in megawatts. | | `unit` | `string \| undefined` | | - | Unit label — defaults to `MW`. | | `className` | `string \| undefined` | | - | - | ### `HeaderEfficiencyBox` Single-row efficiency cell for the dashboard's header strip. Displays the W/TH/s metric derived from `power_w / hashrate_th`. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `icon` | `React.ReactNode` | | - | - | | `valueWthS` | `number \| undefined` | | - | Efficiency in watts per TH/s. | | `unit` | `string \| undefined` | | - | Unit label — defaults to `W/TH/S`. | | `className` | `string \| undefined` | | - | - | ### `HeaderHashrateBox` Two-row hashrate cell for the dashboard's header strip. Shows the app-side and pool-side aggregate hashrate side by side. Values fall back to `—` when undefined. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `icon` | `React.ReactNode` | | - | - | | `appPhs` | `number \| undefined` | | - | App-side aggregate hashrate in PH/s. | | `poolPhs` | `number \| undefined` | | - | Pool-side aggregate hashrate in PH/s. | | `unit` | `string \| undefined` | | - | [**`Hashrate`**](/reference/ui/components/charts/#hashrate) unit label — defaults to `PH/s`. | | `fractionDigits` | `number \| undefined` | | - | Decimal places shown for both values — defaults to `3`. | | `appLabel` | `string \| undefined` | | - | [**`Label`**](/reference/ui/components/forms/#label) for the app-side row — defaults to `APP` (`WEBAPP_SHORT_NAME`). | | `className` | `string \| undefined` | | - | - | ### `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` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `icon` | `React.ReactNode` | | - | Icon shown next to the "Miners" label. Caller-provided so the package stays icon-agnostic. | | `total` | `number \| undefined` | | - | Total miners across the site (denominator of the `158 / 2,188` ratio). | | `online` | `number \| undefined` | | - | Online miners (the `158` numerator). | | `error` | `number \| undefined` | | - | Miners flagged in warning (the small amber count). | | `offline` | `number \| undefined` | | - | Miners offline (the small red count). | | `appTotal` | `number \| undefined` | | - | Optional app-side meta line — total miners reporting to the app. | | `poolTotal` | `number \| undefined` | | - | Optional pool-side meta — total miners as reported by upstream pools. | | `poolOnline` | `number \| undefined` | | - | Optional pool-side online count (green). | | `poolMismatch` | `number \| undefined` | | - | Optional pool-side mismatch count (red). | | `appLabel` | `string \| undefined` | | - | [**`Label`**](/reference/ui/components/forms/#label) for the app-side row — defaults to `APP` (`WEBAPP_SHORT_NAME`). | | `className` | `string \| undefined` | | - | - | ### `HeaderStatsBar` Horizontal flex strip that hosts the dashboard's stat boxes ([**`HeaderMinersBox`**](/reference/ui/components/dashboard/#headerminersbox), [**`HeaderHashrateBox`**](/reference/ui/components/dashboard/#headerhashratebox), [**`HeaderConsumptionBox`**](/reference/ui/components/dashboard/#headerconsumptionbox), [**`HeaderEfficiencyBox`**](/reference/ui/components/dashboard/#headerefficiencybox)). Lives inside [``](/reference/ui/components/navigation/#appheader) as the middle slot. Slot-based: the caller decides which boxes to render and in which order. The bar interleaves an angled chevron divider between adjacent children to match the Mining OS visual treatment. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `children` | `React.ReactNode` | ✓ | - | Stat boxes to render in order, left-to-right. | | `className` | `string \| undefined` | | - | Optional class hook. | ### `ProfileMenu` Top-bar profile dropdown. Wraps the core DropdownMenu primitive with the user-avatar icon as the trigger. Items are caller-provided so the menu surface stays application-driven. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `items` | `ProfileMenuItem[]` | ✓ | - | Items rendered in the dropdown, top-to-bottom. Defaults to a single "Sign out" item. | | `user` | `React.ReactNode` | | - | Optional user label rendered at the top of the dropdown (e.g. an email). | | `icon` | `React.ReactNode` | | - | Override the trigger icon — defaults to the user-avatar icon. | | `label` | `string \| undefined` | | - | Accessible label for the trigger button. | | `className` | `string \| undefined` | | - | - | ### `SiteStatsBar` Site-level summary strip composed from [`WidgetTopRow`](/reference/ui/components/widgets/#widgettoprow) (title + power) and [`GenericDataBox`](/reference/ui/components/widgets/#genericdatabox) (hashrate / miner-count / container-count). Designed to sit at the top of a dashboard page above the chart cards. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `title` | `string` | ✓ | - | Site label rendered in the header row. | | `power` | `number \| undefined` | | - | Current site-level power consumption, in watts (or whatever `powerUnit` says). | | `powerUnit` | `string \| undefined` | | - | Display unit for `power` — defaults to `kW`. | | `totalHashrate` | `number \| undefined` | | - | Aggregate hashrate, in TH/s. | | `hashrateUnit` | `string \| undefined` | | - | [**`Hashrate`**](/reference/ui/components/charts/#hashrate) display unit — defaults to `TH/s`. | | `minerCount` | `number \| undefined` | | - | Total miner count across the site. | | `containerCount` | `number \| undefined` | | - | Total container count across the site. | | `isLoading` | `boolean \| undefined` | | - | Render a skeleton bar while data is loading. | | `className` | `string \| undefined` | | - | Optional class hook. | ### `TimelineSelector` Dropdown for picking the dashboard time range. Wraps `core/Select` and the canonical option list from `getTimelineOptions`. Pair with the `useDashboardTimeRange` hook in `@tetherto/mdk-react-adapter`. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `value` | `string` | ✓ | - | Currently selected timeline value (e.g. `'1m'`, `'5m'`). | | `onChange` | `(next: string) => void` | ✓ | - | Called whenever the user picks a new option. | | `options` | `TimelineOption[] \| undefined` | | - | Available options — defaults to `getTimelineOptions`. Pass a custom list to localise labels or restrict the range. | | `label` | `string \| undefined` | | - | ARIA label / placeholder for the trigger. | | `className` | `string \| undefined` | | - | Tailwind/BEM class hook on the trigger. | # Dashboard compositions (/reference/ui/components/dashboards) Pre-composed dashboard layouts and templates. ## Prerequisites - Complete the installation ```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 MiningOS voting/approval workflow. Three sections (only rendered when non-empty): - **Draft** — locally-staged actions not yet sent to the server. - **In review** — actions this user submitted, awaiting votes. - **Requested** — other users' voting actions this user can approve/reject (only shown when the current token has `actions:w`). Open/close state is driven by `actionsStore.sidebarOpen` so the header [`PendingActionsButton`](/reference/ui/components/dashboards/#pendingactionsbutton) and any in-page code can open it without prop-drilling. Mount this once at the app root — it renders nothing when closed. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `className` | `string \| undefined` | | - | Extra class names merged onto the sidebar root element. | ### `Cost` [**`Cost`**](/reference/ui/components/dashboards/#cost) Summary - composite reporting page (single-site). Reads cost-summary view model fields (from [`useCostSummary`](/reference/ui/hooks/utility/#usecostsummary)) and renders: - page header with "[**`Cost`**](/reference/ui/components/dashboards/#cost) Summary" title and an optional action slot (`setCostAction`) pinned to the opposite edge - period selector slot (`controls`) - pass [``](/reference/ui/components/filters/#timeframecontrols) for the OSS-style Year/Month picker or any other date selector element - shared [`CostContent`](/reference/ui/components/dashboards/#costcontent) 2x2 grid (charts + metric tiles) Multi-site is intentionally out of scope for this extraction wave. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `metrics` | `CostSummaryDisplayMetrics \| null` | ✓ | - | - | | `costLog` | `readonly CostTimeSeriesEntry[]` | ✓ | - | - | | `btcPriceLog` | `readonly BtcPriceTimeSeriesEntry[]` | ✓ | - | - | | `totals` | `CostSummaryMonetaryTotals \| null` | ✓ | - | - | | `isLoading` | `boolean \| undefined` | | - | - | | `error` | `unknown` | | - | - | | `dateRange` | `FinancialDateRange \| null` | ✓ | - | - | | `avgAllInCostData` | `readonly AvgAllInCostDataPoint[] \| undefined` | | - | Optional revenue/cost time-series for the Avg All-in [**`Cost`**](/reference/ui/components/dashboards/#cost) panel. | | `controls` | `React.ReactElement>` | ✓ | - | Period selector element. Pass [``](/reference/ui/components/filters/#timeframecontrols) for the OSS-style year/month picker. | | `setCostAction` | `React.ReactElement> \| undefined` | | - | Optional "Set Monthly [**`Cost`**](/reference/ui/components/dashboards/#cost)" header action slot. A `ReactElement` slot (rather than an href string) so consumers can hand in router-aware components like `` or ` 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 ```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 ```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 will change. 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 ```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 ```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 ```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 ```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` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `isOpen` | `boolean` | ✓ | - | - | | `onClose` | `VoidFunction` | ✓ | - | - | | `partTypes` | `SparePartSubTypesModalPartType[]` | ✓ | - | - | | `defaultPartTypeId` | `string \| undefined` | | - | - | | `modelOptions` | `FormSelectOption[]` | ✓ | - | - | | `isModelOptionsLoading` | `boolean \| undefined` | | - | - | | `minerModelOptions` | `FormSelectOption[]` | ✓ | - | - | | `statusOptions` | `FormSelectOption[]` | ✓ | - | - | | `locationOptions` | `FormSelectOption[]` | ✓ | - | - | | `isControllerPartTypeSelected` | `boolean \| undefined` | | - | - | | `onPartTypeChange` | `(partTypeId: string) => void` | ✓ | - | - | | `onSubmit` | `(values: AddSparePartFormValues) => Promise` | ✓ | - | - | | `isLoading` | `boolean \| undefined` | | - | - | | `subTypesPartTypes` | `SparePartSubTypesModalPartType[] \| undefined` | | - | - | | `subTypesActivePartTypeId` | `string \| undefined` | | - | - | | `subTypes` | `string[] \| undefined` | | - | - | | `onSubTypesPartTypeChange` | `((id: string) => void) \| undefined` | | - | - | | `onAddSubType` | `((name: string) => Promise) \| undefined` | | - | - | | `isSubTypesLoading` | `boolean \| undefined` | | - | - | ### `AlertDialogAction` Primary confirmation button inside an ``; clicking dismisses the dialog and runs the action handler. `agent-ready` ### `AlertDialogCancel` Secondary dismiss button inside an ``; closes the dialog without invoking the destructive action. `agent-ready` ### `AlertDialogContent` Modal content surface for an `` — renders the centered panel above the overlay with focus trap. `agent-ready` ### `AlertDialogDescription` Supporting body text inside an ``; conveys the consequences of the action being confirmed. `agent-ready` ### `AlertDialogFooter` Right-aligned action row inside an `` — hosts the cancel and confirm buttons. `agent-ready` ### `AlertDialogHeader` Top section of an `` that groups the title and description above the action row. `agent-ready` ### `AlertDialogOverlay` Full-viewport scrim rendered behind an `` to block interaction with the page. `agent-ready` ### `AlertDialogTitle` Prominent title text inside an `` summarising the action that requires confirmation. `agent-ready` ### `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` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `isOpen` | `boolean` | ✓ | - | - | | `onClose` | `() => void` | ✓ | - | - | | `onSubmit` | `(values: { pool: PoolSummary; }) => Promise` | ✓ | - | - | | `miners` | `Device[]` | ✓ | - | - | | `poolConfig` | `PoolConfigEntry[]` | ✓ | - | - | ### `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` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `isOpen` | `boolean` | ✓ | - | - | | `onClose` | `VoidFunction` | ✓ | - | - | | `spareParts` | `BatchMoveSparePart[]` | ✓ | - | - | | `locationOptions` | `FormSelectOption[]` | ✓ | - | - | | `statusOptions` | `FormSelectOption[]` | ✓ | - | - | | `onSubmit` | `(values: { location: string \| null; status: string \| null; observation: string \| null; }) => void \| Promise` | ✓ | - | - | ### `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` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `isOpen` | `boolean` | ✓ | - | - | | `onClose` | `VoidFunction` | ✓ | - | - | | `onSubmit` | `(records: CSVRecord[]) => Promise` | ✓ | - | - | | `isLoading` | `boolean \| undefined` | | - | - | ### `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` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `isOpen` | `boolean \| undefined` | | - | - | | `onClose` | `VoidFunction \| undefined` | | - | - | | `onConfirm` | `((sparePart: ConfirmDeleteSparePartModalSparePart) => void \| Promise) \| undefined` | | - | - | | `sparePart` | `ConfirmDeleteSparePartModalSparePart \| undefined` | | - | - | | `isLoading` | `boolean \| undefined` | | - | - | ### `DialogContent` Centered modal surface for a `` — renders above the overlay with focus trap and Escape-to-close. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `title` | `string \| undefined` | | - | - | | `description` | `string \| undefined` | | - | - | | `closeOnClickOutside` | `boolean \| undefined` | | - | - | | `closeOnEscape` | `boolean \| undefined` | | - | - | | `bare` | `boolean \| undefined` | | - | - | | `closable` | `boolean \| undefined` | | - | - | | `onClose` | `VoidFunction \| undefined` | | - | - | ### `DialogDescription` Supporting body copy inside a `` rendered below the title. `agent-ready` ### `DialogFooter` Action row at the bottom of a `` — typically primary/secondary buttons. `agent-ready` ### `DialogHeader` Top region of a `` that groups the title and description. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `bare` | `boolean \| undefined` | | - | - | | `closable` | `boolean \| undefined` | | - | - | | `onClose` | `VoidFunction \| undefined` | | - | - | ### `DialogOverlay` Full-viewport scrim rendered behind an open `` to block background interaction. `agent-ready` ### `DialogTitle` Prominent title text at the top of a `` summarising the modal's purpose. `agent-ready` ### `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` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `isOpen` | `boolean` | | - | - | | `onClose` | `() => void` | | - | - | | `movement` | `MovementData` | | - | - | ### `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` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `isOpen` | `boolean \| undefined` | | - | - | | `onClose` | `VoidFunction \| undefined` | | - | - | | `sparePart` | `MoveSparePartModalSparePart \| undefined` | | - | - | | `requestedValues` | `{ location?: string \| undefined; status?: string \| undefined; } \| undefined` | | - | - | | `locationOptions` | `FormSelectOption[]` | ✓ | - | - | | `statusOptions` | `FormSelectOption[]` | ✓ | - | - | | `onSubmit` | `(values: { location: string; status: string; observation: string; }, sparePart: MoveSparePartModalSparePart) => void \| Promise` | ✓ | - | - | ### `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` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `isOpen` | `boolean` | ✓ | - | - | | `onClose` | `VoidFunction` | ✓ | - | - | | `partTypes` | `SparePartSubTypesModalPartType[]` | ✓ | - | - | | `activePartTypeId` | `string` | ✓ | - | - | | `onPartTypeChange` | `(id: string) => void` | ✓ | - | - | | `subTypes` | `string[]` | ✓ | - | - | | `onAddSubType` | `(name: string) => Promise` | ✓ | - | - | | `isLoading` | `boolean \| undefined` | | - | - | # 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` ### `AvatarFallback` Initials or icon placeholder shown inside [``](/reference/ui/components/display/#avatar) while the image loads or when no image is available. `agent-ready` ### `AvatarImage` Profile image slot inside [``](/reference/ui/components/display/#avatar) — renders `src` and triggers the fallback on load failure. `agent-ready` ### `Badge` [**`Badge`**](/reference/ui/components/display/#badge) component - Display badge with number or dot `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `children` | `React.ReactNode` | | - | [**`Badge`**](/reference/ui/components/display/#badge) content (wraps children with badge) | | `count` | `number \| undefined` | | - | Number to display in badge If > overflowCount, will show "overflowCount+" | | `overflowCount` | `number \| undefined` | | `99` | Maximum count to display | | `showZero` | `boolean \| undefined` | | `false` | Whether to show badge when count is 0 | | `dot` | `boolean \| undefined` | | `false` | Show badge as a dot | | `text` | `string \| undefined` | | - | Custom badge content (overrides count) | | `color` | `ColorVariant \| undefined` | | `'primary'` | Color variant | | `size` | `ComponentSize \| undefined` | | `'md'` | [**`Badge`**](/reference/ui/components/display/#badge) size | | `offset` | `[number, number] \| undefined` | | `[0, 0]` | Offset position [x, y] in pixels | | `square` | `boolean \| undefined` | | `false` | Square badge (no border-radius) | | `className` | `string \| undefined` | | - | Custom className for badge | | `wrapperClassName` | `string \| undefined` | | - | Custom className for wrapper | | `title` | `string \| undefined` | | - | [**`Badge`**](/reference/ui/components/display/#badge) title for accessibility | | `status` | `"success" \| "warning" \| "error" \| "default" \| "processing" \| undefined` | | - | Status badge (small dot badge with text) | ### `BtcAveragePrice` Read-only BTC average price label for reporting toolbars. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `price` | `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). | | `label` | `string` | | - | [**`Label`**](/reference/ui/components/forms/#label) for the BTC average price. | ### `DataLabel` Read-only period label (`PERIOD: start - end`) with timezone-aware dates. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `startDate` | `Date \| null` | | - | Range start; formatted in the active timezone (`dd/MM/yy`). | | `endDate` | `Date \| null` | | - | Range end; formatted in the active timezone (`dd/MM/yy`). | | `label` | `string` | | - | [**`Label`**](/reference/ui/components/forms/#label) text; defaults to `PERIOD`. | ### `Indicator` [**`Indicator`**](/reference/ui/components/display/#indicator) component - display status with colored background and label `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `color` | `"red" \| "gray" \| "blue" \| "yellow" \| "green" \| "purple" \| "amber" \| "slate" \| undefined` | | `'gray'` | Color variant of the indicator | | `size` | `ComponentSize \| undefined` | | `'md'` | Size variant of the indicator | | `className` | `string \| undefined` | | - | Custom className for the root element | | `vertical` | `boolean \| undefined` | | `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. | | `children` | `React.ReactNode` | | - | Children content (can include text, icons, multiple elements) | | `onClick` | `(VoidFunction & React.MouseEventHandler) \| undefined` | | - | Click handler | ### `Tag` [**`Tag`**](/reference/ui/components/display/#tag) component - display labels, categories, or status `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `color` | `"red" \| "blue" \| "green" \| "amber" \| "dark" \| undefined` | | `'dark'` | Color variant of the tag | | `className` | `string \| undefined` | | - | Custom className for the root element | | `children` | `React.ReactNode` | | - | Children content | ### `Typography` [**`Typography`**](/reference/ui/components/display/#typography) component for consistent text styling `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `variant` | `"body" \| "caption" \| "secondary" \| "heading1" \| "heading2" \| "heading3" \| undefined` | | `'body'` | [**`Typography`**](/reference/ui/components/display/#typography) variant | | `size` | `"sm" \| "md" \| "lg" \| "xs" \| "xl" \| "2xl" \| "3xl" \| "4xl" \| undefined` | | `undefined (uses variant default)` | Text size | | `weight` | `"medium" \| "normal" \| "light" \| "semibold" \| "bold" \| undefined` | | `undefined (uses variant default)` | Font weight | | `align` | `TextAlign \| undefined` | | - | Text alignment | | `color` | `"success" \| "warning" \| "error" \| "primary" \| "default" \| "muted" \| undefined` | | - | Text color variant | | `truncate` | `boolean \| undefined` | | - | Truncate text with ellipsis | | `className` | `string \| undefined` | | - | Custom className | # 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` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `name` | `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. | | `tabs` | `ContainerDetailTab[]` | ✓ | - | Ordered tabs for this container model (already resolved by the page). | | `activeTab` | `string` | ✓ | - | Currently active tab key. | | `onTabChange` | `(tab: string) => void` | ✓ | - | Fired with the next tab key when the operator switches tabs. | | `onBack` | `() => void` | ✓ | - | Fired when the back link is clicked (the page decides where to go). | | `backLabel` | `React.ReactNode` | | - | Back-link label. Defaults to "Explorer". | | `children` | `React.ReactNode` | | - | The active tab's body — supplied by the page (real content or a placeholder). | | `className` | `string \| undefined` | | - | - | ### `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` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `containers` | `ContainerWidgetItem[]` | ✓ | - | Card-ready data for every container, shaped by the data hook. | | `title` | `string \| undefined` | | - | Section heading. | | `isLoading` | `boolean \| undefined` | | - | Shows a spinner while the first load is in flight. | | `errorMessage` | `string \| undefined` | | - | Error message shown in place of the grid. | | `onContainerClick` | `((id: string) => void) \| undefined` | | - | Invoked with the container id when a card is clicked. | | `className` | `string \| undefined` | | - | - | ### `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` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `title` | `string \| undefined` | | - | Page heading. | | `headerActions` | `React.ReactNode` | | - | Optional header controls (export button, etc.) shown next to the title. | | `list` | `React.ReactNode` | ✓ | - | The list column — typically a tab switch plus the device/container table. | | `detail` | `React.ReactNode` | | - | The detail column content (shown in the sticky panel when `hasSelection`). | | `hasSelection` | `boolean \| undefined` | | - | 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. | | `className` | `string \| undefined` | | - | - | # 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 ```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` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `alarmsData` | `unknown` | ✓ | - | - | | `onNavigate` | `(path: string) => void` | ✓ | - | - | ### `AlarmRow` Single alarm-feed row with severity dot, timestamp, source device, and the alert message. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `data` | `TimelineItemData` | ✓ | - | - | | `onNavigate` | `(path: string) => void` | ✓ | - | - | ### `CoreAlert` Inline alert banner with type-based icon, optional description, dismissible close button, and trailing action slot. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `type` | `AlertType \| undefined` | | - | Alert type | | `title` | `React.ReactNode` | | - | Main message | | `description` | `React.ReactNode` | | - | Additional description | | `showIcon` | `boolean \| undefined` | | - | Show the type icon | | `icon` | `React.ReactNode` | | - | Custom icon (used when showIcon is true) | | `closable` | `boolean \| undefined` | | - | Makes the alert closable | | `onClose` | `React.MouseEventHandler \| undefined` | | - | Called when close button is clicked | | `banner` | `boolean \| undefined` | | - | Display as full-width banner (no border radius, no margin) | | `action` | `React.ReactNode` | | - | Action element rendered to the right | | `className` | `string \| undefined` | | - | Custom className | | `style` | `React.CSSProperties \| undefined` | | - | Custom styles | ### `EmptyState` Empty state component for displaying placeholder content when no data is available. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `description` | `React.ReactNode` | ✓ | - | Description text or ReactNode displayed below the image | | `image` | `EmptyStateImage` | | `"default"` | Image to display. Use "default" for the standard illustration, "simple" for a minimal icon, or pass a custom ReactNode. | | `size` | `ComponentSize \| undefined` | | `"md"` | Size variant controlling spacing and icon dimensions | | `className` | `string \| undefined` | | - | Additional CSS class name | ### `ErrorCard` Error card component for displaying error messages. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `error` | `string` | ✓ | - | Error message string. Supports `\\n` for line breaks. | | `title` | `string \| undefined` | | `"Errors"` | Title displayed above the error message | | `variant` | `ErrorCardVariant \| undefined` | | `"card"` | Display variant. "card" shows a bordered container, "inline" shows flat text. | | `className` | `string \| undefined` | | - | Additional CSS class name | ### `Loader` [**`Loader`**](/reference/ui/components/feedback/#loader) component - display pulsing dots animation `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `size` | `number \| undefined` | | `10` | Size of each dot in pixels | | `count` | `3 \| 5 \| 7 \| undefined` | | `5` | Number of dots to display | | `color` | `"red" \| "gray" \| "blue" \| "amber" \| "orange" \| undefined` | | `'orange'` | Color variant of the loader | | `className` | `string \| undefined` | | - | Custom className for the root element | ### `SkeletonBlock` Rectangular shimmer placeholder used to hint at content shape while data is loading. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `circle` | `boolean` | | - | - | | `className` | `string` | | - | - | | `width` | `string \| number` | | - | - | | `height` | `string \| number` | | - | - | | `borderRadius` | `string \| number` | | - | - | ### `Spinner` [**`Spinner`**](/reference/ui/components/feedback/#spinner) component - display loading state with rotating squares `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `size` | `ComponentSize \| undefined` | | `'md'` | Size variant of the spinner | | `color` | `"primary" \| "secondary" \| undefined` | | `'primary'` | Color variant of the spinner | | `fullScreen` | `boolean \| undefined` | | `false` | Whether to display in fullscreen mode | | `className` | `string \| undefined` | | - | Custom className for the root element | | `label` | `string \| undefined` | | - | Optional label text to display below the spinner | | `speed` | `"slow" \| "normal" \| "fast" \| undefined` | | `'normal'` | Speed of the animation | | `type` | `"circle" \| "square" \| undefined` | | `'dot'` | Type of spinner animation | ### `Toast` Single transient notification rendered inside the [``](/reference/ui/components/feedback/#toaster) viewport. Use `variant` to convey intent (`default`, `success`, `error`, `warning`, `info`); 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` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `description` | `string \| undefined` | | - | - | | `variant` | `NotificationVariant \| undefined` | | - | - | | `icon` | `React.JSX.Element \| undefined` | | - | - | ### `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 bottom-right placement. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `position` | `ToastPosition \| undefined` | | - | - | # 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` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `options` | `CascaderOption[]` | ✓ | - | [**`Cascader`**](/reference/ui/components/forms/#cascader) options for filtering | | `filterKey` | `string \| undefined` | | - | 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` | `LocalFilters \| undefined` | | - | Current filter values as key-value pairs Example: { type: 'Antminer S19XP H', status: ['active', 'pending'] } | | `onChange` | `(selections: CascaderValue[]) => void` | ✓ | - | Callback when filters change | | `className` | `string \| undefined` | | - | Custom className for the filter button | ### `ReportTimeFrameSelector` Reporting-period selector with preset windows (7d / 30d / month-to-date / custom range). `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `dateRange` | `[Date, Date]` | ✓ | - | - | | `presetTimeFrame` | `number \| null` | ✓ | - | - | | `setPresetTimeFrame` | `(value: number \| null) => void` | ✓ | - | - | | `setDateRange` | `(value: [Date, Date]) => void` | ✓ | - | - | ### `TimeframeControls` Date-range picker for financial reporting pages — supports year, month, and week granularity via connected selects. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `hint` | `string` | | - | - | | `onReset` | `VoidFunction` | | - | - | | `showResetButton` | `boolean` | | - | - | | `isWeekSelectVisible` | `boolean` | | - | - | | `isMonthSelectVisible` | `boolean` | | - | - | | `layout` | `"horizontal" \| "stacked"` | | - | - | | `dateRange` | `TimeframeControlsDateRange` | | - | - | | `timeframeType` | `TimeframeTypeValue \| null` | | - | - | | `onRangeChange` | `TimeframeControlsOnRangeChange` | | - | - | | `onTimeframeTypeChange` | `(type: TimeframeTypeValue) => void` | | - | - | ### `TimeframeWeekFlatContent` Flat list of selectable week items for the [**`TimeframeControls`**](/reference/ui/components/filters/#timeframecontrols) week selector. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `visibleWeeks` | `Week[]` | ✓ | - | - | ### `TimeframeWeekTreeContent` Hierarchical year → month → week tree for the [**`TimeframeControls`**](/reference/ui/components/filters/#timeframecontrols) week selector. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `timezone` | `string` | ✓ | - | - | | `selectedYear` | `number` | ✓ | - | - | | `selectedMonth` | `number` | ✓ | - | - | # 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`. - 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 ```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. #### `DatePicker` props | Prop | Type | Required | Default | Description | | ------------------- | ------------------------------- | -------- | -------------- | --------------------------------- | | `selected` | `Date` | no | — | Selected date. | | `onSelect` | `(date?: Date) => void` | no | — | Setter. | | `placeholder` | `string` | no | `"Pick a date"`| Trigger button placeholder. | | `dateFormat` | `string` | no | `"MM/dd/yyyy"` | `date-fns` format string. | | `disabled` | `boolean` | no | `false` | Disable the trigger. | | `triggerClassName` | `string` | no | — | Class names on the trigger button.| | `calendarClassName` | `string` | no | — | Class names on the day-picker. | #### `DateRangePicker` props Adds `showPresets`, `presets: PresetItem[]`, `allowFutureDates`, and `modalClassName`. `selected` / `onSelect` use `DateRange`. #### Data contracts ```ts type DateRange = { from: Date | undefined; to?: Date | undefined }; type PresetItem = { label: string; value: DateRange }; ``` #### Example ```tsx /** * Runnable example for DatePicker and DateRangePicker. */ const [date, setDate] = useState() const [range, setRange] = useState() return (
) } ``` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `Cascader` Two-panel hierarchical selector for picking a leaf value from a nested tree (categories → subcategories → leaf). Features: - Two-column layout: categories on left, options on right - Single or multiple selection modes - Search/filter functionality via [**`TagInput`**](/reference/ui/components/forms/#taginput) - Category-level selection (select/deselect all children) - Indeterminate state for partial selections - [**`Tag`**](/reference/ui/components/display/#tag) display for multiple selections - Keyboard navigation support - Disabled state support `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `options` | `CascaderOption[]` | ✓ | - | Hierarchical options to display in the cascader Parent options with children appear in the left panel Child options appear in the right panel when parent is selected | | `value` | `CascaderValue \| CascaderValue[] \| undefined` | | - | Current selected value(s) - For single select: CascaderValue (e.g., ['category', 'option']) - For multiple select: CascaderValue[] (e.g., [['cat1', 'opt1'], ['cat2', 'opt2']]) | | `onChange` | `((value: CascaderValue \| CascaderValue[] \| null) => void) \| undefined` | | - | Callback when selection changes - For single select: receives CascaderValue or null - For multiple select: receives CascaderValue[] or null | | `multiple` | `boolean \| undefined` | | `false` | Enable multiple selection mode - true: Shows checkboxes, allows multiple selections, displays selected items as tags - false: Shows radio buttons, allows single selection | | `placeholder` | `string \| undefined` | | `'Select...'` | Placeholder text shown in the input when no selections are made | | `disabled` | `boolean \| undefined` | | `false` | Disable the entire cascader (input and all options) | | `className` | `string \| undefined` | | - | Custom className for the root cascader element | | `dropdownClassName` | `string \| undefined` | | - | Custom className for the dropdown panels container | ### `Checkbox` [**`Checkbox`**](/reference/ui/components/forms/#checkbox) component with full customization `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `size` | `CheckboxSize \| undefined` | | `'md'` | Size variant of the checkbox | | `color` | `"success" \| "warning" \| "error" \| "primary" \| "default" \| undefined` | | `'primary'` | Color variant when checked | | `radius` | `BorderRadius \| undefined` | | `'none'` | Border radius variant | | `className` | `string \| undefined` | | - | Custom className for the root element | | `indicatorClassName` | `string \| undefined` | | - | Custom className for the indicator element | | `onCheckedChange` | `(((checked: CheckedState) => void) & ((checked: CheckedState) => void)) \| undefined` | | - | Callback when the checked state changes | ### `CurrencyToggler` [**`CurrencyToggler`**](/reference/ui/components/forms/#currencytoggler) component for switching between currencies `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `value` | `string` | ✓ | - | - | | `className` | `string \| undefined` | | - | - | | `currencies` | `(string \| CurrencyItem)[]` | ✓ | - | - | | `onChange` | `(currency: string) => void` | ✓ | - | - | ### `DatePicker` Single-date selection component built on react-day-picker with the MDK dark theme. Controlled via `selected` / `onSelect`. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `selected` | `Date \| undefined` | | - | Currently selected date | | `onSelect` | `((date: Date \| undefined) => void) \| undefined` | | - | Callback when date changes | | `placeholder` | `string \| undefined` | | `"Pick a date"` | Placeholder text when no date is selected | | `dateFormat` | `string \| undefined` | | `"MM/dd/yyyy"` | Date format for display | | `disabled` | `(boolean & (Matcher \| Matcher[])) \| undefined` | | `false` | Whether the picker is disabled | | `triggerClassName` | `string \| undefined` | | - | Custom className for the trigger button | | `calendarClassName` | `string \| undefined` | | - | Custom className for the calendar | ### `DateRangePicker` Date-range selection component with preset shortcuts (last 7/14/30/90 days) and a modal interface. Controlled via `selected` / `onSelect`. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `selected` | `DateRange \| undefined` | | - | Selected date range | | `onSelect` | `((range: DateRange \| undefined) => void) \| undefined` | | - | Callback when date range changes | | `placeholder` | `string \| undefined` | | `"Pick a date range"` | Placeholder text when no range is selected | | `dateFormat` | `string \| undefined` | | `"MM/dd/yyyy"` | Date format for display | | `disabled` | `(boolean & (Matcher \| Matcher[])) \| undefined` | | `false` | Whether the picker is disabled | | `showPresets` | `boolean \| undefined` | | `true` | Whether to show preset buttons | | `presets` | `PresetItem[] \| undefined` | | - | Custom preset items | | `allowFutureDates` | `boolean \| undefined` | | `false` | Whether to allow future dates | | `triggerClassName` | `string \| undefined` | | - | Custom className for the trigger button | | `calendarClassName` | `string \| undefined` | | - | Custom className for the calendar | | `modalClassName` | `string \| undefined` | | - | Custom className for the modal | ### `Form` React Hook [**`Form`**](/reference/ui/components/forms/#form) provider wrapper. Pass the result of `useForm()` as `form` and render fields via [`FormField`](/reference/ui/components/forms/#formfield) / [`FormItem`](/reference/ui/components/forms/#formitem) / [`FormLabel`](/reference/ui/components/forms/#formlabel) / [`FormControl`](/reference/ui/components/forms/#formcontrol) / [`FormMessage`](/reference/ui/components/forms/#formmessage). `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `form` | `UseFormReturn` | ✓ | - | - | | `children` | `React.ReactNode` | ✓ | - | - | ### `FormCascader` Pre-built [**`Cascader`**](/reference/ui/components/forms/#cascader) field component with integrated form state. Perfect for hierarchical selections like categories and subcategories. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string \| undefined` | | - | - | | `description` | `string \| undefined` | | - | - | | `placeholder` | `string \| undefined` | | - | - | | `options` | `CascaderOption[]` | ✓ | - | - | | `multiple` | `boolean \| undefined` | | - | - | | `cascaderProps` | `Omit, "onChange" \| "value" \| "options" \| "placeholder"> \| undefined` | | - | - | ### `FormCheckbox` Pre-built [**`Checkbox`**](/reference/ui/components/forms/#checkbox) field component with integrated form state. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string \| undefined` | | - | - | | `description` | `string \| undefined` | | - | - | | `placeholder` | `string \| undefined` | | - | - | | `checkboxProps` | `({ size?: CheckboxSize \| undefined; color?: ComponentColor \| undefined; radius?: BorderRadius \| undefined; className?: string \| undefined; indicatorClassName?: string \| undefined; onCheckedChange?: ((checked: CheckedState) => void) \| undefi… /* see source */` | | - | - | | `layout` | `"row" \| "column" \| undefined` | | - | - | ### `FormControl` Slot-based wrapper that injects ARIA attributes onto its child input element without adding an extra DOM wrapper. `agent-ready` ### `FormDatePicker` Pre-built [**`DatePicker`**](/reference/ui/components/forms/#datepicker) field component with integrated form state. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string \| undefined` | | - | - | | `description` | `string \| undefined` | | - | - | | `placeholder` | `string \| undefined` | | - | - | | `datePickerProps` | `Omit<{ selected?: Date \| undefined; onSelect?: ((date: Date \| undefined) => void) \| undefined; placeholder?: string \| undefined; dateFormat?: string \| undefined; disabled?: boolean \| undefined; triggerClassName?: string \| undefined; calenda… /* see source */` | | - | - | ### `FormDescription` Optional helper text displayed below the input. `agent-ready` ### `FormField` Wraps react-hook-form's Controller and provides field context to descendants. `agent-ready` ### `FormInput` Pre-built [**`Input`**](/reference/ui/components/forms/#input) field component with integrated form state. Reduces boilerplate by combining [**`FormField`**](/reference/ui/components/forms/#formfield), [**`FormItem`**](/reference/ui/components/forms/#formitem), [**`FormLabel`**](/reference/ui/components/forms/#formlabel), [**`FormControl`**](/reference/ui/components/forms/#formcontrol), and [**`FormMessage`**](/reference/ui/components/forms/#formmessage). `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string \| undefined` | | - | - | | `description` | `string \| undefined` | | - | - | | `placeholder` | `string \| undefined` | | - | - | | `type` | `React.HTMLInputTypeAttribute \| undefined` | | - | - | | `variant` | `"search" \| "default" \| undefined` | | - | - | | `inputProps` | `Omit & React.RefAttributes, "type" \| "variant"> \| undefined` | | - | - | ### `FormItem` Layout wrapper for a form field. Generates a unique ID for accessibility linking. `agent-ready` ### `FormLabel` [**`Label`**](/reference/ui/components/forms/#label) that auto-links to the form field input via generated IDs. Applies error styling when the field has a validation error. `agent-ready` ### `FormMessage` Displays the validation error message from react-hook-form field state. Falls back to children if no error is present. Always renders to prevent layout shift when errors appear. `agent-ready` ### `FormRadioGroup` Pre-built [**`RadioGroup`**](/reference/ui/components/forms/#radiogroup) field component with integrated form state. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string \| undefined` | | - | - | | `description` | `string \| undefined` | | - | - | | `placeholder` | `string \| undefined` | | - | - | | `options` | `FormRadioOption[]` | ✓ | - | - | | `orientation` | `"horizontal" \| "vertical" \| undefined` | | - | - | | `radioGroupProps` | `Omit<{ orientation?: "horizontal" \| "vertical" \| undefined; noGap?: boolean \| undefined; className?: string \| undefined; } & Omit, "ref"> & React.RefAttributes, "defaultV… /* see source */` | | - | - | ### `FormSelect` Pre-built [**`Select`**](/reference/ui/components/forms/#select) field component with integrated form state. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string \| undefined` | | - | - | | `description` | `string \| undefined` | | - | - | | `placeholder` | `string \| undefined` | | - | - | | `options` | `FormSelectOption[]` | ✓ | - | - | | `selectProps` | `Omit \| undefined` | | - | - | ### `FormSwitch` Pre-built [**`Switch`**](/reference/ui/components/forms/#switch) field component with integrated form state. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string \| undefined` | | - | - | | `description` | `string \| undefined` | | - | - | | `placeholder` | `string \| undefined` | | - | - | | `switchProps` | `Omit<{ size?: ComponentSize \| undefined; color?: ComponentColor \| undefined; radius?: BorderRadius \| undefined; className?: string \| undefined; thumbClassName?: string \| undefined; } & Omit, "label" \| "value" \| "placeholder" \| "onTagsChange"> \| undefined` | | - | - | ### `FormTextArea` Pre-built [**`TextArea`**](/reference/ui/components/forms/#textarea) field component with integrated form state. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `label` | `string \| undefined` | | - | - | | `description` | `string \| undefined` | | - | - | | `placeholder` | `string \| undefined` | | - | - | | `textAreaProps` | `(Omit & React.RefAttributes) \| undefined` | | - | - | ### `Input` Text input with optional label, prefix/suffix slots, and a `search` variant. Forwards refs and all native `` attributes. `agent-ready` #### Props | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `error` | `string \| undefined` | | - | Validation error message. When provided, displays error styling (red border) and the message below the input. | | `label` | `string \| undefined` | | - | Optional label displayed above the input | | `prefix` | `React.ReactNode` | | - | Prefix element displayed before the input (left side) | | `variant` | `"search" \| "default" \| undefined` | | `'default'` | Variant of the input - `default`: Standard text input - `search`: [**`Input`**](/reference/ui/components/forms/#input) with magnifying glass icon on the right | | `size` | `InputSize \| undefined` | | `'default'` | Size of the input - `default`: padding 10px 12px, icon 16px - `medium`: padding 6px 12px, icon 12px | | `wrapperClassName` | `string \| undefined` | | - | Custom className for the root wrapper | | `suffix` | `React.ReactNode` | | - | Suffix element displayed after the input (right side) | ### `Label` Accessible text label for form controls. Associates with an input via `htmlFor` and supports a required-mark indicator. Built on Radix [**`Label`**](/reference/ui/components/forms/#label). `agent-ready` ### `MultiLevelSelect` Multi-level select component with collapsible sections. `agent-ready` ### `MultiSelect` Multi-select picker built on Radix [**`Popover`**](/reference/ui/components/overlays/#popover) + [**`Checkbox`**](/reference/ui/components/forms/#checkbox). Sibling to [` )} /> ) } ``` @tetherto/mdk-react-devkit ### Reset form state ```tsx ``` #### Related API - [**`Form`**](/reference/ui/components/forms/#form) - [**`FormInput`**](/reference/ui/components/forms/#forminput) ### When to use `useFormReset` Use `useFormReset` when resetting a [`react-hook-form`](https://react-hook-form.com/) form also needs before/after callbacks or a reusable dirty-state guard. Call the form object's `reset` method directly when no lifecycle behaviour is needed. ### `useFormReset` workflow Create the form with `useForm`, pass that instance to `useFormReset`, then wire the returned `resetForm` handler to a reset or cancel action. The hook reports `isDirty` from the same form state so the action can be disabled until values change. ### `useFormReset` prerequisite The hook requires a configured `react-hook-form` instance. It composes with [`Form`](/reference/ui/components/forms/#form) and fields such as [`FormInput`](/reference/ui/components/forms/#forminput); it does not create or submit the form. ### `useFormReset` example ```tsx type MinerFields = { name: string; ip: string } function MinerEditForm({ onSubmit }: { onSubmit: (values: MinerFields) => void }) { const form = useForm({ defaultValues: { name: '', ip: '' } }) const { resetForm, isDirty } = useFormReset({ form, onAfterReset: () => console.log('Form reset'), }) return (
) } ``` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `useFormField` Read-only context hook for form field children — returns the field's id, error state, and ARIA attributes. ```typescript () => UseFormFieldReturn ``` ### `useFormReset` Hook to handle form reset with callbacks. ```typescript ({ form, onBeforeReset, onAfterReset, }: UseFormResetOptions) => UseFormResetReturn ``` # Miscellaneous (/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 centre (/reference/ui/hooks/op-centre) Hooks for operations centre functionality and monitoring. ## From `@tetherto/mdk-react-adapter` @tetherto/mdk-react-adapter Import the public APIs on this page from [`@tetherto/mdk-react-adapter`](/reference/ui/#tethertomdk-react-adapter). ### `useCabinetDevices` Fetches one LV cabinet's family of devices — the powermeters and temperature sensors whose `info.pos` sits under the cabinet `root` (`buildCabinetDetailParams`) — polled at the Op-Centre realtime cadence and flattened across the per-… ```typescript (root: string, options: UseCabinetDevicesOptions = {}) => UseCabinetDevicesResult ``` ### `useCabinetGroups` Fetches the Explorer cabinet-tab devices (powermeters + temperature sensors) and groups them by their owning container (`info.container`); devices without a container assignment (site-level meters) collect under the `site` group, sorted la… ```typescript (options: UseCabinetGroupsOptions = {}) => UseCabinetGroupsResult ``` ### `useContainerSettings` Fetches per-model container thresholds/parameters from `GET /auth/global/data?type=containerSettings`. Feeds the threshold status indicators (tank pressure, oil/water temperature) on the container widgets and detail tabs. ```typescript (options: UseContainerSettingsOptions = {}) => UseContainerSettingsResult ``` ### `useContainerSnapshots` Fetches the detail snapshots for the selected containers — the richer projection (`buildContainerDetailParams`) that carries `container_specific.pdu_data` plus the tank / cooling / power-mode config the detail panel controls read. `c… ```typescript (containerKeys: string[], options: UseContainerSnapshotsOptions = {}) => UseContainerSnapshotsResult ``` ### `useContainerWidgets` Data source for the Site Overview Container Widgets grid: the container inventory (one card per container) plus the latest per-miner realtime aggregate sample the cards derive their summaries from. Card-shaped payload derivation lives with… ```typescript (options: UseContainerWidgetsOptions = {}) => UseContainerWidgetsResult ``` ### `useExplorerList` Fetches the thing list behind one Explorer tab (`container` / `miner` / `cabinet`) from `GET /auth/list-things`, tag-filtered and projected by the foundation's Explorer params builder. Rows are flattened across the per-Kernel envelope so r… ```typescript (tab: ExplorerTabValue, options: UseExplorerListOptions = {}) => UseExplorerListResult ``` ### `useFeatureFlags` Fetches the deployment feature flags from `GET /auth/featureConfig` (camelCase route — there is no `/auth/feature-config`). Static deployment config — fetched once per session, no polling. Gates optional tabs/sub-routes (`containerCharts`,… ```typescript (options: UseFeatureFlagsOptions = {}) => UseFeatureFlagsResult ``` ### `usePduLayout` Fetches a container type's static PDU socket grid from `GET /auth/pdu-layout`. The grid is provisioned in the container worker's `pduGridLayout` config keyed by the exact type string — an unprovisioned type is a backend 400 (`ERR_PDU_LAYOU… ```typescript (params: UsePduLayoutParams, options: UsePduLayoutOptions = {}) => UsePduLayoutResult ``` ### `useRackLayout` Fetches the rack structure for a worker type from `GET /auth/list-racks` (`type` is required — the backend 400s with `ERR_TYPE_INVALID` without it). Feeds the Explorer rack grouping. ```typescript (params: UseRackLayoutParams, options: UseRackLayoutOptions = {}) => UseRackLayoutResult ``` ### `useSite` Fetches the configured site label from `GET /auth/site`. Static deployment config — fetched once per session, no polling. ```typescript (options: UseSiteOptions = {}) => UseSiteResult ``` ### `useThingComment` Device-comment writes for the Explorer detail panel — add, edit, and delete against `/auth/thing/comment` (the author is stamped server-side from the session token). Comments ride on the thing rows themselves (`comments` in the list-things… ```typescript () => UseThingCommentResult ``` ### `useThingDetail` Fetches a single thing by id from `GET /auth/list-things` with the full Op Centre field projection — the data source for the Explorer detail panel and the container Thing-detail view. ```typescript (id: string | undefined, options: UseThingDetailOptions = {}) => UseThingDetailResult ``` ## From `@tetherto/mdk-react-devkit` @tetherto/mdk-react-devkit Import the public APIs on this page from [`@tetherto/mdk-react-devkit`](/reference/ui/#tethertomdk-react-devkit). ### `useExplorerSelection` Bridges the Explorer table selection into the shared `devicesStore` that the write-control cards read. Given the active tab and the table's row-selection, it dispatches the matching setters — containers → `selectMultipleContainers`, miners → `setSelectDevice` + `selectDeviceTag`, cabinets → `selectLVCabinet` — and, for containers/miners, fetches the richer detail snapshots (`useContainerSnapshots`) so the controls see the full `last.snap` config (tank / cooling / power-mode) the lean list projection omits, then derives the per-socket selection into the store. Selections are reset whenever the selection or tab changes and on unmount, so a stale selection can never drive the panel. ```typescript ({ deviceType, rows, selected, }: UseExplorerSelectionParams) => UseExplorerSelectionResult ``` ### `useMinerDetail` Reads the miner selection the [`useExplorerSelection`](/reference/ui/hooks/op-centre/#useexplorerselection) bridge writes into `devicesStore` and shapes the head miner for the read-only cards of the miner detail panel — the info rows ([`MinerInfoCard`](/reference/ui/components/widgets/#minerinfocard)) and the per-chip frequency / temperature stats ([`MinerChipsCard`](/reference/ui/components/widgets/#minerchipscard)). The write controls ([`MinerControlsCard`](/reference/ui/components/widgets/#minercontrolscard)) and the aggregate stats ([`StatsGroupCard`](/reference/ui/components/widgets/#statsgroupcard)) read the same store directly. ```typescript () => UseMinerDetailResult ``` # Permission (/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: HeaderPreferences; isLoading: boolean; error: null; handleToggle: (key: keyof HeaderPreferences, value: boolean) => void; handleReset: () => void; } ``` # 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 MOS/the reference app. Fine for containers/cabinets; for very large miner fleets this fetches the cap and filters in the browser (no server paging). Push status into the foundation query + wire `limit`/`offset` if that ceiling is ever hit. ```typescript (options: UseExplorerDataOptions) => UseExplorerDataResult ``` # 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). ### `actionsQuery` `GET /auth/actions` — pending/voting actions list (the review-tray source). Array params serialize comma-separated. ```typescript (client: QueryClient, params: ActionsParams = {}, fetcher: Fetcher = mdkFetch) => { queryKey: readonly ["auth", "actions", ActionsParams]; queryFn: ({ signal }?: QueryFnContext) => Promise; } ``` ### `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. ```typescript (client: QueryClient, fetcher: Fetcher = mdkFetch) => { mutationKey: readonly ["auth", "thing", "comment", "add"]; mutationFn: (body: ThingCommentBody) => Promise; } ``` ### `appendCommaQuery` Append query params to a URL, serializing array values comma-separated (e.g. `{ ids: ['a', 'b'] }` → `?ids=a,b`). Mirrors the `qs` `arrayFormat: 'comma'` convention MiningOS expects, without the extra dependency. `undefined` / `null` and e… ```typescript (url: string, params: Record) => string ``` ### `authQuery` TanStack Query factory for the `/auth` session lookup. Pass into `useQuery(authQuery(client))` to fetch the current session. ```typescript (client: QueryClient, fetcher: Fetcher = defaultFetcher) => { queryKey: readonly ["auth"]; queryFn: ({ signal }?: QueryFnContext) => Promise; } ``` ### `authTokenMutation` TanStack Mutation factory for `POST /auth/token`. Used by `useTokenPolling` to refresh the session token every 250 s. ```typescript (client: QueryClient, fetcher: Fetcher = mdkFetch) => { mutationKey: readonly ["auth", "token"]; mutationFn: (body?: AuthTokenRequest) => Promise; } ``` ### `cancelActionsMutation` `DELETE /auth/actions/:type/cancel?ids=<comma>` — cancel pending actions. ```typescript (client: QueryClient, fetcher: Fetcher = mdkFetch) => { mutationKey: readonly ["auth", "actions", "cancel"]; mutationFn: ({ type, ids }: CancelActionsPayload) => Promise; } ``` ### `containerPoolStatsQuery` `GET /auth/pools/stats/containers` — per-container override counts. ```typescript (client: QueryClient, fetcher: Fetcher = mdkFetch) => { queryKey: readonly ["auth", "pools", "stats", "containers"]; queryFn: ({ signal }?: QueryFnContext) => Promise; } ``` ### `containerSettingsQuery` Convenience wrapper around `globalDataQuery` pinned to `type=containerSettings` — per-model container thresholds/parameters. Verified live: the response is a flat `ContainerSettingsEntry[]`, not the per-Kernel envelope. ```typescript (client: QueryClient, options: { model?: string; overwriteCache?: boolean } = {}, fetcher: Fetcher = mdkFetch) => { queryKey: readonly ["auth", "global", "data", GlobalDataParams]; queryFn: ({ signal… ``` ### `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` for an already-resolved `url`. 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… ```typescript (fetcher: Fetcher, url: string) => ({ signal }?: QueryFnContext) => Promise ``` ### `createMdkQueryClient` Build a TanStack `QueryClient` configured with the resolved API base URL. The base URL is exposed via `client.getDefaultOptions().queries.meta` so callers can read it without importing the `resolveApiBaseUrl` helper. ```typescript (options: CreateMdkQueryClientOptions = {}) => QueryClient ``` ### `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). ```typescript (client: QueryClient, fetcher: Fetcher = mdkFetch) => { mutationKey: readonly ["auth", "thing", "comment", "delete"]; mutationFn: (body: ThingCommentBody) => Promise; } ``` ### `deviceQuery` TanStack Query factory for a single device by id (`/devices/:id`). ```typescript (client: QueryClient, id: string, fetcher: Fetcher = defaultFetcher) => { queryKey: readonly ["devices", string]; queryFn: ({ signal }?: QueryFnContext) => Promise; } ``` ### `devicesQuery` TanStack Query factory for the full `/devices` inventory listing. ```typescript (client: QueryClient, fetcher: Fetcher = defaultFetcher) => { queryKey: readonly ["devices"]; queryFn: ({ signal }?: QueryFnContext) => Promise; } ``` ### `editThingCommentMutation` TanStack Mutation factory for `PUT /auth/thing/comment` — edit an existing device comment (`body.id` identifies it). ```typescript (client: QueryClient, fetcher: Fetcher = mdkFetch) => { mutationKey: readonly ["auth", "thing", "comment", "edit"]; mutationFn: (body: 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… ```typescript (client: QueryClient, params: ExtDataParams, fetcher: Fetcher = mdkFetch) => { 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… ```typescript (client: QueryClient, fetcher: Fetcher = mdkFetch) => { queryKey: readonly ["auth", "featureConfig"]; queryFn: ({ signal }?: QueryFnContext) => Promise; } ``` ### `getApiBaseUrl` Read the configured base URL back from a `QueryClient` produced by `createMdkQueryClient`. Falls back to the default if metadata is absent. ```typescript (client: QueryClient) => string ``` ### `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. ```typescript (client: QueryClient, fetcher: Fetcher = mdkFetch) => { 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. ```typescript (client: QueryClient, params: GlobalDataParams, fetcher: Fetcher = mdkFetch) => { queryKey: readonly ["auth", "global", "data", GlobalDataParams]; queryFn: ({ signal }?: QueryFnContext) => Promise { queryKey: readonly ["auth", "history-log", HistoryLogParams]; queryFn: ({ signal }?: QueryFnContext) => Promise { queryKey: readonly ["auth", "list-racks", ListRacksParams]; queryFn: ({ signal }?: QueryFnContext) => Promise { queryKey: readonly ["auth", "list-things", ListThingsParams]; queryFn: ({ signal }?: QueryFnContext) => Promise<… ``` ### `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 }`. ```typescript (client: QueryClient, queries: ActionTypeQuery[] = [ { type: 'voting', opts: { reverse: true, limit: LIVE_ACTIONS_LIMIT } }, { type: 'ready', opts: { reverse: true, limit: LIVE_ACTIONS_LIMIT } }, { t… ``` ### `mdkFetch` Module-level singleton bearer fetcher reading from the global `authStore`. Used as the default by the mining query factories (`tailLogQuery`, etc.). ```typescript Fetcher ``` ### `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. ```typescript (client: QueryClient, fetcher: Fetcher = mdkFetch) => { queryKey: readonly ["auth", "ext-data", ExtDataParams]; queryFn: ({ signal }?: QueryFnContext) => Promise; } ``` ### `minersQuery` `GET /auth/miners` — miners with their assigned `poolConfig` (Miner Explorer rows). `filter` / `fields` / `sort` are JSON-stringified selectors. Returns the paginated `MinersResponse` envelope. ```typescript (client: QueryClient, params: MinersParams = {}, fetcher: Fetcher = mdkFetch) => { queryKey: readonly ["auth", "miners", MinersParams]; queryFn: ({ signal }?: QueryFnContext) => Promise { queryKey: readonly ["auth", "pdu-layout", PduLayoutParams]; queryFn: ({ signal }?: QueryFnContext) => Promise { queryKey: readonly ["auth", "pools", string, "balance-history", PoolBalanceHistoryParams];… ``` ### `poolConfigForDeviceQuery` `GET /auth/pools/config/:minerId` — pool config + override count for a single device/miner. ```typescript (client: QueryClient, minerId: string, fetcher: Fetcher = mdkFetch) => { 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. ```typescript (client: QueryClient, fetcher: Fetcher = mdkFetch) => { queryKey: readonly ["auth", "pools"]; queryFn: ({ signal }?: QueryFnContext) => Promise; } ``` ### `resolveApiBaseUrl` Resolves the Gateway API base URL using the priority order described in HLD §2.4 / §5: ```typescript (override?: string) => string ``` ### `siteQuery` TanStack Query factory for `GET /auth/site` — the configured site label. ```typescript (client: QueryClient, fetcher: Fetcher = mdkFetch) => { 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… ```typescript (client: QueryClient, fetcher: Fetcher = mdkFetch) => { 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… ```typescript (client: QueryClient, fetcher: Fetcher = mdkFetch) => { mutationKey: readonly ["auth", "actions", "submit"]; 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? }`). ```typescript (client: QueryClient, fetcher: Fetcher = mdkFetch) => { mutationKey: readonly ["auth", "actions", "submit", "batch"]; 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. ```typescript (client: QueryClient, params: TailLogMultiParams, fetcher: Fetcher = mdkFetch) => { queryKey: readonly ["auth", "tail-log", "multi", TailLogMultiParams]; queryFn: ({ signal }?: QueryFnContext) => Pro… ``` ### `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). ```typescript (client: QueryClient, params: TailLogParams, fetcher: Fetcher = mdkFetch) => { queryKey: readonly ["auth", "tail-log", TailLogParams]; queryFn: ({ signal }?: QueryFnContext) => Promise { queryKey: readonly ["telemetry", string]; 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. ```typescript (client: QueryClient, params: ThingConfigParams, fetcher: Fetcher = mdkFetch) => { queryKey: readonly ["auth", "thing-config", ThingConfigParams]; queryFn: ({ signal }?: QueryFnContext) => Promise { queryKey: readonly ["auth", "userinfo"]; queryFn: ({ signal }?: QueryFnContext) => Promise; } ``` ### `voteActionMutation` `PUT /auth/actions/voting/:id/vote` — approve or reject a pending action. ```typescript (client: QueryClient, fetcher: Fetcher = mdkFetch) => { mutationKey: readonly ["auth", "actions", "vote"]; mutationFn: ({ id, approve }: VoteActionPayload) => Promise; } ``` # 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 `miningos-gateway`. {todo: update miningos-gateway} ```typescript type TailLogMultiParams = { keys: string start?: number end?: number offset?: number limit?: number fields?: string aggrFields?: string aggrTimes?: string overwriteCache?: boolean } ``` ### `TailLogParams` Query parameters for `GET /auth/tail-log`. Mirrors the Fastify schema in `miningos-gateway`. `aggrFields` is a JSON-stringified object describing which aggregate columns to include in each row. ```typescript type TailLogParams = { key: string type?: string tag?: string start?: number end?: number offset?: number limit?: number fields?: string aggrFields?: string aggrTimes?: string merg… ``` ### `ThingCommentBody` Body for `POST | PUT | DELETE /auth/thing/comment` (add / edit / delete a device comment — one Fastify schema covers all three verbs). `rackId` + `thingId` + `comment` are required; `id` and `ts` identify an existing comment for edit/delet… ```typescript type ThingCommentBody = { rackId: string thingId: string comment: string /** Existing comment id — required when editing or deleting. */ id?: string pos?: string ts?: number } ``` ### `ThingConfigParams` Query parameters for `GET /auth/thing-config` — both fields are required by the Fastify schema. ```typescript type ThingConfigParams = { type: string requestType: string } ``` ### `VoteActionPayload` Body for `PUT /auth/actions/voting/:id/vote`. ```typescript type VoteActionPayload = { id: string | number approve: boolean } ``` ### `VotingActionObjectParam` Object-shaped `params[]` entry on a voting submission. Pool create/update carry `{ type: 'pool', data }` (+ `id` for updates); assign-pool carries `{ poolConfigId, configType: 'pool' }`; `switchSocket` carries `{ pdu, socket, enabled }`; `… ```typescript type VotingActionObjectParam = { type?: string id?: string poolConfigId?: string configType?: string data?: Record [key: string]: unknown } ``` ### `VotingActionParam` A single `params[]` entry on a voting submission. Positional and action-specific: device actions carry primitives (`setPowerMode` sends `['sleep']`, `setLED` sends `[true]`, `setTankEnabled` sends `[3, true]`), pool/thing actions carry {@l… ```typescript type VotingActionParam = string | number | boolean | VotingActionObjectParam ``` ### `VotingActionPayload` Body for `POST /auth/actions/:type` (default `voting`). `type` selects the URL segment and is stripped from the JSON body before posting — the remaining fields (`query`, `action`, `params`, `rackType`, …) form the request body. ```typescript type VotingActionPayload = { /** URL segment under `/auth/actions/:type`. Defaults to `voting`. */ type?: string query?: Record action?: string params?: VotingActionPara… ``` # 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 = { label?: string loading?: boolean disabled?: boolean className?: string variant?: TActionButtonVariant confirmation: ActionButtonConfirmation /** Confirmation mode: popover (inline) or dialog (modal). Default: popover */ mode?: 'popover'… ``` ### `ActualEbitdaCardProps` ```typescript type ActualEbitdaCardProps = { value: number } ``` ### `AddSparePartModalProps` Props for `AddSparePartModal`; option lists and handlers are supplied by the caller. ```typescript type AddSparePartModalProps = { isOpen: boolean; onClose: VoidFunction; partTypes: SparePartSubTypesModalPartType[]; defaultPartTypeId?: string; modelOptions: FormSelectOption[]; isModelOptionsLoading?: boolean; minerModelOptions: FormSelectOption[]; statusOptions: For… ``` ### `AddUserModalProps` ```typescript type AddUserModalProps = { open: boolean onClose: VoidFunction roles: RoleOption[] onSubmit: (data: { name: string; email: string; role: string }) => Promise isSubmitting?: boolean } ``` ### `AlarmsBellButtonProps` ```typescript type AlarmsBellButtonProps = { /** Severity-bucketed alarm counts rendered in the stacked badge. */ counts?: AlarmsBellButtonCounts /** Click handler — typically opens an alerts panel or routes to /alerts. */ onClick?: (event: MouseEvent) => void /*… ``` ### `AlertConfirmationModalProps` ```typescript type AlertConfirmationModalProps = { isOpen: boolean onOk: VoidFunction } ``` ### `AlertsProps` ```typescript type AlertsProps = { /** * Devices payload powering the "Current Alerts" table. * Mirrors the API response shape of `useGetListThingsQuery({ ... alerts query })`. */ devices?: Device[][] /** * Loading flag for the "Current Alerts" table. */ isCurrentAlertsLo… ``` ### `AlertsTableTitleProps` ```typescript type AlertsTableTitleProps = { title: ReactNode subtitle?: ReactNode className?: string } ``` ### `AppHeaderProps` ```typescript type AppHeaderProps = { /** Left-most slot — typically the app's brand lockup / logo. */ logo?: ReactNode /** Left-edge content — e.g. a sidebar collapse toggle button. */ start?: ReactNode /** Middle slot — typically the dashboard's stats strip. */ children?:… ``` ### `AreaChartProps` ```typescript type AreaChartProps = { /** Chart data - required, provided by parent */ data: ChartJS<'line'>['data'] /** Chart.js options - merged with defaults */ options?: ChartJS<'line'>['options'] /** Custom HTML tooltip configuration. When provided, replaces the default… ``` ### `ArrowIconProps` ```typescript type ArrowIconProps = { isOpen?: boolean } & IconProps ``` ### `AssignPoolModalProps` ```typescript type AssignPoolModalProps = { isOpen: boolean onClose: () => void onSubmit: (values: { pool: PoolSummary }) => Promise miners: Device[] poolConfig: PoolConfigData[] } ``` ### `AverageDowntimeChartProps` ```typescript type AverageDowntimeChartProps = Partial<{ title: string unit: string height: number barWidth: number className: string isLoading: boolean emptyMessage: string data: AverageDowntimeChartData /** Formats Y-axis ticks, tooltips, and bar data labels (values are 0–1 rates). *… ``` ### `AvgAllInCostChartProps` ```typescript type AvgAllInCostChartProps = { data?: ReadonlyArray dateRange: FinancialDateRange | null isLoading?: boolean } ``` ### `BadgeProps` ```typescript type BadgeProps = { /** * Badge content (wraps children with badge) */ children?: ReactNode /** * Number to display in badge * If > overflowCount, will show "overflowCount+" */ count?: number /** * Maximum count to display * @default 99 */ overflowCount?: n… ``` ### `BarChartProps` ```typescript type BarChartProps = { /** Chart data - required, provided by parent. Use `as any` for mixed bar+line datasets. */ data: any /** Chart.js options - merged with defaults */ options?: ChartJS<'bar'>['options'] /** Stack bars on top of each other */ isStacked?: b… ``` ### `BatchMoveSparePartsModalProps` Props for `BatchMoveSparePartsModal`; unselected location/status arrive as `null` on submit. ```typescript type BatchMoveSparePartsModalProps = { isOpen: boolean; onClose: VoidFunction; spareParts: BatchMoveSparePart[]; locationOptions: FormSelectOption[]; statusOptions: FormSelectOption[]; onSubmit: (values: { location: string | null; status: string | null; observation: string |… ``` ### `BitcoinPriceCardProps` ```typescript type BitcoinPriceCardProps = { value: number } ``` ### `BitcoinProducedCardProps` ```typescript type BitcoinProducedCardProps = { value: number } ``` ### `BitcoinProducedChartProps` ```typescript type BitcoinProducedChartProps = { chartData: BarChartDataResult isLoading?: boolean hasAllZeros?: boolean height?: number } ``` ### `BitcoinProductionCostCardProps` ```typescript type BitcoinProductionCostCardProps = { value: number } ``` ### `BitMainControlsTabProps` ```typescript type BitMainControlsTabProps = { /** Device data */ data: Device } ``` ### `BitMainHydroSettingsProps` ```typescript type BitMainHydroSettingsProps = { /** Device data */ data?: Device } ``` ### `BitMainImmersionSummaryBoxProps` ```typescript type BitMainImmersionSummaryBoxProps = { data?: Device containerSettings?: BitMainImmersionSummaryBoxContainerSettings | null } ``` ### `BreadcrumbsProps` ```typescript type BreadcrumbsProps = { items: BreadcrumbItem[] showBack?: boolean backLabel?: string className?: string itemClassName?: string backClassName?: string onBackClick?: VoidFunction separator?: ReactNode } ``` ### `BtcAveragePriceProps` ```typescript type BtcAveragePriceProps = Partial<{ /** * BTC price in USD; formatted with grouping and no decimal places. * When `null`, `undefined`, non-finite, or negative, the value shows `-` (`FALLBACK` from format utils). */ price: number | null /** * Label for the BTC avera… ``` ### `BulkAddSparePartsModalProps` Props for `BulkAddSparePartsModal`; `onSubmit` receives the parsed CSV records. ```typescript type BulkAddSparePartsModalProps = { isOpen: boolean; onClose: VoidFunction; onSubmit: (records: CSVRecord[]) => Promise<{ error?: string } | void>; isLoading?: boolean; } ``` ### `ButtonProps` Props for `Button`. Extends all native `<button>` attributes. ```typescript type ButtonProps = Partial< { /** Show a spinner instead of the content and disable the button. */ loading: boolean /** Make the button stretch to fill its container. */ fullWidth: boolean /** Icon node rendered alongside `children`. */ icon: ReactNode /** V… ``` ### `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 = { open: boolean title: string onConfirm: VoidFunction onClose: VoidFunction children: ReactNode confirmText?: string destructive?: boolean } ``` ### `ChartContainerProps` ```typescript type ChartContainerProps = { title?: string /** * Optional node rendered immediately after the title text (e.g. an info * tooltip). Only shown when `title` is set and `header` is not. Additive - * omit it and the title renders exactly as before. */ titleExtra?: Reac… ``` ### `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) */ statsPerColumn: number /** Secondary… ``` ### `CheckboxProps` ```typescript type CheckboxProps = { /** * Size variant of the checkbox * @default 'md' */ size?: CheckboxSize /** * Color variant when checked * @default 'primary' */ color?: ComponentColor /** * Border radius variant * @default 'none' */ radius?: BorderRadius /** * Custom… ``` ### `ConfirmDeleteSparePartModalProps` Props for `ConfirmDeleteSparePartModal`. ```typescript type ConfirmDeleteSparePartModalProps = { isOpen?: boolean; onClose?: VoidFunction; onConfirm?: (sparePart: ConfirmDeleteSparePartModalSparePart) => Promise | void; sparePart?: ConfirmDeleteSparePartModalSparePart; isLoading?: boolean; } ``` ### `ContainerChartsProps` ```typescript type ContainerChartsProps = { /** When false, shows an empty state (feature gate). @default true */ featureEnabled?: boolean /** Message when `featureEnabled` is false */ disabledMessage?: string /** Options for the combination selector */ combinations: ContainerChar… ``` ### `ContainerControlsBoxProps` ```typescript type ContainerControlsBoxProps = { data?: Device isBatch?: boolean isCompact?: boolean // --- data from outside (no API calls inside) --- selectedDevices?: Device[] pendingSubmissions?: PendingSubmission[] alarmsDataItems?: TimelineItemData[] tailLogData?: UnknownRecord[]… ``` ### `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. */ isLoading?: boolean /** Error message… ``` ### `CostChartsProps` ```typescript type CostChartsProps = { costLog: ReadonlyArray btcPriceLog: ReadonlyArray totals: CostSummaryMonetaryTotals | null dateRange: FinancialDateRange | null avgAllInCostData?: ReadonlyArray isLoadi… ``` ### `CostContentProps` ```typescript type CostContentProps = CostViewModelProps & CostQueryStateProps & { dateRange: FinancialDateRange | null /** Optional revenue/cost time-series for the Avg All-in Cost panel. */ avgAllInCostData?: ReadonlyArray } ``` ### `CostMetricsProps` ```typescript type CostMetricsProps = { metrics: CostSummaryDisplayMetrics } ``` ### `CostProps` ```typescript type CostProps = CostContentProps & CostChromeProps ``` ### `CurrentAlertsProps` ```typescript type CurrentAlertsProps = { /** * Raw devices (with last.alerts) used to derive current alerts from. * Shape mirrors the API response from the source app. */ devices?: Device[][] isLoading?: boolean /** * Filters controlled outside (typically by URL severity param)… ``` ### `DashboardDateRangePickerProps` ```typescript type DashboardDateRangePickerProps = { /** Current range as `{ start, end }` epoch-millisecond timestamps. */ value: DashboardDateRange /** Fires with the next `{ start, end }` window when the user applies a range. */ onChange: (next: DashboardDateRange) => void /** Display f… ``` ### `DataLabelProps` ```typescript type DataLabelProps = Partial<{ /** * Range start; formatted in the active timezone (`dd/MM/yy`). */ startDate: Date | null /** * Range end; formatted in the active timezone (`dd/MM/yy`). */ endDate: Date | null /** * Label text; defaults to `PERIOD`. */ label:… ``` ### `DataTableProps` ```typescript type DataTableProps = { /** * The data to be shown in the table. See https://tanstack.com/table/v8/docs/guide/data */ data: I[] /** * The column configuration table. See https://tanstack.com/table/v8/docs/guide/column-defs */ columns: DataTableColumnDef… ``` ### `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 = { deviceType: DeviceExplorerDeviceType className?: string selectedDevices?: DataTableRowSelectionState onSelectedDevicesChange?: (selections: DataTableRowSelectionState) => void } & ForwardedToolbarProps & ForwardedTableProps ``` ### `DialogContentProps` ```typescript type DialogContentProps = { title?: string description?: string closeOnClickOutside?: boolean closeOnEscape?: boolean } & DialogHeaderProps ``` ### `DialogHeaderProps` ```typescript type DialogHeaderProps = { bare?: boolean closable?: boolean onClose?: VoidFunction } ``` ### `DividerProps` ```typescript type DividerProps = { /** Line orientation */ orientation?: DividerOrientation /** Line style */ dashed?: boolean dotted?: boolean /** Text or node rendered in the middle of the divider */ children?: ReactNode /** Horizontal alignment of the label */ align?:… ``` ### `DoughnutChartProps` ```typescript type DoughnutChartProps = { /** Array of labelled slices */ data: DoughnutChartDataset[] /** Unit suffix shown in tooltips */ unit?: string /** Chart.js options – merged with defaults */ options?: ChartJS<'doughnut'>['options'] /** Doughnut cutout percentage (defau… ``` ### `EbitdaChartsProps` ```typescript type EbitdaChartsProps = { showEbitdaBarChart: boolean ebitdaChartData: BarChartDataResult btcDisplayData: BarChartDataResult isLoading: boolean hasBtcProducedAllZeros: boolean } ``` ### `EbitdaHodlCardProps` ```typescript type EbitdaHodlCardProps = { value: number currentBTCPrice: number } ``` ### `EbitdaMetricsProps` ```typescript type EbitdaMetricsProps = { metrics: EbitdaDisplayMetrics currentBTCPrice: number } ``` ### `EbitdaProps` ```typescript type EbitdaProps = { metrics: EbitdaDisplayMetrics | null ebitdaChartInput: ToBarChartDataInput | null btcProducedChartInput: ToBarChartDataInput | null hasBtcProducedAllZeros: boolean showEbitdaBarChart: boolean currentBTCPrice: number datePicker: ReactElem… ``` ### `EbitdaSellingCardProps` ```typescript type EbitdaSellingCardProps = { value: number } ``` ### `EfficiencyMinerTypeViewProps` ```typescript type EfficiencyMinerTypeViewProps = Omit ``` ### `EfficiencyMinerUnitViewProps` ```typescript type EfficiencyMinerUnitViewProps = Omit ``` ### `EfficiencySiteViewProps` ```typescript type EfficiencySiteViewProps = { log?: MetricsEfficiencyLogEntry[] avgEfficiency?: number | null nominalValue?: number | null isLoading?: boolean dateRange?: EfficiencyDateRange onDateRangeChange?: (range: EfficiencyDateRange) => void onReset?: VoidFunction } ``` ### `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 = { value: unknown tankNumber: number | string isButtonDisabled: boolean isOffline: boolean onToggle: (params: EnabledDisableToggleCbParams) => void } ``` ### `EnergyBalanceCostChartsProps` ```typescript type EnergyBalanceCostChartsProps = { costChartData: BarChartDataResult btcUnit: EnergyCostChartInput['btcUnit'] powerChartInput: ThresholdLineChartInput displayMode: DisplayMode barLabelFormatter: (v: number) => string onDisplayModeChange: (mode: DisplayMode) => void /** Sh… ``` ### `EnergyBalanceCostMetricsProps` ```typescript type EnergyBalanceCostMetricsProps = { metrics: EnergyCostMetrics } ``` ### `EnergyBalancePowerChartProps` ```typescript type EnergyBalancePowerChartProps = { height?: number fillHeight?: boolean periodType: PeriodType chartInput: ThresholdLineChartInput } ``` ### `EnergyBalanceProps` ```typescript type EnergyBalanceProps = { viewModel: EnergyBalanceViewModel onTabChange: (tab: EnergyBalanceTab) => void onRevenueDisplayModeChange: (mode: DisplayMode) => void onCostDisplayModeChange: (mode: DisplayMode) => void isDemoMode?: boolean /** Slot for timeframe / dat… ``` ### `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 = { name: string value: number unit: string fallback?: string } ``` ### `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`. */ onNavigate?: (path: string) => void /** Compact layout… ``` ### `ExplorerLayoutProps` ```typescript type ExplorerLayoutProps = { /** Page heading. */ title?: string /** Optional header controls (export button, etc.) shown next to the title. */ headerActions?: ReactNode /** The list column — typically a tab switch plus the device/container table. */ list: ReactNode… ``` ### `ExportButtonProps` ```typescript type ExportButtonProps = { /** Fires with the chosen format when the user picks an item. */ onExport: (format: ExportFormat) => void /** Formats to offer in the dropdown — defaults to `['csv', 'json']`. */ formats?: readonly ExportFormat[] /** Button label — defau… ``` ### `FeatureFlagsSettingsProps` ```typescript type FeatureFlagsSettingsProps = { featureFlags: Record isEditingEnabled: boolean isLoading?: boolean isSaving?: boolean onSave: (flags: Record) => void className?: string } ``` ### `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. */ colors?: string[] /** Arc thickness as a fraction of the gauge radius (0–1). */ arcWidth?: number /**… ``` ### `HashBalanceCostPanelProps` ```typescript type HashBalanceCostPanelProps = HashBalancePanelProps ``` ### `HashBalanceProps` ```typescript type HashBalanceProps = Partial<{ isError: boolean isLoading: boolean errorMessage: string className: string tabsClassName: string tabsListClassName: string data: HashRevenueResponse | null initialDateRange: FinancialDateRange onDateRangeChange: (dateRange: Finan… ``` ### `HashBalanceRevenuePanelProps` ```typescript type HashBalanceRevenuePanelProps = HashBalancePanelProps & { currency: HashBalanceCurrency onCurrencyChange: (currency: HashBalanceCurrency) => void } ``` ### `HashrateMinerTypeViewProps` ```typescript type HashrateMinerTypeViewProps = { /** Hashrate log grouped by miner type (groupBy=miner). */ log?: HashrateGroupedLog isLoading?: boolean dateRange?: HashrateDateRange onDateRangeChange?: (range: HashrateDateRange) => void onReset?: VoidFunction } ``` ### `HashrateMiningUnitViewProps` ```typescript type HashrateMiningUnitViewProps = { /** Hashrate log grouped by container / mining unit (groupBy=container). */ log?: HashrateGroupedLog isLoading?: boolean dateRange?: HashrateDateRange onDateRangeChange?: (range: HashrateDateRange) => void onReset?: VoidFunction } ``` ### `HashrateProps` ```typescript type HashrateProps = { /** Tab selected on first render. Defaults to the Site View. */ defaultTab?: HashrateTabValue /** Props forwarded to the Site View tab. */ siteView?: HashrateSiteViewProps /** Props forwarded to the Miner Type View tab. */ minerTypeView?… ``` ### `HashrateSiteViewProps` ```typescript type HashrateSiteViewProps = { /** Hashrate log grouped by miner type. */ log?: HashrateGroupedLog /** Loading state - drives the chart spinner. */ isLoading?: boolean /** Selected date range used by the host to drive the query. */ dateRange?: HashrateDateRange /** Fi… ``` ### `HeaderConsumptionBoxProps` ```typescript type HeaderConsumptionBoxProps = { icon?: ReactNode /** Current site-level power consumption, in megawatts. */ valueMw?: number /** Unit label — defaults to `MW`. */ unit?: string className?: string } ``` ### `HeaderControlsSettingsProps` ```typescript type HeaderControlsSettingsProps = { preferences: HeaderPreferences isLoading?: boolean onToggle: (key: keyof HeaderPreferences, value: boolean) => void onReset: VoidFunction className?: string } ``` ### `HeaderEfficiencyBoxProps` ```typescript type HeaderEfficiencyBoxProps = { icon?: ReactNode /** Efficiency in watts per TH/s. */ valueWthS?: number /** Unit label — defaults to `W/TH/S`. */ unit?: string className?: string } ``` ### `HeaderHashrateBoxProps` ```typescript type HeaderHashrateBoxProps = { icon?: ReactNode /** 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`. */ unit?: string /** Decimal places shown for both v… ``` ### `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; auto-derived from the finite values when omitted. */ min?: number /** Range ceiling; auto-derived from the finite values when omitted. */ max?:… ``` ### `HistoricalAlertsProps` ```typescript type HistoricalAlertsProps = { /** * Pre-fetched historical alerts log entries (each with a `thing` device payload). */ alerts?: Alert[] isLoading?: boolean /** * Filters and search tags coming from the parent (typically shared with `CurrentAlerts`). */ localFilters:… ``` ### `ImportExportSettingsProps` ```typescript type ImportExportSettingsProps = { onExport: VoidFunction onImport: (data: SettingsExportData) => void onParseFile?: (file: File) => Promise isExporting?: boolean isImporting?: boolean className?: string } ``` ### `IndicatorProps` ```typescript type IndicatorProps = { /** * Color variant of the indicator * @default 'gray' */ color?: IndicatorColor /** * Size variant of the indicator * @default 'md' */ size?: ComponentSize /** * Custom className for the root element */ className?: string /** * When tru… ``` ### `InputProps` ```typescript type InputProps = Omit, 'prefix' | 'size'> & { /** * Optional label displayed above the input */ label?: string /** * HTML id for the input. Required when using label for accessibility. */ id?: string /** * Variant of the input * - `… ``` ### `LabeledCardProps` ```typescript type LabeledCardProps = Partial<{ isDark: boolean className: string hasNoWrap: boolean isRelative: boolean isFullWidth: boolean hasNoMargin: boolean hasNoBorder: boolean isFullHeight: boolean isScrollable: boolean label: ReactNode children: ReactNode getNavigateO… ``` ### `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 = { type: string status: string } ``` ### `LogItemProps` ```typescript type LogItemProps = { data: LogData onLogClicked?: (uuid: string) => void } ``` ### `LogRowProps` ```typescript type LogRowProps = { log: LogData type: string style?: CSSProperties onLogClicked?: (uuid: string) => void } ``` ### `LogsCardProps` ```typescript type LogsCardProps = Partial<{ type: string label: string isDark: boolean isLoading: boolean logsData: LogData[] emptyMessage: string skeletonRows: number pagination: LogPagination onLogClicked: (uuid: string) => void }> ``` ### `ManageUserModalProps` ```typescript type ManageUserModalProps = { open: boolean onClose: VoidFunction user: SettingsUser roles: RoleOption[] rolePermissions: Record> permissionLabels: Record onSubmit: (data: { id: string; name: string; email: string; ro… ``` ### `MdkWordmarkProps` ```typescript type MdkWordmarkProps = { /** Visual size of the wordmark. `sm` ≈ 24px tall, `md` ≈ 32px, `lg` ≈ 64px. */ size?: MdkWordmarkSize /** Optional class hook on the outer ``. */ className?: string /** Accessible label. Defaults to "MDK". */ title?: string } ``` ### `MicroBTWidgetBoxProps` ```typescript type MicroBTWidgetBoxProps = { 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`. */ label: string /** Hide the title row entirely. */ hideHeader: boolean /** Loading state — renders skeleton rows. */ isLoading: boolean /** Number of skeleton rows to sh… ``` ### `MinMaxAvgProps` ```typescript type MinMaxAvgProps = MinMaxAvgValues & { 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<{ isOpen: boolean onClose: () => void movement: MovementData }> ``` ### `MoveSparePartModalProps` Props for `MoveSparePartModal`. ```typescript type MoveSparePartModalProps = { isOpen?: boolean; onClose?: VoidFunction; sparePart?: MoveSparePartModalSparePart; requestedValues?: { location?: string; status?: string }; locationOptions: FormSelectOption[]; statusOptions: FormSelectOption[]; onSubmit: ( values: { lo… ``` ### `MultiSelectProps` ```typescript type MultiSelectProps = { options: MultiSelectOption[] /** Controlled selected values. Omit to use `defaultValue` for uncontrolled mode. */ value?: string[] /** Initial values for uncontrolled mode. Ignored when `value` is provided. */ defaultValue?: string[] onV… ``` ### `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<{ hashrate: OperationalDashboardTrendInput consumption: OperationalDashboardTrendInput efficiency: OperationalDashboardTrendInput miners: OperationalDashboardMinersInput /** Optional controls (e.g. a date-range picker) rendered abo… ``` ### `OperationalMinersStatusChartProps` Props for the miners-status chart component. ```typescript type OperationalMinersStatusChartProps = Partial<{ data: MinersStatusChartData isLoading: boolean isExpanded: boolean onToggleExpand: VoidFunction }> ``` ### `OperationsEfficiencyProps` ```typescript type OperationsEfficiencyProps = { defaultTab?: EfficiencyTabValue siteView?: EfficiencySiteViewProps minerTypeView?: EfficiencyMinerTypeViewProps minerUnitView?: EfficiencyMinerUnitViewProps } ``` ### `OperationsEnergyChartProps` ```typescript type OperationsEnergyChartProps = { totals: CostSummaryMonetaryTotals | null isLoading?: boolean } ``` ### `OperationsEnergyCostChartProps` ```typescript type OperationsEnergyCostChartProps = Partial<{ title: string unit: string height: number className: string isLoading: boolean emptyMessage: string data: OperationsEnergyCostChartData }> ``` ### `PaginationProps` ```typescript type PaginationProps = { /** * Current active page number */ current?: number /** * Total number of items */ total?: number /** * Number of items per page * @default 20 */ pageSize?: number /** * Page size options for the select dropdown * @default [10, 20, 50,… ``` ### `PendingActionsButtonProps` ```typescript type PendingActionsButtonProps = { /** Click handler override — defaults to toggling the actionsStore sidebar. */ onClick?: (event: MouseEvent) => void className?: string } ``` ### `PoolDetailsCardProps` ```typescript type PoolDetailsCardProps = PoolDetailsCardPartialProps & { 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 while… ``` ### `PowerModeTimelineChartProps` Props for `PowerModeTimelineChart`. ```typescript type PowerModeTimelineChartProps = Partial<{ /** Initial power-mode entries (each with start/end ts + mode). */ data: PowerModeTimelineEntry[] /** Streaming updates appended to the initial data. */ dataUpdates: PowerModeTimelineEntry[] /** Show a loading skeleton instead of… ``` ### `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. Defaults to a single "Sign out" item. */ items: ProfileMenuItem[] /** Optional user label rendered at the top of the dropdown (e.g. an email). */ user?: ReactNode /** Override the trigge… ``` ### `RadioGroupProps` ```typescript type RadioGroupProps = { /** * Layout orientation * @default 'vertical' */ orientation?: 'horizontal' | 'vertical' /** * Remove gap between radio items * @default false */ noGap?: boolean /** * Custom className for the group */ className?: string } & ComponentPr… ``` ### `RadioProps` ```typescript type RadioProps = { /** * Size variant of the radio * @default 'md' */ size?: ComponentSize /** * Color variant when checked * @default 'default' */ color?: ComponentColor /** * Border radius variant (full makes it circular) * @default 'full' */ radius?: Bo… ``` ### `RBACControlSettingsProps` ```typescript type RBACControlSettingsProps = { users: SettingsUser[] roles: RoleOption[] rolePermissions: Record> permissionLabels: Record canWrite: boolean isLoading?: boolean onCreateUser: (data: { name: string; email: string; role:… ``` ### `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<{ data: RevenueDataItem[] isLoading: boolean siteList: (string | SiteItem)[] legendPosition: Position legendAlign: 'start' | 'center' | 'end' }> ``` ### `SelectProps` ```typescript type SelectProps = ComponentPropsWithoutRef & { /** * Show a clear button when a value is selected * @default false */ allowClear?: boolean } ``` ### `SettingsDashboardProps` ```typescript type SettingsDashboardProps = { dangerActions?: ActionButtonProps[] headerControlsProps?: HeaderControlsSettingsProps rbacControlProps?: RBACControlSettingsProps importExportProps?: ImportExportSettingsProps featureFlagsProps?: FeatureFlagsSettingsProps showFeatureFlag… ``` ### `SidebarProps` ```typescript type SidebarProps = SidebarOptions & SidebarCallbacks & { items: SidebarMenuItem[] } ``` ### `SignInGoogleButtonProps` ```typescript type SignInGoogleButtonProps = Omit & { /** * Base URL of the OAuth backend (no trailing slash). Click navigates to * `${oauthBaseUrl}/oauth/google`. */ oauthBaseUrl: string /** Override the visible button label. */ label?: string /*… ``` ### `SiteStatsBarProps` ```typescript type SiteStatsBarProps = { /** Site label rendered in the header row. */ title: string /** Current site-level power consumption, in watts (or whatever `powerUnit` says). */ power?: number /** Display unit for `power` — defaults to `kW`. */ powerUnit?: string /** A… ``` ### `SkeletonBlockProps` ```typescript type SkeletonBlockProps = Partial<{ circle: boolean className: string width: number | string height: number | string borderRadius: number | string }> ``` ### `SocketProps` ```typescript type SocketProps = { /** Current in amperes */ current_a?: number | null /** Power in watts */ power_w?: number | null /** Whether socket is enabled */ enabled?: boolean /** Socket number/index */ socket?: number | null /** Whether socket is selected */ sele… ``` ### `SparePartSubTypesModalProps` Props for `SparePartSubTypesModal`; the active part type is controlled by the caller. ```typescript type SparePartSubTypesModalProps = { isOpen: boolean; onClose: VoidFunction; partTypes: SparePartSubTypesModalPartType[]; activePartTypeId: string; onPartTypeChange: (id: string) => void; subTypes: string[]; onAddSubType: (name: string) => Promise<{ error?: string } | void>… ``` ### `SpinnerProps` ```typescript type SpinnerProps = { /** * Size variant of the spinner * @default 'md' */ size?: ComponentSize /** * Color variant of the spinner * @default 'primary' */ color?: 'primary' | 'secondary' /** * Whether to display in fullscreen mode * @default false */ fullScre… ``` ### `SupplyLiquidBoxProps` ```typescript type SupplyLiquidBoxProps = { data?: Device containerSettings?: SupplyLiquidBoxContainerSettings | null } ``` ### `SwitchProps` ```typescript type SwitchProps = { /** * Size variant of the switch * @default 'md' */ size?: ComponentSize /** * Color variant when checked * @default 'default' */ color?: ComponentColor /** * Border radius variant * @default 'none' */ radius?: BorderRadius /** * Custom… ``` ### `TagFilterBarProps` ```typescript type TagFilterBarProps = { filterTags: string[] localFilters: AlertLocalFilters onSearchTagsChange: (tags: string[]) => void onLocalFiltersChange: (filters: AlertLocalFilters) => void /** * Site-specific overrides for the "type" filter children. * If provided, the… ``` ### `TagInputProps` ```typescript type TagInputProps = { /** * Controlled tags (array of tag values) */ value?: string[] /** * Callback when tags change (add/remove) */ onTagsChange?: (tags: string[]) => void /** * Callback when input value changes (typing). Receives current input value. Usefu… ``` ### `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 = { label: string temperature: number unit: string oilPumpEnabled: boolean waterPumpEnabled: boolean color: string flash?: boolean tooltip?: string pressure: TankRowPressure } ``` ### `TanksBoxProps` ```typescript type TanksBoxProps = { 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. */ id?: string /** * Validation error message. When provided, d… ``` ### `ThresholdLineChartProps` ```typescript type ThresholdLineChartProps = Partial<{ title: string unit: string height: number /** When true, uses a taller default height (360px). */ isTall: boolean className: string emptyMessage: string isLegendVisible: boolean data: ThresholdLineChartData yTicksFormatter: (valu… ``` ### `TimeframeControlsProps` ```typescript type TimeframeControlsProps = Partial<{ hint: string onReset: VoidFunction showResetButton: boolean isWeekSelectVisible: boolean isMonthSelectVisible: boolean layout: 'horizontal' | 'stacked' dateRange: TimeframeControlsDateRange timeframeType: TimeframeTypeValue | nul… ``` ### `TimeframeWeekFlatContentProps` ```typescript type TimeframeWeekFlatContentProps = { visibleWeeks: ReturnType } ``` ### `TimeframeWeekTreeContentProps` ```typescript type TimeframeWeekTreeContentProps = { timezone: string selectedYear: number selectedMonth: number } ``` ### `TimelineChartProps` ```typescript type TimelineChartProps = { initialData: TimelineChartData newData?: TimelineChartData skipUpdates?: boolean range?: ChartRange axisTitleText?: AxisTitleText isLoading?: boolean title?: string height?: number } ``` ### `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 = { /** * Typography variant * @default 'body' */ variant?: 'heading1' | 'heading2' | 'heading3' | 'body' | 'secondary' | 'caption' /** * Text size * @default undefined (uses variant default) */ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl… ``` ### `WidgetTopRowProps` ```typescript type WidgetTopRowProps = { title: string power?: number unit?: string statsErrorMessage?: string | ErrorWithTimestamp[] | null alarms?: AlarmsMap className?: string } ``` # 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 **seconds** (lightweight-charts convention); `y` is `null` to render gaps. ```typescript type ChartDataPoint = { x: number y: number | null } ``` ### `ChartDataset` A named line series with colour and points. Optional `visible` lets pages pre-hide datasets without removing them from the data array. ```typescript type ChartDataset = { label?: string borderColor: string data: ChartDataPoint[] visible?: boolean } ``` # Foundation general (/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 Mining OS's `breakTimeIntoIntervals`. **Function** ```typescript (start: number, end: number, intervalMs: number = ONE_DAY_MS) => TimeInterval[] ``` ### `buildCurrentAlertDevicesParams` `list-things` params for the current-alerts table: every device that currently carries one or more alerts, with the fields the `<CurrentAlerts>` table reads. Consumed by `useCurrentAlertDevices`. **Function** ```typescript (filterTags: string[] = []) => ListThingsParams ``` ### `buildHistoricalAlertsParams` `history-log` params for a single alerts window. The chunked fetch (`useHistoricalAlerts`) calls this once per 24h sub-window. **Function** ```typescript (range: HistoricalAlertsRange) => HistoryLogParams ``` ### `fetchHistoricalAlertsInChunks` Fetch a historical-alerts range as successive 24h windows, merging the results by `uuid`. `fetchWindow` is called once per window (oldest → newest); individual window failures are swallowed (matches Mining OS) so one bad window doesn't dro… **Function** ```typescript (range: { start: number; end: number }, fetchWindow: (window: TimeInterval) => Promise, options: FetchHistoricalAlertsOptions = {}) => Promise ``` ### `getAlertsForDevices` Flatten an array of devices into a list of incident rows, one per alert. Devices without `last.alerts` are skipped. The output is **not yet sorted**; pair with `sortIncidentsBySeverity` for the final list-view order. **Function** ```typescript (devices: ListThingsDevice[], formatDate: (d: Date) => string = (d) => d.toISOString()) => IncidentRow[] ``` ### `getDefaultHistoricalAlertsRange` Default historical-alerts range: the last `DEFAULT_HISTORICAL_WINDOW_MS` ending now. Used by the devkit `<Alerts>` feature and the shell Alerts page to seed their range state. **Function** ```typescript (now: number = Date.now()) => HistoricalAlertsRange ``` ### `mapDevicesToIncidents` One-shot helper: `devices → sorted rows`. Used by the `useActiveIncidents` hook's `select` projection. **Function** ```typescript (devices: ListThingsDevice[], formatDate?: (d: Date) => string) => IncidentRow[] ``` ### `mapHistoryLogToAlerts` Normalise raw `history-log` alert rows into the shape the devkit `<HistoricalAlerts>` table consumes. The table derives its device label, short code, and filter tokens from each row's `thing` (treated as a device), so this guarantees `thin… **Function** ```typescript (rows: HistoricalAlert[] = []) => HistoricalAlert[] ``` ### `mergeAlertsByUuid` Concatenate `next` onto `prev`, replacing any row that shares a `uuid` (later windows win) and appending the rest. Rows without a `uuid` are always appended. Mirrors Mining OS's `updateHistoricalData`. **Function** ```typescript (prev: T[], next: T[]) => T[] ``` ### `sortIncidentsBySeverity` Sort rows by severity (critical → high → medium), then by `id` for deterministic ordering when severities tie. Returns a new array. **Function** ```typescript (rows: IncidentRow[]) => IncidentRow[] ``` # Auth (/reference/ui/utilities/auth) Helper functions for authentication flows. ## Package `@tetherto/mdk-ui-foundation` ## Utilities @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `extractAuthTokenFromUrl` Extract `?authToken=` from a URL search string. Accepts either a full URL, a query string with leading `?`, or a bare query string. **Function** ```typescript (search: string) => string | null ``` ### `stripAuthTokenFromUrl` Build a URL string with the `?authToken=` parameter stripped. Used after the OAuth callback to remove the token from the address bar without losing any other query state. **Function** ```typescript (search: string) => string ``` # Dashboard (/reference/ui/utilities/dashboard) Utilities for dashboard data processing. ## Package `@tetherto/mdk-ui-foundation` ## Utilities @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `buildHashrateTailLogParams` Hashrate tail-log params — per-miner 1-minute aggregate, summed across the `t-miner` tag. **Function** ```typescript (range: DashboardQueryRange) => TailLogParams ``` ### `buildMinerpoolStatsHistoryExtDataParams` Ext-data params for `type=minerpool, key=stats-history` — per-pool hashrate snapshots over time. Pair with `extDataQuery` to feed the multi-series Hash Rate chart (Mining OS + Aggr Pool + per-pool lines). **Function** ```typescript (range: MinerpoolStatsHistoryRange = {}) => ExtDataParams ``` ### `buildSiteConsumptionTailLogParams` Site-level consumption tail-log params — reads the dedicated powermeter's `site_power_w` aggregate (Mining OS's `type=powermeter, tag=t-powermeter, aggrFields={site_power_w:1}` query). Returns the same series the header's `useSitePowerMete… **Function** ```typescript (range: DashboardQueryRange) => TailLogParams ``` ### `DEFAULT_TIMELINE_OPTIONS` Canonical short-form intervals exposed by MDK UI Shell. The keys map onto the `key=stat-<value>` query parameter expected by `GET /auth/tail-log`. **Constant** ```typescript readonly TimelineOption[] ``` ### `getTimelineOptions` Default options for the dashboard timeline selector. Mirrors Mining OS's `timelineRadioButtons` (5m / 30m / 3h / 1D) — the production dashboard doesn't expose `stat-1m` because the backend typically only emits 5-minute and longer aggregate… **Function** ```typescript (opts: { includeOneMinute?: boolean } = {}) => TimelineOption[] ``` ### `normalizeAlertSeverity` Narrow an arbitrary backend severity string to the `AlertSeverity` literal union expected by the `ActiveIncidentsCard` row component. Unknown values fall back to `'medium'` so the row still renders rather than crashing on an unexpected pay… **Function** ```typescript (raw: string | null | undefined) => AlertSeverity ``` ### `readHashrateMhs` Reads the hashrate aggregate from a tail-log entry. Mining OS emits `hashrate_mhs_1m_sum_aggr` across every `stat-*` bucket, so a single field check covers all timelines; the `_5m_` legacy fallback is retained as a defensive secondary in c… **Function** ```typescript (entry: { hashrate_mhs_1m_sum_aggr?: unknown hashrate_mhs_5m_sum_aggr?: unknown }) => number | undefined ``` ### `SEVERITY_WEIGHT` Severity level → numeric weight. Higher is more urgent. Used for sorting the active-incidents list (most-severe first). **Constant** ```typescript Record ``` # General (/reference/ui/utilities/general) General-purpose helper functions. ## Package `@tetherto/mdk-ui-foundation` ## Utilities @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `ALERT_TYPE_POOL_NAME` **Constant** ```typescript { readonly ip_worker_name: "IP worker name"; readonly wrong_miner_pool: "Wrong miner pool"; readonly wrong_worker_name: "Wrong worker name"; readonly wrong_miner_subaccount: "Wrong miner subaccount";… ``` ### `ALERT_TYPE_POOL_VALUE` **Constant** ```typescript { readonly IP_WORKER_NAME: "ip_worker_name"; readonly WRONG_MINER_POOL: "wrong_miner_pool"; readonly WRONG_WORKER_NAME: "wrong_worker_name"; readonly WRONG_MINER_SUBACCOUNT: "wrong_miner_subaccount";… ``` ### `appendContainerToTag` **Function** ```typescript (deviceId: string) => string ``` ### `appendIdToTag` **Function** ```typescript (deviceId: string) => string ``` ### `appendIdToTags` **Function** ```typescript (deviceIdList: string[]) => string[] ``` ### `AUTH_LEVELS` **Constant** ```typescript { readonly READ: "r"; readonly WRITE: "w"; } ``` ### `AUTH_PERMISSIONS` **Constant** ```typescript { readonly TEMP: "temp"; readonly MINER: "miner"; readonly USERS: "users"; readonly ALERTS: "alerts"; readonly TICKETS: "tickets"; readonly ACTIONS: "actions"; readonly REVENUE: "revenue"; readonly F… ``` ### `AUTH_TOKEN_QUERY_PARAM` Pure URL helpers shared by the auth flow. Kept framework-agnostic so the react-adapter (and any future framework adapter) can call them without pulling in router-specific APIs. **Constant** ```typescript "authToken" ``` ### `buildCabinetDetailParams` List-things params for one LV cabinet's family of devices — the powermeters and temperature sensors whose `info.pos` sits under the cabinet `root`. Mirrors the reference app's `getLvCabinetDevicesByRoot(root)`; the detail hook groups the r… **Function** ```typescript (root: string) => ListThingsParams ``` ### `buildContainerCrossThing` `{ type: 'container', params: { containers } }` fan-out for miner-level actions. **Function** ```typescript (containers: string[]) => DeviceActionCrossThing ``` ### `buildContainerDetailParams` List-things params for the selected containers' detail snapshots. Takes the raw container keys (the `selectedDevicesTags` outer keys / `info.container` names), tags them with `container-` and filters to `t-container` things — mirrors the r… **Function** ```typescript (containerKeys: string[]) => ListThingsParams ``` ### `buildContainerWidgetsListParams` List-things params for the Site Overview container widgets grid. **Function** ```typescript () => ListThingsParams ``` ### `buildContainerWidgetsRealtimeTailLogParams` Tail-log params for the Container Widgets realtime snapshot — the latest `stat-realtime` sample across all miners, grouped so the cards can slice per container. **Function** ```typescript () => TailLogParams ``` ### `buildDeviceActionSubmission` Core submission assembler. Prefer the per-action builders below — they pin each action's param arity/encoding; this is the escape hatch for actions without a dedicated builder. `extras` is spread first so it can never override the pinned s… **Function** ```typescript (action: DeviceActionValue, tags: string[], params: VotingActionParam[] = [], extras: Record = {}) => DeviceActionSubmission ``` ### `buildExplorerListThingsParams` List-things params for one Explorer tab: tag-filtered, status-enriched, projected to `OP_CENTRE_LIST_THINGS_FIELDS`. **Function** ```typescript (tab: ExplorerTabValue, options: { limit?: number; offset?: number } = {}) => ListThingsParams ``` ### `buildMinerCrossThing` `{ type: 'miner', params: { containers } }` fan-out for container-level actions. **Function** ```typescript (containers: string[]) => DeviceActionCrossThing ``` ### `buildRebootAction` `reboot` — no params. **Function** ```typescript (tags: string[]) => DeviceActionSubmission ``` ### `buildResetAlarmAction` `resetAlarm` — no params. **Function** ```typescript (tags: string[]) => DeviceActionSubmission ``` ### `buildSetAirExhaustEnabledAction` `setAirExhaustEnabled` — single boolean param. **Function** ```typescript (tags: string[], isOn: boolean) => DeviceActionSubmission ``` ### `buildSetLedAction` `setLED` — single boolean param. **Function** ```typescript (tags: string[], isOn: boolean) => DeviceActionSubmission ``` ### `buildSetPlcRegistersAction` `setPlcRegisters` (Gamma) — single `{ register: value }` map param. **Function** ```typescript (tags: string[], registers: Record) => DeviceActionSubmission ``` ### `buildSetPowerModeAction` `setPowerMode` — single power-mode param, optional container fan-out. **Function** ```typescript (tags: string[], mode: PowerModeValue, crossThing?: DeviceActionCrossThing) => DeviceActionSubmission ``` ### `buildSetPowerPctAction` `setPowerPct` — percentage encoded as a string, optional container fan-out. **Function** ```typescript (tags: string[], percentage: number, crossThing?: DeviceActionCrossThing) => DeviceActionSubmission ``` ### `buildSetTankEnabledAction` `setTankEnabled` — positional `[tankNumber, isOn]`. **Function** ```typescript (tags: string[], tankNumber: number, isOn: boolean) => DeviceActionSubmission ``` ### `buildSwitchContainerAction` `switchContainer` — single boolean param. **Function** ```typescript (tags: string[], isOn: boolean) => DeviceActionSubmission ``` ### `buildSwitchCoolingSystemAction` `switchCoolingSystem` — single boolean param, optional miner fan-out. **Function** ```typescript (tags: string[], isOn: boolean, crossThing?: DeviceActionCrossThing) => DeviceActionSubmission ``` ### `buildSwitchSocketAction` `switchSocket` — one `{ pdu, socket, enabled }` param per toggled socket. **Function** ```typescript (tags: string[], sockets: SocketSwitch[]) => DeviceActionSubmission ``` ### `buildUpdateThingBatchEntry` One `updateThing` entry inside a move/add/replace-miner batch — carries the miner's new rack/position/network config plus the owning `minerId` the backend uses to group batch progress. **Function** ```typescript (params: UpdateThingParams, minerId: string = params.id) => VotingActionPayload ``` ### `CABINET_DEVICES_TYPES_NAME_MAP` **Constant** ```typescript { readonly "powermeter-abb-b24": "Powermeter ABB B24"; readonly "sensor-temp-seneca": "Sensor Temp Seneca"; readonly "powermeter-abb-m4m20": "Powermeter ABB M4M20"; readonly "powermeter-abb-m1m20": "… ``` ### `checkPermission` Check if user has the requested permission **Function** ```typescript (config: AuthConfig | null | undefined, { perm, write, cap }: PermissionCheck) => boolean ``` ### `COMPLETE_CONTAINER_TYPE` **Constant** ```typescript { readonly BITMAIN_HYDRO: "container-as-hk3"; readonly BITDEER_M30: "container-bd-d40-m30"; readonly BITDEER_M56: "container-bd-d40-m56"; readonly MICROBT_ALPHA: "container-mbt-alpha"; readonly BITDE… ``` ### `COMPLETE_MINER_TYPES` **Constant** ```typescript { readonly ANTMINER_AM_S21: "miner-am-s21"; readonly WHATSMINER_WM_63: "miner-wm-m63"; readonly WHATSMINER_WM_53: "miner-wm-m53s"; readonly AVALON_AV_a1346: "miner-av-a1346"; readonly WHATSMINER_WM_5… ``` ### `CONTAINER_LIST_THINGS_LIMIT` **Constant** ```typescript 1000 ``` ### `CONTAINER_MODEL` Container tag / type / threshold literals. Data-layer contracts shared across the toolkit — owned by ui-foundation so the React layers stay free of tag strings. **Constant** ```typescript { readonly M221: "m221"; readonly GAMMA: "gamma"; readonly BITDEER: "bitdeer"; readonly BITMAIN: "bitmain"; readonly MICROBT: "microbt"; readonly ANTSPACE: "antspace"; readonly BITMAIN_IMM: "bitmain-… ``` ### `CONTAINER_MODEL_FAMILY` Container model families the tab matrix distinguishes. Detection order matters and mirrors the reference app's if/else chain — see `resolveContainerModelFamily`. **Constant** ```typescript { readonly BITDEER: "bitdeer"; readonly ANTSPACE_HYDRO: "antspace-hydro"; readonly ANTSPACE_IMMERSION: "antspace-immersion"; readonly MICROBT: "microbt"; readonly GAMMA: "gamma"; } ``` ### `CONTAINER_SETTINGS_MODEL` **Constant** ```typescript { BITDEER: string; MICROBT: string; HYDRO: string; IMMERSION: string; } ``` ### `CONTAINER_STATUS` **Constant** ```typescript { readonly RUNNING: "running"; readonly OFFLINE: "offline"; readonly STOPPED: "stopped"; } ``` ### `CONTAINER_TAB` Container detail-view tab keys. The per-model availability matrix lives in `utils/container-tabs.ts`. **Constant** ```typescript { readonly PDU: "pdu"; readonly HOME: "home"; readonly ALARM: "alarm"; readonly CHARTS: "charts"; readonly HEATMAP: "heatmap"; readonly CONTROLS: "controls"; readonly SETTINGS: "settings"; readonly P… ``` ### `CONTAINER_TAB_LABEL` Display labels for the container detail tabs, mirroring the reference app's `containerTabsHelper` (`PDU` renders as "PDU Layout", the rest are capitalised keys). **Constant** ```typescript { readonly pdu: "PDU Layout"; readonly home: "Home"; readonly alarm: "Alarm"; readonly charts: "Charts"; readonly heatmap: "Heatmap"; readonly controls: "Controls"; readonly settings: "Settings"; rea… ``` ### `CONTAINER_TAB_MATRIX` Base tab sequence per model family — order is display order. Power Adjustment is intentionally absent here: it is inserted positionally by `getSupportedContainerTabs`. **Constant** ```typescript Record ``` ### `CONTAINER_TACTICS_TYPE` **Constant** ```typescript { readonly COIN: "coin"; readonly DISABLED: "disabled"; readonly ELECTRICITY: "electricity"; } ``` ### `CONTAINER_TYPE` **Constant** ```typescript { readonly BITDEER: "bd"; readonly ANTSPACE: "as"; readonly MICROBT: "mbt"; readonly ANTSPACE_HYDRO: "as-hk3"; readonly ANTSPACE_IMMERSION: "as-immersion"; } ``` ### `CONTAINER_TYPE_NAME_MAP` **Constant** ```typescript { readonly "container-bd-d40-m30": "Bitdeer M30"; readonly "container-bd-d40-m56": "Bitdeer M56"; readonly "container-bd-d40-s19xp": "Bitdeer S19XP"; readonly "container-as-hk3": "Bitmain Hydro"; rea… ``` ### `CONTAINER_WIDGETS_AGGR_FIELD_KEYS` All realtime aggregate fields the cards read — the tail-log `aggrFields` set. **Constant** ```typescript readonly string[] ``` ### `CONTAINER_WIDGETS_SUMMARY_FIELD` Realtime summary aggregate field names (these carry the `_aggr` suffix). **Constant** ```typescript { readonly HASHRATE_MHS_1M_SUM: "hashrate_mhs_1m_group_sum_aggr"; readonly TEMPERATURE_MAX: "temperature_c_group_max_aggr"; readonly TEMPERATURE_AVG: "temperature_c_group_avg_aggr"; } ``` ### `CONTAINERS_MINER_TYPE` **Constant** ```typescript { readonly M56: "m56"; readonly M30: "m30"; readonly A1346: "a1346"; readonly S19XP: "s19xp"; } ``` ### `DEFAULT_HISTORICAL_WINDOW_MS` Default historical-alerts look-back window (14 days), matching the devkit `<Alerts>` feature default. Wider ranges fan out into more 24h requests — see `fetchHistoricalAlertsInChunks`. **Constant** ```typescript number ``` ### `deriveContainerActivity` Slice the realtime aggregate into one container's per-status miner counts. `total` is the container's nominal miner capacity; miners not accounted for by any status count collapse into `disconnected` (never negative). With no realtime samp… **Function** ```typescript (realtime: TailLogEntry | undefined, containerModel: string, total: number) => ContainerActivity ``` ### `deriveContainerSummary` **Function** ```typescript (realtime: TailLogEntry | undefined, containerModel: string) => ContainerSummary ``` ### `deriveContainerTanks` Derive the per-tank readings for a container's immersion cooling system — one entry per oil pump, joined with the matching water pump and (when present) the tank pressure. Returns an empty array for containers without an immersion cooling… **Function** ```typescript (container: ListThingsDevice) => TankReading[] ``` ### `deriveSelectedSockets` Derive the store's `selectedSockets` map (keyed by container tag) from the selected device-tags — the pure body of the reference app's `findAndSetSelectedSockets`. Each per-container tag key (`pos-…` / `id-…`) is stripped of its `pos-` pre… **Function** ```typescript (containers: ContainerSnapshotForSockets[] | undefined, selectedDevicesTags: Record>, allDevices: MinerForSocket[] | undefined) => Record ``` ### `exportSettingsToFile` **Function** ```typescript (data: SettingsExportData) => string ``` ### `filterUsers` **Function** ```typescript ({ users, email, role }: FilterUsersParams) => SettingsUser[] ``` ### `findMatchingContainer` Find the container-settings row for a container type: an exact `model` match first, else the settings-model family fallback. Mirrors the reference app's `findMatchingContainer`. **Function** ```typescript (settings: ContainerSettingsEntry[] | undefined, containerType: string | undefined) => ContainerSettingsEntry | null ``` ### `flattenKernelEnvelope` Flattens the per-Kernel response envelope the gateway wraps around merged worker responses (`/auth/list-things`, `/auth/list-racks`, ... return `[[row, ...], [row, ...]]` — one inner array per Kernel). Null-safe on both levels: a missing e… **Function** ```typescript (envelope: ReadonlyArray | null | undefined) => T[] ``` ### `formatLastActive` **Function** ```typescript (timestamp: string | undefined) => string ``` ### `formatRoleLabel` **Function** ```typescript (role: string) => string ``` ### `getAntspaceHydroIndexes` Antspace Hydro position → `[rack, pdu, socket]`. **Function** ```typescript (pos: string) => string[] ``` ### `getAntspaceImmersionIndexes` Antspace Immersion position → `[pdu, socket]`. **Function** ```typescript (pos: string) => string[] ``` ### `getBitdeerIndexes` Bitdeer / MicroBT position → `[pdu, socket]`. **Function** ```typescript (pos: string) => string[] ``` ### `getByIdsQuery` **Function** ```typescript (ids: string[], allowEmptyArray?: boolean) => string ``` ### `getByTagsQuery` **Function** ```typescript (filterTags: string[], allowEmptyArray?: boolean) => string ``` ### `getByTagsWithAlertsQuery` **Function** ```typescript (filterTags: string[], allowEmptyArray?: boolean) => string ``` ### `getByTagsWithCriticalAlertsQuery` **Function** ```typescript (filterTags: string[], allowEmptyArray?: boolean) => string ``` ### `getByThingsAttributeQuery` **Function** ```typescript (filterAttributes: FilterAttribute[], selectedTypes: string[], allowEmptyArray?: boolean) => UnknownRecord ``` ### `getByTypesQuery` **Function** ```typescript (filterTypes: string[], allowEmptyArray?: boolean) => string ``` ### `getConnectedMinerForSocket` The selected miner sitting at `pos`, if any (miners only, exact position). **Function** ```typescript (devices: MinerForSocket[] | undefined, pos: string) => MinerForSocket | undefined ``` ### `getContainerByContainerTagsQuery` **Function** ```typescript (filterTags: string[], allowEmptyArray?: boolean) => string ``` ### `getContainerMinersByContainerTagsQuery` **Function** ```typescript (filterTags: string[], allowEmptyArray?: boolean) => string ``` ### `getContainerSettingsModel` Map a container type string to its settings-model key (`bd` / `mbt` / `hydro` / `immersion`), or `null` for an unknown family. Mirrors the reference app's `getContainerSettingsModel`. **Function** ```typescript (containerType: string | undefined) => string | null ``` ### `getDeviceByAlertId` **Function** ```typescript (uuid: string) => string ``` ### `getFiltersQuery` **Function** ```typescript (filterTags?: string[], filters?: Record, selectedTypes: string[] = ['t-container']) => UnknownRecord ``` ### `getListQuery` **Function** ```typescript (filterTags: string[], filters?: Record, selectedTypes: string[] = ['t-container']) => string ``` ### `getLvCabinetDevicesByRoot` **Function** ```typescript (root: string) => string ``` ### `getMinersByContainerTagsQuery` **Function** ```typescript (filterTags: string[], allowEmptyArray?: boolean) => string ``` ### `getPduByIndex` The PDU row whose `pdu` matches `pduIndex` on this container, or undefined. **Function** ```typescript (container: ContainerSnapshotForSockets | undefined, pduIndex: string | number) => PduSnapshot | undefined ``` ### `getPduData` The `pdu_data` array off a container detail snapshot, or undefined. **Function** ```typescript (last: ContainerSnapshotForSockets['last']) => PduSnapshot[] | undefined ``` ### `getRolesFromAuthToken` Extract roles from authentication token **Function** ```typescript (authToken?: string) => string[] ``` ### `getSignInRedirectUrl` Get redirect URL based on user's primary role **Function** ```typescript (authToken: string | null | undefined) => string ``` ### `getSitePowerMeterQuery` **Function** ```typescript () => string ``` ### `getSocketInfo` Resolve one socket for `pos` on `container`: pick the vendor index scheme off the container name, then (for Bitdeer / MicroBT) join the live PDU row so `enabled` / `cooling` reflect the snapshot. Hydro / Immersion have no live socket table… **Function** ```typescript (container: ContainerSnapshotForSockets, pos: string, allDevices: MinerForSocket[] | undefined) => DerivedSocket ``` ### `getSupportedContainerTabs` Full tab sequence for a container type: the family's base sequence, plus Power Adjustment spliced in after the PDU tab for Whatsminer containers. Unknown types resolve to an empty list. **Function** ```typescript (type: string | undefined) => ContainerTabValue[] ``` ### `getWidgetAlarmState` Resolve a container's alarm state from its live stats and matched settings. **Function** ```typescript (_container: ListThingsDevice, _settings: ContainerSettingsEntry | null = null) => ContainerAlarmState ``` ### `isAntminer` **Function** ```typescript (type: string | undefined) => boolean ``` ### `isAntspaceHydroContainer` Antspace/Bitmain hydro family (`as-hk3`, `antspace-hydro`, `bitmain-hydro`). **Function** ```typescript (type: string | undefined) => boolean ``` ### `isAntspaceImmersionContainer` Antspace/Bitmain immersion family (`as-immersion`, `bitmain-imm[ersion]`). **Function** ```typescript (type: string | undefined) => boolean ``` ### `isAvalon` **Function** ```typescript (type: string | undefined) => boolean ``` ### `isBitdeerContainer` Bitdeer family — `container-bd-*` and anything mentioning `bitdeer`. **Function** ```typescript (type: string | undefined) => boolean ``` ### `isContainer` **Function** ```typescript (type: string | undefined) => boolean ``` ### `isGammaContainer` Gamma family (`m221`, `gamma`). **Function** ```typescript (type: string | undefined) => boolean ``` ### `isMicroBTContainer` MicroBT family (`mbt`, `microbt`). **Function** ```typescript (type: string | undefined) => boolean ``` ### `isMiner` **Function** ```typescript (type: string | undefined) => boolean ``` ### `isPduContainerTab` The PDU grid renders under the `pdu` tab key. **Function** ```typescript (tab: string | undefined) => boolean ``` ### `isWhatsminer` **Function** ```typescript (type: string | undefined) => boolean ``` ### `isWhatsminerContainer` Containers populated with Whatsminer miners (`m56` / `m30` positions, or any MicroBT container) — the set that gets the Power Adjustment tab. **Function** ```typescript (type: string | undefined) => boolean ``` ### `LV_CABINET_DEVICES_TAG` **Constant** ```typescript { readonly POWERMETER: "t-powermeter"; readonly SENSOR_TEMP: "t-sensor-temp"; } ``` ### `LV_CABINET_DEVICES_TYPE` **Constant** ```typescript { readonly POWERMETER_ABB_B24: "powermeter-abb-b24"; readonly SENSOR_TEMP_SENECA: "sensor-temp-seneca"; readonly POWERMETER_ABB_M1M20: "powermeter-abb-m1m20"; readonly POWERMETER_ABB_M4M20: "powermet… ``` ### `MAINTENANCE_CONTAINER` **Constant** ```typescript "maintenance" ``` ### `MINER_BRAND_NAMES` **Constant** ```typescript { readonly av: "Avalon"; readonly am: "Antminer"; readonly wm: "Whatsminer"; } ``` ### `MINER_MODEL` Device / miner / power-meter tag + type literals. These are data-layer contracts shared between the API surface and the UI — owned by ui-foundation so the React layers can stay free of tag strings. **Constant** ```typescript { readonly AVALON: "avalon"; readonly ANTMINER: "antminer"; readonly WHATSMINER: "whatsminer"; } ``` ### `MINER_MODEL_TO_TYPE_MAP` **Constant** ```typescript { readonly av: "avalon"; readonly am: "antminer"; readonly wm: "whatsminer"; } ``` ### `MINER_POWER_MODE` **Constant** ```typescript { readonly SLEEP: "sleep"; readonly LOW: "low"; readonly NORMAL: "normal"; readonly HIGH: "high"; } ``` ### `MINER_TYPE` **Constant** ```typescript { readonly AVALON: "av"; readonly ANTMINER: "am"; readonly WHATSMINER: "wm"; } ``` ### `MINER_TYPE_MESSAGE` **Constant** ```typescript { readonly "miner-av-a1346": "A1346 miners do not report consumption individually, so Avg. efficiency cannot be calculated"; } ``` ### `MINER_TYPE_NAME_MAP` **Constant** ```typescript { readonly "miner-av-a1346": "Avalon A1346"; readonly "miner-am-s21": "Antminer S21"; readonly "miner-wm-m63": "Whatsminer M63"; readonly "miner-wm-m56s": "Whatsminer M56S"; readonly "miner-wm-m53s":… ``` ### `MinerStatuses` **Constant** ```typescript { readonly MINING: "mining"; readonly OFFLINE: "offline"; readonly SLEEPING: "sleeping"; readonly ERROR: "error"; readonly NOT_MINING: "not_mining"; readonly MAINTENANCE: "maintenance"; readonly ALER… ``` ### `NO_MAINTENANCE_CONTAINER` **Constant** ```typescript "no_maintenance" ``` ### `ONE_DAY_MS` One day in milliseconds — the historical-log fetch window size. **Constant** ```typescript number ``` ### `OP_CENTRE_CABINET_DETAIL_FIELDS` The cabinet-detail field projection — the sensor/powermeter fields the LV cabinet detail reads: each device's `power_w` / `temp_c` reading, status (for the offline marker) and `last.alerts` (for the warnings timeline). **Constant** ```typescript string ``` ### `OP_CENTRE_CONTAINER_DETAIL_FIELDS` The container-detail field projection — a superset of the list projection that also pulls the full `last.snap.stats` (so the socket transform sees `container_specific.pdu_data`, ambient temp, humidity, …) and the full `last.snap.config` (p… **Constant** ```typescript string ``` ### `OP_CENTRE_CONTAINER_WIDGETS_FIELDS` Container list projection for the Site Overview widgets — the lean list fields plus `last.snap.stats.container_specific` (cooling system: oil / water pumps, tanks) and `last.snap.config` (per-vendor thresholds), which the vendor boxes (e.g… **Constant** ```typescript string ``` ### `OP_CENTRE_LIST_THINGS_FIELDS` The list-things field projection the Explorer tables and Container Widgets cards read — the reference app's `LIST_THINGS_FIELDS`, kept as one projection so every consumer sees the same row shape. **Constant** ```typescript string ``` ### `parseSettingsFile` **Function** ```typescript (file: File) => Promise ``` ### `PM_ATTRIBUTE_LABEL_MAP` **Constant** ```typescript { readonly 'I1 a': "Current L1"; readonly 'I2 a': "Current L2"; readonly 'I3 a': "Current L3"; readonly 'V3 n v': "Voltage L3-N"; readonly 'V2 n v': "Voltage L2-N"; readonly 'V1 n v': "Voltage L1-N";… ``` ### `POWER_MODE` Miner power modes accepted by `setPowerMode`. **Constant** ```typescript { readonly SLEEP: "sleep"; readonly LOW: "low"; readonly NORMAL: "normal"; readonly HIGH: "high"; } ``` ### `removeContainerPrefix` **Function** ```typescript (text: string) => string ``` ### `resolveContainerModelFamily` Resolves a raw container `type` string (e.g. `container-bd-d40-m56`) to its model family, or `undefined` for unknown types. First match wins, in the reference app's original branch order. **Function** ```typescript (type: string | undefined) => ContainerModelFamily | undefined ``` ### `SITE_OVERVIEW_STATUSES` **Constant** ```typescript { readonly OFFLINE: "offline"; readonly EMPTY: "empty"; readonly NOT_MINING: "not_mining"; readonly MINING: "mining"; } ``` ### `SOCKET_STATUSES` **Constant** ```typescript { readonly ERROR_MINING: "errorMining"; readonly MINER_DISCONNECTED: "disconnected"; readonly CONNECTING: "connecting"; readonly SLEEP: "sleep"; readonly LOW: "low"; readonly NORMAL: "normal"; readon… ``` ### `THRESHOLD_LEVEL` **Constant** ```typescript { readonly ALERT: "alert"; readonly ALARM: "alarm"; readonly NORMAL: "normal"; readonly ALARM_LOW: "alarmLow"; readonly ALARM_HIGH: "alarmHigh"; readonly CRITICAL_LOW: "criticalLow"; readonly CRITICA… ``` ### `THRESHOLD_TYPE` **Constant** ```typescript { readonly TANK_PRESSURE: "tankPressure"; readonly OIL_TEMPERATURE: "oilTemperature"; readonly WATER_TEMPERATURE: "waterTemperature"; readonly SUPPLY_LIQUID_PRESSURE: "supplyLiquidPressure"; } ``` ### `USER_ROLE` **Constant** ```typescript { readonly ADMIN: "admin"; readonly READ_ONLY: "read_only_user"; readonly SITE_MANAGER: "site_manager"; readonly SITE_OPERATOR: "site_operator"; readonly FIELD_OPERATOR: "field_operator"; readonly RE… ``` ### `validateSettingsJson` **Function** ```typescript (data: unknown) => data is SettingsExportData ``` ### `VOTING_SUBMISSION_TYPE` Submission `type` for the voting/approval workflow. **Constant** ```typescript "voting" ``` # Core utils (/reference/ui/utilities/utils) Core utility functions for formatting, validation, and data processing. ## Package `@tetherto/mdk-ui-foundation` ## Utilities @tetherto/mdk-ui-foundation Import the public APIs on this page from [`@tetherto/mdk-ui-foundation`](/reference/ui/#tethertomdk-ui-foundation). ### `getLatestSample` Tiny projection helper used by the dashboard's header-stat hooks. **Function** ```typescript (entries: readonly T[] | null | undefined) => T | undefined ``` # Workers (/reference/worker) # Workers 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 — Whatsminer, 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](/guides/deployment), 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/whatsminer/ plugin/ index.js # the Worker Plugin: { contract, dir, connect, disconnect } mdk-contract.json boot.js # startWhatsminerWorker(opts) — constructs WorkerRuntime lib/whatsminer.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)
Migrating from MDKWorkerAdapter / ThingManager (pre-0.5.0) `WorkerRuntime` generalizes the former `MDKWorkerAdapter` (persistent seeds, single HRPC respond loop, DHT topic announce carried over) and replaces `ThingManager` delegation with per-device handler dispatch. See [Worker Runtime legacy services](https://github.com/tetherto/mdk/blob/main/docs/reference/maintainers/worker-runtime-legacy-services.md) for the full migration history and the optional `opts.services` built-in surface that lets a host answer legacy queries and commands from a manager's store.
### 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 Kernel fetches this contract once via `capability.request` and caches it. The Gateway and AI agents use it to derive available operations dynamically. ## 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') const { startWhatsminerWorker } = require('@tetherto/mdk-worker-whatsminer') const kernel = await getKernel() const worker = await startWhatsminerWorker({ workerId: 'whatsminer-rack-1', model: 'm56s', storeDir: './store/whatsminer-rack-1', seedDevices: [{ info: { serialNum: 'WM-001' }, opts: { address: '192.168.1.10', port: 14028, password: 'admin' } }] }) 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/whatsminer/USAGE.md`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/whatsminer/USAGE.md)). ## 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) (Whatsminer, 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 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/whatsminer && npm test cd backend/workers/miners/antminer && 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 will be handled confidentially and reviewed in coordination with the MDK core team. --- ## Enforcement Guidelines The MDK core team may take any action deemed appropriate, including: - Warning the participant - Temporarily restricting access - Permanently banning a participant from the community - Removing content that violates this Code of Conduct Decisions regarding enforcement are final. --- ## Amendments This Code of Conduct may be updated from time to time by the MDK core team to reflect evolving community needs. # Contributing (/support/community/contributing) # Contribute to MDK Thank you for your interest in contributing to [MDK](https://github.com/tetherto/mdk/). This document outlines the contribution workflow for the MDK repository, from setting up your development environment to submitting pull requests and participating in releases. ## Security If you discover a security vulnerability, do not report it in a public issue. Please follow the private disclosure instructions in [SECURITY.md](https://github.com/tetherto/mdk/blob/main/SECURITY.md). ## Monorepo structure MDK is a monorepo with separate backend and frontend workspaces: - Backend: - `backend/core/`: Backend services, container modules, and integration/unit tests (npm-based) - [`backend/workers/`](/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** (version 11 or higher) ### Licensing MDK is released under the [**Apache License 2.0**](https://github.com/tetherto/mdk/blob/main/LICENSE). By contributing, you agree that: - You retain copyright over your contributions - You grant a perpetual, worldwide, royalty-free license for their use - Contributions are provided **“AS IS”**, without warranty ## Development environment setup
1. Fork and clone 1. Fork [the repository](https://github.com/tetherto/mdk.git) on GitHub. 2. Clone your fork locally and navigate into the project directory: ```bash git clone https://github.com/username/mdk.git cd mdk ``` 1. Add the upstream remote: ```bash git remote add upstream https://github.com/tetherto/mdk.git ```
2. Stay in sync Keep your fork in sync with the main repository. For example: ```bash git fetch upstream git merge --ff-only upstream/main # fails loudly if main has diverged ```
### Backend contribution setup Use this workflow when contributing to backend code under `backend/core/`. ```bash cd backend/core npm install ``` #### Common commands ```bash # Lint backend code npm run lint # Run backend test suite (lint + unit + integration + package tests) npm test ``` ### Frontend contribution setup Use this workflow when contributing to frontend code under `ui/`. ```bash cd ui npm install ``` #### Common commands ```bash # Build packages npm run build # Run dev mode (all packages + demo) npm run dev # Lint and type-check npm run lint npm run typecheck # Run tests npm test ``` ## Pull request workflow ### Conventional types MDK uses Conventional Commits-style types for both branch names and PR titles. | Type | Use for | |---|---| | `feat` | New features | | `fix` | Bug fixes | | `docs` | Documentation changes | | `refactor` | Code refactoring without behaviour change | | `test` | Test additions or changes | | `chore` | Tooling, dependencies, repo maintenance | | `perf` | Performance improvements | | `style` | Formatting only (no logic change) | | `ci` | CI configuration changes | | `build` | Build system or external dependency changes | ### Branch naming convention Create branches using the following pattern: ```bash {type}/{short-description} ``` Where `{type}` is one of the [conventional types](#conventional-types). #### Branch naming examples ```bash # New feature git checkout -b feat/mdk-new-device # Bug fix git checkout -b fix/timeout-handling ``` ### Commit message template The repository ships a commit message template at [`.gitmessage`](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 ### 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 will be proposed and approved by the MDK core team. # MDK Repositories (/support/resources/repositories) The MDK monorepo makes the frontend and backend components publicly accessible: - [https://github.com/tetherto/mdk](https://github.com/tetherto/mdk) *MDK is developed by Tether and released under the [Apache 2.0 license](/support/community/contributing#licensing).* # Roadmap (/support/resources/roadmap) MDK follows a **two-week release cadence** to keep progress visible, collect feedback early, and progressively harden the platform — from a rudimentary end-to-end foundation toward a production-ready release. ## Release principles - **Release every two weeks** to keep momentum and feedback loops short - **Use four maturity phases** to mark clear readiness jumps - **Start with a working end-to-end developer experience**, then harden progressively - **Reach production readiness progressively**, not by a single large release - **Follow standard [semantic versioning](https://semver.org)** — the `0.x` line means MDK is still in development and not intended for production ## Versioning and naming strategy MDK uses standard [semantic versioning](https://semver.org) (`MAJOR.MINOR.PATCH`): - A **new version ships every two weeks**, bumping the **minor**: `0.2.0` → `0.3.0` → `0.4.0` → … - **Patch** releases (`0.x.1`) go out only when a fix is needed between the scheduled releases, e.g. 0.2.1 - While MDK is in the **`0.x` line**, interfaces may change and the platform is **not intended for production** — this is the standard semver signal, and we use it deliberately so developers can read it literally - **`1.0.0`** marks the first **production-ready** release Maturity is tracked by **phase**, not by version number — each phase spans several bi-weekly releases. The four phases describe how ready MDK is at each stage: | Phase | Production-readiness | |---|---| | **Foundation** | Experimental. End-to-end but rudimentary. Not stable. | | **Lab testing** | Complete enough to build against end-to-end, in a lab. Not stable, not for production. | | **On site testing** | Stable enough to test at real sites under real operating conditions, closely monitored. Not yet production-ready. | | **Production Ready** | Ready for production deployment, with stable interfaces and compatibility guarantees. | ## 2026 2026 moves MDK through **four phases**: - **May:** [Foundation](#foundation) - **July:** [Lab testing](#lab-testing) - **October:** [On site testing](#on-site-testing) - **December:** [Production Ready](#production-ready) Between phases, MDK ships a new release **every two weeks**, starting from the Foundation release at the end of May. The diagram below shows only the phase milestones; the bi-weekly releases land in the windows between them. ```mermaid graph TB subgraph foundation [Foundation] p1([May 2026

Foundation
Public, end-to-end but rudimentary]) it1[[Jun–Jul 2026
New release every 2 weeks]] end subgraph lab [Lab testing] p2([Jul 2026
Lab testing
Complete end-to-end, lab only]) it2[[Aug–Oct 2026
New release every 2 weeks]] end subgraph onsite [On site testing] p3([Oct 2026
On site testing
Testing at real sites]) it3[[Nov–Dec 2026
New release every 2 weeks]] end subgraph prod [Production Ready] p4([Dec 2026
Production Ready
Production-ready baseline]) end p1 --> it1 --> p2 --> it2 --> p3 --> it3 --> p4 style foundation fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A style lab fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A style onsite fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A style prod fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A ``` ## Release plan Phase descriptions below focus on the **level of maturity and production-readiness** at each stage. The exact feature scope of each two-week release is decided as we go and is not fixed by this roadmap. ### Foundation *Released end of May 2026.* The first public release is about **visibility, direction, and feedback**. It is intentionally rudimentary: developers can clone it, run an end-to-end example, and get a concrete feel for how MDK fits together. Interfaces are expected to change. The goal is to show where MDK is heading and invite feedback from developers, partners, and early contributors — not to support real workloads. ### Lab testing *Target: end of July 2026.* At this stage MDK is **complete enough to build against end to end**. Developers can run a full workflow and experiment with MDK in their own labs. It is **not stable** — breaking changes are still expected between releases — and it is **not ready for production**. The goal is to validate the end-to-end developer workflow and surface integration gaps before MDK is exposed to real operational conditions. ### On site testing *Target: end of October 2026.* This is the first stage intended for **testing at real sites**. MDK should be stable enough to run on site under real operating conditions, while still being monitored closely. The goal is to validate performance, robustness, deployment workflows, and operational fit in real scenarios. This is not yet a stability commitment — interfaces may still change before production readiness. ### Production Ready *Target: end of December 2026.* This is the first **production-ready** release (`1.0.0`). By this point MDK offers a solid baseline for production deployment, with stable core interfaces, validated workflows, and documentation that supports adoption by operators, integrators, and developers building on top of the platform. From here, standard semver compatibility guarantees apply. # Tutorials (/tutorials) ## New to MDK? 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 run setup:core npm run setup:workers 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 a plugin and SQLite helper, not a boot function. The separate [`demo-worker-caller`](https://github.com/tetherto/mdk/blob/main/examples/backend/demo-worker-caller/index.js) used here owns `WorkerRuntime`, device configuration, persistence, sampling, and shutdown. `getKernel({ root: ROOT })` with no `topic`/`discovery` option defaults to DHT discovery with a fresh random topic. This example then registers the Worker's public key directly because both objects are in the same process. See the discovery model 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. 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. ## 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, read [terminology](/reference/glossary) first. ## 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 - Two MCP surfaces exposing the site as tools for AI agents: the Gateway's auto-exported routes on `:3100`, and a hand-authored, agent-contract tool set on `:3101` 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 >=24 (LTS) - npm >=11 - 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; see [configure devices](https://github.com/tetherto/mdk/blob/main/examples/mvp-site/README.md#configure-devices) for 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 There are a range of agents and connection modes; 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 two ways, both listed in `.mcp.json.example`: - The Gateway auto-exports its own `/site/*` routes as tools, on `:3100` - A hand-authored tool set with agent-contract metadata — `backend/mcp-plugins/site/mcp-plugin.json` — on `:3101`, 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 Point an MCP client at either URL and it can query the fleet or, for the second surface, act on it with operator approval. ### 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 servers. 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 both MCP surfaces — the Gateway's auto-exported tools on `:3100`, and the hand-authored agent-contract tool set on `:3101`. ## 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](https://github.com/tetherto/mdk/blob/main/examples/mvp-site/README.md#troubleshooting) - 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 two-step flow every MDK agent session uses: wire the IDE once, 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. The prompt asked for a **statistics tutorial page** so students can explore distributions, trends, and raw grades. You see every: - [UI CLI](/guides/ui/ui-cli) command the agent would run - Resulting page code - Run instructions We use mock JSON in the browser. No Kernel, Worker, pool API, or fleet backend is required. Presentational imports from `@tetherto/mdk-react-devkit/core` are enough for a highly visual, shippable UI. ## What you'll learn 1. **Domain is yours.** Component contracts describe shape and behavior (chart datasets, table columns), not industry vocabulary. 2. **The agent path is unchanged.** `init` → plain-language intent → local manifests → scaffold → `check`. 3. **Visual density is supported.** Bar charts, line trends, sortable tables, and stat cards compose into a dashboard that *looks* like a product, not a wireframe. The demo app is **Stats Lab**: a fictional intro statistics course where Teaching Assistants review quiz score histograms, weekly class averages, and per-student grades. ## Prerequisites - **Node.js** >=24 - **npm** >=11 - **React** 19+ and **react-dom** 19+ - A project folder inside the MDK UI monorepo (this walkthrough uses `apps/stats-lab`, same pattern as [Install and wire the React packages](/guides/ui/install)) - Your IDE [wired to MDK once](/guides/ui/ui-cli#init) with `mdk-ui init`, or willingness to run `init` in step 1 ## Agent session walkthrough ### Wire the IDE (same as agents) From your app or monorepo project root: ```bash npx @tetherto/mdk-ui-cli init --ide cursor ``` This writes `.mdk/context.md` and `.cursor/rules/mdk.mdc` so the session already knows the UI CLI surface. See [init](/guides/ui/ui-cli#init). ### State a non-mining intent Paste a prompt that names the domain explicitly so the agent does not reach for hashrate widgets: > Build a **statistics tutorial dashboard** for students in `apps/stats-lab`. Include: > > - A **histogram** of final exam scores (bar chart buckets 50–59 through 90–100) > - A **line chart** of weekly class average over six weeks > - A **sortable table** of students with midterm, final, and section > - Three **summary stat cards**: mean final, median final, enrollment count > > Use mock data in the repo. Import only from `@tetherto/mdk-react-devkit/core` and `@tetherto/mdk-react-devkit/foundation` where > needed. No mining APIs. The agent's job is the same whatever the domain: discover exports, scaffold, and verify compile. ### Discovery commands the agent runs Behind the prompt, a well-behaved session issues deterministic CLI lookups (no model calls). A representative transcript: ```bash npx @tetherto/mdk-ui-cli suggest "statistics dashboard histogram line chart data table stat cards" ``` Typically returns chart-category components near the top — `BarChart`, `LineChart`, and closely related chart primitives. General-purpose components like `DataTable`, `SingleStatCard`, and `Tabs` score lower on chart-focused queries and are best found with the `find --category` commands in the next accordion. Because we asked for mock data only, the agent skips adapter hooks like `useDevices`. ```bash npx @tetherto/mdk-ui-cli find --category charts --format table npx @tetherto/mdk-ui-cli find --category tables --format table ``` Narrows to chart primitives and table components with stable exports. ```bash npx @tetherto/mdk-ui-cli docs BarChart npx @tetherto/mdk-ui-cli docs LineChart npx @tetherto/mdk-ui-cli example LineChart npx @tetherto/mdk-ui-cli docs DataTable ``` The agent copies real prop shapes (`LineChartData` millisecond `x` values, `DataTableColumnDef` accessors) instead of inventing APIs. ```bash npx @tetherto/mdk-ui-cli add page StatsLab \ --component BarChart \ --component LineChart \ --component DataTable \ --component SingleStatCard npx @tetherto/mdk-ui-cli check apps/stats-lab/src/App.tsx ``` `add page` emits a starter layout; the agent fills in mock datasets and labels. `check` validates imports and component prop contracts against the real package barrels; the app build in the local run verifies the final Vite project. Full command reference: [UI CLI](/guides/ui/ui-cli). ### Review the scaffolded page After the agent edits `App.tsx`, a Stats Lab dashboard might look like this: ```tsx BarChart, LineChart, DataTable, Tabs, TabsList, TabsTrigger, TabsContent, } from '@tetherto/mdk-react-devkit/core' type Student = { id: string name: string section: 'A' | 'B' midterm: number final: number } const students: Student[] = [ { id: '1', name: 'Alex Kim', section: 'A', midterm: 82, final: 88 }, { id: '2', name: 'Jordan Lee', section: 'B', midterm: 74, final: 79 }, { id: '3', name: 'Sam Rivera', section: 'A', midterm: 91, final: 94 }, { id: '4', name: 'Taylor Ng', section: 'B', midterm: 68, final: 72 }, { id: '5', name: 'Casey Park', section: 'A', midterm: 85, final: 90 }, ] const weekStart = (weekIndex: number): number => new Date(2025, 0, 6 + weekIndex * 7).valueOf() const StatsLab = (): React.JSX.Element => { const [sorting, setSorting] = useState([]) const finals = students.map((s) => s.final) const meanFinal = finals.reduce((a, b) => a + b, 0) / finals.length const sortedFinals = [...finals].sort((a, b) => a - b) const medianFinal = sortedFinals[Math.floor(sortedFinals.length / 2)] const histogram = useMemo( () => ({ labels: ['50–59', '60–69', '70–79', '80–89', '90–100'], datasets: [ { label: 'Students', data: [1, 2, 4, 6, 3], backgroundColor: '#6366f1', }, ], }), [], ) const weeklyAverage = useMemo( () => ({ datasets: [ { label: 'Class average', borderColor: '#22c55e', data: [71, 74, 76, 79, 81, 83].map((y, i) => ({ x: weekStart(i), y, })), }, ], }), [], ) const columns: DataTableColumnDef[] = [ { accessorKey: 'name', header: 'Student' }, { accessorKey: 'section', header: 'Section' }, { accessorKey: 'midterm', header: 'Midterm' }, { accessorKey: 'final', header: 'Final' }, ] return (

Stats Lab

Intro statistics — interpret distributions and trends

Charts Roster

Final exam distribution

Weekly class average

) } ```
Nothing in this file references pools, workers, or TH/s. The same components appear on mining pages because the **data model is generic**.
## Run Stats Lab locally Follow the same monorepo workflow as [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 Manifests list real exports, `check` catches many invented props, and `docs` / `example` ground the session in shipped contracts. The app build remains the final TypeScript and Vite verification. ## Next steps - Install and wire the [MDK React packages](/guides/ui/install): the provider, hooks, and theming your generated pages rely on - [UI CLI reference](/guides/ui/ui-cli): every command in the discovery transcript - [Chart components](/reference/ui/components/charts): `BarChart`, `LineChart`, and related data shapes - [Data display](/reference/ui/components/display): `DataTable` sorting and pagination - [Install and wire the React packages](/guides/ui/install): hands-on setup without an agent