# MDK Docs (/v0-4-0)

<LandingHero
  title="MDK Docs"
  description="Open, modular infrastructure for Bitcoin mining at any scale. Pick the path that matches how you build."
/>

<LandingJourney title="I'm building with MDK" variant="builder">
  <LandingCards>
    <LandingCard
      accent="amber"
      image="/images/ui-kit/icons/site-overview-icon.svg"
      icon={<BookOpen />}
      title={<span {...cardTitle}>Learn what MDK is</span>}
      href="/v0-4-0/concepts/about"
      description={
        <span {...cardProse}>
          Product overview, architecture, and the MDK packages
        </span>
      }
    />
    <LandingCard
      accent="teal"
      image="/images/ui-kit/icons/operations-nav-icon.svg"
      icon={<Compass />}
      title={<span {...cardTitle}>Get started path picker</span>}
      href="/v0-4-0/get-started"
      description={
        <span {...cardProse}>
          Discover your path, including backend tools and the raw client
        </span>
      }
    />
    <LandingCard
      accent="violet"
      image="/images/ui-kit/icons/dashboard-nav-icon.svg"
      icon={<Rocket />}
      title={<span {...cardTitle}>Ship a dashboard fast</span>}
      description={
        <>
          <span {...cardProse}>Two ways to ship a mining dashboard:</span>
          <Link href="/v0-4-0/agents" className="landing-card-link landing-card-link--teal">
            Build with your AI agent{' '}
            <LandingLinkSuffix>&rarr;</LandingLinkSuffix>
          </Link>
          <Link href="/v0-4-0/ui/" className="landing-card-link landing-card-link--amber">
            Browse the UI Kit{' '}
            <LandingLinkSuffix>&rarr;</LandingLinkSuffix>
          </Link>
        </>
      }
    />
    <LandingCard
      accent="slate"
      image="/images/ui-kit/icons/hashrate-card-icon.svg"
      icon={<Atom />}
      title={<span {...cardTitle}>Use UI Core without a dashboard</span>}
      href="/v0-4-0/how-to/ui/core/use-ui-core-headlessly"
      description={
        <span {...cardProse}>
          <code>@tetherto/mdk-ui-core</code> is headless: Zustand stores and a <code>QueryClient</code> factory you can wire into any runtime
        </span>
      }
    />
  </LandingCards>
</LandingJourney>

<LandingJourney
  variant="agent"
  title={
    <>
      I&apos;m an AI agent |{' '}
      <span className="landing-journey-title-muted">I am building with one</span>
    </>
  }
  subtitle="Optimize your development workflow by bridging the gap between large language models and high-performance mining infrastructure."
>
  <LandingCards>
    <LandingCard
      accent="teal"
      image="/images/ui-kit/icons/miner-explorer-icon.svg"
      icon={<Bot />}
      title={<span {...cardTitle}>Build dashboards with your AI agent</span>}
      href="/v0-4-0/agents"
      description={
        <span {...cardProse}>
          Wire your LLM to MDK with the UI CLI, then build from plain-language prompts
        </span>
      }
    />
    <LandingCard
      accent="violet"
      image="/images/ui-kit/icons/reporting-nav-icon.svg"
      icon={<FileText />}
      title={<span {...cardTitle}>Full docs in one file</span>}
      href="/v0-4-0/llms-full.txt"
      description={
        <span {...cardProse}>
          Every page of these docs in one plain-text file—open in the browser to copy or save
        </span>
      }
    />
  </LandingCards>
</LandingJourney>

# Build dashboards with your AI agent (/v0-4-0/agents)

MDK is built so your AI coding agent can build mining dashboards straight from plain-language
prompts, without you wiring components by hand. There are two steps: wire your IDE once, then describe what you want.

<Callout type="info" title="Learn more about controlling MDK with agents">

<div className="agents-branch-links not-prose">

<a className="agents-branch-link" data-accent="violet" href="/v0-4-0/tutorials/ui/react/build-any-dashboard-with-an-agent/">
  <span className="agents-branch-link-icon" aria-hidden><Lightbulb size={14} /></span>
  <span className="agents-branch-link-body">
    <span className="agents-branch-link-title">Build any dashboard with an agent</span>
    <span className="agents-branch-link-desc">
      Stats Lab walkthrough with UI CLI commands — no fleet backend
    </span>
  </span>
</a>

<a className="agents-branch-link" data-accent="sky" href="/v0-4-0/concepts/architecture/#ai-agents-and-the-mcp-server">
  <span className="agents-branch-link-icon" aria-hidden><Server size={14} /></span>
  <span className="agents-branch-link-body">
    <span className="agents-branch-link-title">Runtime agents via MCP Server</span>
    <span className="agents-branch-link-desc">
      Monitor and control a live fleet at runtime through the MCP server
    </span>
  </span>
</a>

</div>

</Callout>

## Wire your IDE once

<Steps>

<Step>

### Run init

Point the UI CLI at your IDE from your project root:

```bash
npx @tetherto/mdk-ui-cli init --ide cursor
```

Use `--ide claude` for Claude. This writes a `.mdk/context.md` agent-context file and an IDE rule (`.cursor/rules/mdk.mdc` for
Cursor, `CLAUDE.md` for Claude) so every AI session knows about MDK automatically.

</Step>

<Step>

### Prompt your agent

Describe the dashboard you want in plain language:

> Build me an operations dashboard with live hashrate and a device list.

Your agent takes it from there: it finds the right components and hooks, scaffolds the page, and checks that it compiles. You
review and run it.

</Step>

</Steps>

## What your agent does for you

Behind a single prompt, the agent:

- Finds the right MDK components and hooks for your intent
- Wires real adapter hooks and state, with no guessed imports or props
- Scaffolds a page and verifies that it compiles
- Reaches for stable, supported components by default, so the result is something you can ship

The outcome: the agent pulls real metadata, real examples, and real types instead of hallucinating an API.

## How it works

You do not need any of this to use the tooling, it is here to explain why the results are trustworthy.

<Accordions>

<Accordion title="Why the agent doesn't hallucinate components">

MDK ships a small set of machine-readable files that describe every component, hook, and store. Your agent reads those local
files, makes no network or model calls of its own to discover them, and only reaches for exports that MDK marks as stable. Because
it works from generated metadata rather than guesswork, it does not invent props or imports.

```mermaid
flowchart LR
  prompt["Your prompt"]
  agent["AI agent"]
  manifests["MDK local manifests"]
  page["Scaffolded, verified page"]

  prompt --> agent
  agent --> manifests
  manifests --> agent
  agent --> page
```

For the full command surface those manifests power, see the [UI CLI reference](/v0-4-0/reference/app-toolkit/ui-cli). The complete
contract that keeps the metadata honest lives in the [MDK repositories](/v0-4-0/resources/repositories).

</Accordion>

</Accordions>

## Build any dashboard (example walkthrough)

MDK is not mining-only. This concept page replays the same agent flow above and ships a **statistics lab** dashboard with charts,
tables, and mock data.

## Next steps

- [UI CLI reference](/v0-4-0/reference/app-toolkit/ui-cli): every command your agent (or you) can run
- [Ship a dashboard fast](/v0-4-0/ui/react/quickstart): wire the React packages by hand
- [UI Kit](/v0-4-0/ui): browse the component library your agent draws from
- [AI agents and the MCP Server](/v0-4-0/concepts/architecture#ai-agents-and-the-mcp-server): runtime path for live fleet control

# Code of Conduct (/v0-4-0/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 (/v0-4-0/community/contributing)

# Contribute to MDK

Thank you for your interest in contributing to [MDK](https://github.com/tetherto/mdk/).

This document outlines the contribution workflow for the MDK repository, from setting up your development environment to submitting pull requests and
participating in releases.

## Security

If you discover a security vulnerability, do not report it in a public issue.

Please follow the private disclosure instructions in [SECURITY.md](https://github.com/tetherto/mdk/blob/main/SECURITY.md).

## Monorepo structure

MDK is a monorepo with separate backend and frontend workspaces:

- Backend:
   - `backend/core/`: Backend services, container modules, and integration/unit tests (npm-based)
   - `backend/workers/`: Protocol-translator worker packages (miners, miner-pools, power-meter, temperature, containers), per-worker mock servers, and
per-worker tests (npm-based)
- Frontend: `ui/`: Frontend packages, demo app, and shared UI foundation (npm + Turbo-based)

Choose the backend or frontend workflow that matches the area you are contributing to.

### Root configuration must be domain-aware

The repo top level is a fixed set of domains (`ui/`, `backend/`, `docs/`, `examples/`) plus tooling and repo-meta files. Shared root config (today just `.gitignore`) is read across all of them, so every pattern must be written so it cannot silently match another domain's source:

- **Anchor anything that targets one domain's build or runtime output.** Use `/name/` for the repo root or `domain/**/name/` for a subtree. A bare `status` / `store` / `tmp` / `Checklist*` matches a file or directory of that name *anywhere*, including UI source. That is exactly what caused a prior root ignore regression, where bare `status` / `store` swallowed `ui/packages/ui-core/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

<details>
<summary>1. Fork and clone</summary>

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
```

3. Add the upstream remote:

```bash
git remote add upstream https://github.com/tetherto/mdk.git
```

</details>

<details>
<summary>2. Stay in sync</summary>

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
```

</details>

### 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
```

### 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

### Version tagging

```bash
git checkout main
git pull origin main

git tag -a v1.2.0 -m "Release v1.2.0: Add RTD support"

git push origin main
git push origin v1.2.0
```

### Versioning scheme

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

Happy contributing, and thanks for helping improve MDK! 🚀

# Governance (/v0-4-0/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](/v0-4-0/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.

# About MDK (/v0-4-0/concepts/about)

## Introducing MDK

MDK, the Mining Development Kit, is an [open-source platform](/v0-4-0/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 (ORK)](#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 App Node, the secure gateway your team builds with the SDK. Tying everything together is a **single contract per device type**: the same
[`mdk-contract.json`](/v0-4-0/concepts/stack/workers#capability-contract) serves the UI (data labels), the orchestrator (validation rules), and AI agents (reasoning context).
One file, three audiences, no drift.

### The orchestration kernel

[ORK](/v0-4-0/concepts/stack/ork), the Orchestration Kernel, is distributed as [`@tetherto/mdk-ork`](https://github.com/tetherto/mdk/blob/main/backend/core/ork/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-ork` 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-ork` 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/ork/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-ork`. 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**](/v0-4-0/concepts/stack/app-toolkit) is the optional, batteries-included application
layer that sits on top of `@tetherto/mdk-ork`. It ships in three parts:

- **Frontend tools**: a headless state brain ([`@tetherto/mdk-ui-core`](/v0-4-0/reference/app-toolkit/ui-core)), framework adapters
([`@tetherto/mdk-react-adapter`](/v0-4-0/ui/react/get-started) for React today), and a production-tested React UI Kit
([`@tetherto/mdk-react-devkit`](/v0-4-0/ui/react/get-started)) for dashboards.
- **Backend tools**: a plug-and-play library that drops into Fastify or Express to handle JWT auth, RBAC, and
 command proxying, with hooks for custom routes and aggregations.
- **Plugins**: drop-in modules that pair a frontend tools widget with a backend tools route, so third parties
can ship whole features without forking the App Node.

Using [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) without the App Node is technically possible but not supported by this monorepo — most applications build on the App Node.

## 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`](/v0-4-0/concepts/stack/workers#capability-contract). No involvement from MDK maintainers needed.
- **Software developers**: build custom mining applications in any language, or leverage the
[MDK App Toolkit](/v0-4-0/concepts/stack/app-toolkit)'s frontend and backend tools for rapid development.
- **AI/Automation teams**: [connect intelligent agents](#ai-ready-with-unified-intelligence) that can monitor, diagnose,
and act on device issues autonomously

## Architecture overview

`@tetherto/mdk-ork` is [the kernel](https://github.com/tetherto/mdk/blob/main/backend/core/ork/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:

- **App Node**: the [App Node](/v0-4-0/concepts/stack/app-node) wraps `@tetherto/mdk-client` and adds [authentication](https://github.com/tetherto/mdk/blob/main/backend/core/app-node/README.md#security-model),
  [RBAC](https://github.com/tetherto/mdk/blob/main/backend/core/app-node/README.md#security-model), fleet aggregation, and an HTTP/WebSocket/MCP interface. AI agents drive the fleet through its MCP endpoint
- **MDK App Toolkit**: sits on top of the App Node. Adds a plugin system for declarative route extensions and frontend
  packages ([`@tetherto/mdk-ui-core`](/v0-4-0/reference/app-toolkit/ui-core), 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-ork`, which orchestrates a synchronized view across the fleet.

For the full layer-by-layer view with transports and discovery flows, see the [MDK stack](/v0-4-0/concepts/architecture#mdk-stack) on the
Architecture page.

## AI-ready with unified intelligence

MDK is designed from the ground up for [AI-driven operations](/v0-4-0/concepts/architecture#ai-agents-and-the-mcp-server). Rather than bolting AI on as an afterthought,
intelligence is woven directly into the device definition itself.

In addition to the technical schemas, every device's contract file ([`mdk-contract.json`](/v0-4-0/concepts/stack/workers#capability-contract)) contains:

- **Safety rules**: for example, "Outlet temperature > 85°C requires immediate intervention"
- **Operational constraints**: limits on command frequency, power thresholds, cooling requirements
- **Troubleshooting guides**: if/then recovery steps that AI agents can follow autonomously

This means an AI agent connecting to MDK doesn't need a separate knowledge base or custom prompts per device.
The intelligence travels with the device; the same contract that validates commands and generates dashboards also determines
how AI reasons about that hardware.

## What you can build

- Operational dashboards (hashrate, power, temperature)
- Multisite fleet management with centralized oversight
- Alerts and notifications for critical device events
- Overheating detection and automated remediation
- AI-driven autonomous monitoring and control
- Custom analytics and reporting pipelines
- White-labeled hosted mining platforms
- Third-party device integrations and plugins

## Scaling

MDK [scales](/v0-4-0/concepts/architecture#scaling) naturally without architectural changes:

- **More devices?** Add more Workers. Each Worker owns a specific set of devices, and `@tetherto/mdk-ork` routes commands to
the right one automatically.
- **More sites?** Each physical site runs its own `@tetherto/mdk-ork` instance. A single App Node connects to all of them,
giving you one view across your entire operation.
- **Site isolation**: `@tetherto/mdk-ork` instances are fully independent. A problem at one site has zero impact on any other.

## Next steps

Learn more about:

- [Architecture](/v0-4-0/concepts/architecture)
- [MDK App Toolkit](/v0-4-0/concepts/stack/app-toolkit)
- [Connecting intelligent agents](/v0-4-0/concepts/architecture#ai-agents-and-the-mcp-server)

# Architecture (/v0-4-0/concepts/architecture)

<Callout type="info">
Status: 🚧 MDK is in active development. This page describes the target architecture and may evolve as real-world implementations land.
</Callout>

## How MDK works

MDK is built around a small kernel with one job: route validated commands to whichever Worker owns a device, and pull telemetry
back. Everything else (authentication, business logic, UI, AI agents) sits outside the kernel as composable layers: keeping the kernel
small and the application surface open.

To prevent unbound flexibility from manifesting as system rigidity, the architecture draws a hard line between what is
standardized and what is delegated. It's:

- **Opinionated where needed**: strict transport envelopes, unified JSON schema, unidirectional flows
- **Flexible where it matters**: isolated Workers handle translation logic, enabling integrations without polluting the core
infrastructure

Five layers compose the stack, with strict, unidirectional flows between them. The kernel itself is **ORK**, the
Orchestration Kernel, distributed as `@tetherto/mdk-ork`.

## MDK stack

```mermaid
graph TB
    subgraph consumers ["Layer 1: Consumers"]
        UI["UI / Frontend"]
        AI["AI Agent"]
    end

    subgraph appNode ["Layer 2: App Node"]
        WebApp["HTTP / API Router"]
        MCPServer["MCP Server Endpoint"]
    end

    subgraph orkKernel ["Layer 3: <code>@tetherto/mdk-ork</code>"]
        ORK["<b>ORK</b><br/>• Command routing<br/>• Health monitoring<br/>• Device registry<br/>• Telemetry collection"]
    end

    subgraph workers ["Layer 4: Workers"]
        Workers["WORKERS"]
    end

    subgraph devices ["Layer 5: Physical Devices"]
        Devices["<b>Physical devices</b><br/>• Miners<br/>• Containers<br/>• Sensors"]
    end

    UI -->|"HTTP / WebSocket"| WebApp
    AI -->|"MCP Protocol"| MCPServer
    WebApp -->|"MDK Protocol via @tetherto/mdk-client / HRPC"| ORK
    MCPServer -->|"MDK Protocol via @tetherto/mdk-client / HRPC"| ORK
    Workers -.->|"join known DHT topic"| ORK
    ORK -->|"MDK Protocol: pull (identity / schema / telemetry) + command"| Workers
    Workers -->|"device libs"| Devices

    style consumers fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A
    style appNode fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A
    style orkKernel fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A
    style workers fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A
    style devices fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A
```

The MDK components that compose those layers:

| Component | What it does |
|---|---|
| [`@tetherto/mdk-ork`](#the-ork-kernel) | Central coordination: routes commands, collects telemetry, monitors health |
| [`@tetherto/mdk-client`](#the-sdk) | Universal SDK applications use to talk to `@tetherto/mdk-ork` |
| [MDK Protocol](#the-mdk-protocol) | Standardized message envelope every layer speaks |
| [MDK App Toolkit](/v0-4-0/concepts/stack/app-toolkit) | Optional frontend tools, backend tools, and plugins on top of `@tetherto/mdk-ork` |

## Storage

[Hypercore](https://github.com/holepunchto/hypercore)-backed stores (such as
[Hyperbee](https://github.com/holepunchto/hyperbee)) are recommended across the `@tetherto/mdk-ork`, Worker, and App Node layers.
This choice satisfies all storage requirements without the operational baggage of a centralized database.

## The MDK protocol

The MDK protocol is the contract that crosses every layer of the stack. Workers become reachable — via a
[DHT topic](/v0-4-0/concepts/stack/workers#dht-mode) or [same-machine discovery](/v0-4-0/concepts/stack/workers#local-mode), and `@tetherto/mdk-ork`
initiates every RPC call. Workers issue no callbacks, emit no fan-out events, and make no exceptions to the direction of flow.

<Callout type="info">
For the full [envelope schema](https://github.com/tetherto/mdk/blob/main/backend/core/ork/lib/protocol/envelope.js), [action catalogue](https://github.com/tetherto/mdk/blob/main/backend/core/ork/lib/protocol/actions.js), and
[base command set](https://github.com/tetherto/mdk/blob/main/backend/core/ork/lib/protocol/schemas.js), see the [Protocol reference](https://github.com/tetherto/mdk/blob/main/backend/core/ork/README.md).
</Callout>

### Design principles

- **Transport-agnostic**: identical messages over [in-process calls](https://github.com/tetherto/mdk/blob/main/backend/core/ork/lib/transport/ipc-gateway.js), [Holepunch RPC (HRPC)](https://github.com/tetherto/mdk/blob/main/backend/core/ork/lib/transport/hrpc-gateway.js),
or API calls
- **Strictly unidirectional**: [Workers](/v0-4-0/concepts/stack/workers) never initiate RPC calls to `@tetherto/mdk-ork`; `@tetherto/mdk-ork`
discovers their presence and initiates all subsequent communication downwards (identity, capabilities, telemetry, commands)
- **Generic interface**: the accepted interface is defined dynamically at the Worker level via a self-describing capabilities
schema containing both structure and semantic context for AI agents

### Governance

To maintain structural integrity and contract stability across `@tetherto/mdk-ork`, App Node, and Workers, MDK protocol messages are
governed and strictly validated using [Hyperschema](https://github.com/holepunchto/hyperschema). Hyperschema also aligns
natively with the system's underlying Hyperbee storage.

### Discovery, telemetry, and command flows

```mermaid
sequenceDiagram
    participant W as Worker
    participant DHT as DHT Topic (Hyperswarm)
    participant O as @tetherto/mdk-ork
    participant G as Gateway (App Node / MCP)

    Note over W,O: Worker discovery and registration
    W->>DHT: Joins known topic
    O-->>DHT: Detects new peer connection
    O->>W: identity.request
    W-->>O: identity.response (devices)
    O->>O: Save Worker to registry
    O->>W: capability.request
    W-->>O: capability.response (schema)

    Note over O,W: Telemetry pull loop
    O->>W: telemetry.pull
    W-->>O: metrics and pending commands

    Note over G,W: Command execution
    G->>O: MDK Protocol HRPC envelope
    O->>W: command.request (routed by deviceId)
    W-->>O: command.result
    O-->>G: result
```

## The ORK kernel

[ORK](/v0-4-0/concepts/stack/ork), [`@tetherto/mdk-ork`](https://github.com/tetherto/mdk/blob/main/backend/core/ork/README.md), is the trusted coordination layer at the heart of MDK. It [routes commands](https://github.com/tetherto/mdk/blob/main/backend/core/ork/README.md#commanddispatcher),
[monitors device health](https://github.com/tetherto/mdk/blob/main/backend/core/ork/README.md#healthmonitor), [registers Workers](https://github.com/tetherto/mdk/blob/main/backend/core/ork/README.md#workerregistry), and [pulls telemetry](https://github.com/tetherto/mdk/blob/main/backend/core/ork/README.md#telemetrycollector) — all on a
[pull-only model](https://github.com/tetherto/mdk/blob/main/backend/core/ork/README.md#scheduler), so the kernel cannot be overwhelmed by upstream pressure.

When a command arrives, callers only need to provide a `deviceId`; `@tetherto/mdk-ork` resolves the owning Worker internally via
the [`CommandDispatcher`](https://github.com/tetherto/mdk/blob/main/backend/core/ork/README.md#commanddispatcher) and dispatches the `command.request`.

## Workers
[Workers](/v0-4-0/concepts/stack/workers) wrap a device library and expose it via the MDK protocol. They are the integration handlers between physical hardware
and `@tetherto/mdk-ork`, and the unyielding source of truth for that hardware: `@tetherto/mdk-ork` itself operates purely as a synchronized state
machine over Worker-reported state.

Workers are passive — ORK initiates every RPC call; Workers only ever respond. ORK discovers Workers according to the
[discovery model](/v0-4-0/concepts/stack/workers#discovery-model), then requests identity and capabilities.

## The SDK

The [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) SDK is the transport abstraction layer used to connect to `@tetherto/mdk-ork` reliably.
It is the essential glue between the kernel and any consumer layer developers choose to build on top.

**Responsibility**: connects the MDK Protocol over native transports (HRPC or IPC) seamlessly, offering:

- **Transport abstraction**: handles MDK Protocol message construction and reconnection logic with exponential backoff.
- **Automatic transport selection**: the SDK picks the transport mechanism based entirely on the URL scheme provided by the
developer.
  - `hrpc://` connects over encrypted Hyperswarm streams for remote server-to-server production.
  - `ipc://` connects via direct local sockets for low-latency local testing.
- **Major language support**: `@tetherto/mdk-client` is intended to support all major languages (Node.js, Python, Go, and others), allowing
developers to dispatch commands, subscribe to live streams, or pull status snapshots from any stack.

## App Node

The [App Node](https://github.com/tetherto/mdk/blob/main/backend/core/app-node/README.md) wraps `@tetherto/mdk-client` — the MDK protocol connector to ORK — to add an authenticated
HTTP, WebSocket, and MCP interface on top. Consumers that need those capabilities connect through the App Node.

The supported development path is the [MDK App Toolkit](/v0-4-0/concepts/stack/app-toolkit), which ships backend middleware (JWT auth, RBAC, and command
proxying), frontend tools, and an `mdk-plugin.json`-based plugin system for declarative HTTP route extensions
([plugin guide](/v0-4-0/how-to/app-node/plugins)).

For the full developer model — extension patterns, data access, auth design, and ORK connection — read the [App Node concept page](/v0-4-0/concepts/stack/app-node).

## AI agents and the MCP server

AI agents connect to MDK through an **MCP endpoint** on the App Node, not directly to `@tetherto/mdk-ork`. By routing agents through the App Node,
developers can keep them inside the same security envelope as every other consumer: they are ordinary authenticated clients; subject to the same JWT validation, rate limits, and RBAC as a human user. This is intentional: the kernel does not perform user-level [authentication](/v0-4-0/concepts/stack/app-node#authentication-design).

What makes the integration distinctive is **[runtime tool derivation](https://github.com/tetherto/mdk/blob/main/docs/reference/maintainers/agent-ready-sdk.md)**. The tools exposed to an agent (for example,
`get_device_telemetry` or `reboot_device`) are not hardcoded; they are parsed at runtime from each registered Worker's
[`mdk-contract.json`](/v0-4-0/concepts/stack/workers#capability-contract). When a new device type joins the network, the agent gains
the ability to query and control it without any change to the App Node.

## End-to-end data flows

Two scenarios show the full request path from consumer to device and back: a [human user clicking through the UI](#human-ui-scenario), and an [AI
agent executing a multi-step prompt](#ai-agent-scenario).

### AI agent scenario

A user instructs the AI Agent: *"Keep the fleet healthy."* The agent monitors continuously, catches `wm002` overheating, reboots it, and notifies the user.

```mermaid
sequenceDiagram
    actor User
    participant AI as AI Agent
    participant Node as App Node (MCP)
    participant ORK as @tetherto/mdk-ork
    participant Worker as Generic Worker

    User->>AI: "Keep the fleet healthy."

    Note over AI,ORK: Step 1: Fleet discovery (read)
    AI->>Node: Call MCP tool get_fleet_alerts (token auth)
    Node->>Node: Validate agent token and RBAC
    Node->>ORK: HRPC query (via @tetherto/mdk-client)
    ORK-->>Node: Metrics
    Node-->>AI: Tool result (wm002 is overheating)

    Note over AI,ORK: Step 2: Execution (write)
    AI->>Node: Call MCP tool reboot_device (deviceId wm002)
    Node->>Node: Validate token and device:write RBAC
    Node->>ORK: dispatch generic protocol message
    ORK->>ORK: Resolve deviceId
    ORK->>Worker: command.request (HRPC)
    Worker-->>ORK: command.result
    ORK-->>Node: result OK
    Node-->>AI: Tool result (Success)

    AI-->>User: "wm002 was overheating and has been rebooted."
```
### Human UI scenario

A user clicks "Reboot" on device `wm001` in the UI.

```mermaid
sequenceDiagram
    actor User
    participant UI as React UI
    participant Node as App Node
    participant ORK as @tetherto/mdk-ork
    participant Worker as Generic Worker

    User->>UI: Click "Reboot" on wm001
    UI->>Node: POST { `deviceId`, action, payload }

    Note over Node,ORK: Delegation
    Node->>ORK: dispatch generic protocol message
    ORK->>ORK: Verify against capabilities
    ORK->>ORK: Resolve Worker for `deviceId`

    Note over ORK,Worker: Execution
    ORK->>Worker: command.request (HRPC)
    Worker-->>ORK: Ack start
    Worker->>Worker: Hardware-specific translation
    Worker-->>ORK: command.result

    ORK-->>Node: result OK
    Node-->>UI: HTTP 200

    Note over Worker,ORK: State reflection
    ORK->>Worker: telemetry.pull (tick)
    Worker-->>ORK: Updated status (rebooting)
```

## Scaling

As MDK deployments scale to large mining sites (5,000+ devices), the system must explicitly manage parallel Workers and parallel
`@tetherto/mdk-ork` instances. The kernel is only an execution layer; it does not perform application-level aggregation or
cross-regional business logic.

<Callout type="info">
Scaling here means *how many* Workers and kernels you run. That is independent of [deployment topology](/v0-4-0/concepts/deployment-topologies) —
</Callout>

*how those processes are packaged* on a host (one process vs many).

### Parallel Workers

Multiple Workers of the same type (for example, `whatsminer-worker`) can be active concurrently and connected to the same
`@tetherto/mdk-ork` kernel.

```mermaid
flowchart TD
    subgraph kernel ["Single @tetherto/mdk-ork kernel"]
        ORK["ORK"]
    end

    W1["Worker 1"]
    W2["Worker 2"]
    D1["Devices wm001 to wm500"]
    D2["Devices wm501 to wm999"]

    ORK -->|Routes commands| W1
    ORK -->|Routes commands| W2
    W1 --- D1
    W2 --- D2

    style kernel fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A
```

**Device-level routing and ownership**: Workers never share devices. When a Worker connects, its `identity.register` payload
explicitly lists the `deviceId`s it exclusively manages. The Worker registry maintains this strict mapping and deterministically
routes arriving commands to the designated Worker.

### Multi-site deployments

A deployment may need to manage multiple massive physical boundaries (for example, a Texas Site and an Iceland Site). Each
location runs its own dedicated site-level `@tetherto/mdk-ork` kernel, but all are overseen globally by a single App Node and AI Agent.

```mermaid
flowchart TD
    Global["Global App Node / AI Agent"]

    subgraph texas ["Texas site"]
        ORK_TX["@tetherto/mdk-ork"]
        W1_TX["Whatsminer Worker"]
        W2_TX["Antminer Worker"]
        D1_TX["Whatsminers"]
        D2_TX["Antminers"]
        ORK_TX -->|Routes| W1_TX
        ORK_TX -->|Routes| W2_TX
        W1_TX --- D1_TX
        W2_TX --- D2_TX
    end

    subgraph iceland ["Iceland site"]
        ORK_IC["@tetherto/mdk-ork"]
        W1_IC["Whatsminer Worker"]
        W2_IC["Avalon Worker"]
        D1_IC["Whatsminers"]
        D2_IC["Avalons"]
        ORK_IC -->|Routes| W1_IC
        ORK_IC -->|Routes| W2_IC
        W1_IC --- D1_IC
        W2_IC --- D2_IC
    end

    Global <-->|MDK Protocol via HRPC| ORK_TX
    Global <-->|MDK Protocol via HRPC| ORK_IC

    style texas fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A
    style iceland fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A
```

The single App Node and AI Agent connect globally to all distributed `@tetherto/mdk-ork` kernels via the native HRPC mesh (Hyperswarm).
Parallel `@tetherto/mdk-ork` instances remain entirely isolated from one another: they do not federate registries, share queues, or
synchronize state. A crash at one site has zero impact on any other.

Cross-site aggregation is handled purely at the App Node layer, where routes query multiple Workers via `@tetherto/mdk-ork` and merge
the responses before returning them to the UI or Agent.

## Next steps

Learn more about:

- [About MDK](/v0-4-0/concepts/about)
- [MDK App Toolkit](/v0-4-0/concepts/stack/app-toolkit)
- [ORK](/v0-4-0/concepts/stack/ork)
- [Get started](/v0-4-0/get-started)

# Deployment topologies (/v0-4-0/concepts/deployment-topologies)

This page explains the three supported deployment shapes and when to pick each.

## Overview

MDK's runtime pieces — the [ORK kernel](/v0-4-0/concepts/architecture), the App Node, and one or more Workers — can run together
in a single process or be split across several. This is a **packaging and operations** choice, and it's
independent of how MDK [scales logically](/v0-4-0/concepts/architecture#scaling) (adding Workers, adding sites).

<Callout type="info">
If ORK, worker, manager, or thing are unfamiliar, read [`terminology.md`](/v0-4-0/concepts/terminology) first.
</Callout>

## Connection model

Before choosing a shape, it helps to understand which components initiate connections:

- The App Node dials ORK — it is the active side of that connection, over IPC (same host) or HRPC (remote host)
- ORK discovers Workers and initiates every RPC call — Workers are passive; they become reachable and wait
- Workers never initiate any connection

This directionality is what drives the transport and discovery configuration in each shape below.
For detail, see the [Workers discovery model](/v0-4-0/concepts/architecture#workers) and the [App Node ORK connection](/v0-4-0/concepts/stack/app-node#ork-connection).

## The three shapes

### Single process

```mermaid
flowchart LR
  classDef mdk fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A
  sApp["App Node"]:::mdk -->|"IPC"| sOrk["ORK"]:::mdk
  sOrk -.->|"in-process"| sW1["Worker A"]:::mdk
  sOrk -.->|"in-process"| sW2["Worker B"]:::mdk
```

*Solid arrow: active connection initiated by the source. Dashed arrow — ORK-initiated discovery.*

ORK, the App Node, and every Worker run inside one Node.js heap and event loop. Lowest footprint, simplest to start, nothing external to supervise. This is the shape behind the [single-process site how-to](/v0-4-0/how-to/deployment/run-single-process-site).

### Local

```mermaid
flowchart LR
  classDef mdk fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A
  lApp["App Node"]:::mdk -->|"IPC"| lOrk["ORK"]:::mdk
  lOrk -.->|"shared dir"| lW1["Worker A"]:::mdk
  lOrk -.->|"shared dir"| lW2["Worker B"]:::mdk
```

*Solid arrow: active connection initiated by the source. Dashed arrow — ORK-initiated discovery.*

Each service runs as its own OS process on the same machine. ORK discovers Workers via a shared directory — no DHT configuration needed. The [full-site example](https://github.com/tetherto/mdk/blob/main/examples/full-site/README.md#how-out-of-process-workers-find-the-ork) runs in local mode by default.

### Microservices

```mermaid
flowchart LR
  classDef mdk fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A
  mApp["App Node (host 1)"]:::mdk -->|"HRPC"| mOrk["ORK (host 2)"]:::mdk
  mOrk -.->|"DHT"| mW1["Worker A (host 3)"]:::mdk
  mOrk -.->|"DHT"| mW2["Worker B (host N)"]:::mdk
```

*Solid arrow: active connection initiated by the source. Dashed arrow — ORK-initiated discovery.*

Each service runs as its own OS process or container, potentially on separate hosts, supervised by pm2 or Docker and connected via DHT. This is the shape behind the [microservices site how-to](/v0-4-0/how-to/deployment/run-microservices-site).

## The trade-off

Pick **single-process** when:

- You are developing locally, running demos, or want a self-contained site for tests
- Footprint matters more than isolation (minimal or embedded deployments)
- You do not need supervisor-managed restarts

Pick **local** when:

- All services run on one machine and you want independent process restarts
- Outbound networking is restricted removing DHT as an option
- You want process isolation and independent restarts without the complexity of DHT

Pick **microservices** when:

- You want to allocate resources per service — CPU and memory limits per process or container
- Workers run on separate hosts from ORK or the App Node
- You are orchestrating many Workers across one or more hosts

## Where `worker.js` fits

The microservices shape is built on [`backend/core/mdk/worker.js`](https://github.com/tetherto/mdk/blob/main/backend/core/mdk/worker.js), a shared process entry compatible with pm2, Docker, or a direct `node worker.js`. It is driven by environment variables (`SERVICE`, and for a Worker `WORKER`/`TYPE`/`RACK`) rather than CLI flags. One `worker.js` runs per service, and the supervisor (pm2 or Docker) owns its lifecycle and resource limits. The [standalone `worker.js` install pattern](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/install-pattern.md#standalone-via-workerjs) defines the per-Worker mechanics.

The single-process and local shapes both call the programmatic APIs (`getOrk`, `startWorker`, `startAppNode`) directly. Local mode passes `discovery: { mode: 'local' }` to both `getOrk` and `startWorker` so they coordinate via a shared directory rather than DHT — see [local worker discovery](/v0-4-0/concepts/stack/workers#local-mode) for configuration options.

## Relationship to scaling

Topology is orthogonal to scale. [Logical scaling](/v0-4-0/concepts/architecture#scaling) is about *how many* Workers and ORK kernels you run (parallel Workers, per-site kernels, multi-site oversight). Deployment topology is about *how those processes are packaged* on a given host. You choose both: for example, a production site typically runs multiple processes (this page) and multiple parallel Workers per kernel ([scaling](/v0-4-0/concepts/architecture#scaling)).

## Next steps

- Run a self-contained local site: [Single-process site](/v0-4-0/how-to/deployment/run-single-process-site)
- Run [same-machine services without DHT](/v0-4-0/concepts/stack/workers#local-mode)
- Run [supervised services on one or more hosts](/v0-4-0/how-to/deployment/run-microservices-site)
- Register [one miner before packaging a whole site](/v0-4-0/how-to/miners)

# Stack (/v0-4-0/concepts/stack)

MDK's backend is composed of four coordinated layers. Each layer has a single, bounded responsibility; together they form a complete path from physical device to application consumer.

| Layer | What it owns |
| --- | --- |
| [Workers](/v0-4-0/concepts/stack/workers) | Device integration — translate hardware telemetry and commands into the MDK Protocol |
| [ORK](/v0-4-0/concepts/stack/ork) | Kernel — route commands, monitor health, register Workers, and pull telemetry |
| [App Node](/v0-4-0/concepts/stack/app-node) | Application gateway — authenticated HTTP, WebSocket, and MCP interface on top of ORK |
| [MDK App Toolkit](/v0-4-0/concepts/stack/app-toolkit) | Development toolkit — App Node backend, plugin system, and frontend packages |

For the architecture overview and how data flows between layers, see [Architecture](/v0-4-0/concepts/architecture).

# App Node (/v0-4-0/concepts/stack/app-node)

## Overview

This page introduces the [App Node](https://github.com/tetherto/mdk/blob/main/backend/core/app-node/README.md) surface. It explains what concerns it owns, how to extend it with plugins and routes,
how data flows from ORK to your controllers, and why authentication lives here rather than in the kernel.
Read this before building [plugins](/v0-4-0/how-to/app-node/plugins), auth flows, or aggregation routes on top of MDK.

<Callout type="info">
The App Node is the backend layer of the [MDK App Toolkit](/v0-4-0/concepts/stack/app-toolkit), which aligns the [plugin system](/v0-4-0/how-to/app-node/plugins)
and [frontend packages](/v0-4-0/concepts/stack/app-toolkit) into the supported development path for this monorepo.
</Callout>

## What the App Node owns

The App Node wraps [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) — the MDK protocol connector to ORK — and adds an authenticated HTTP,
WebSocket, and MCP interface on top. Consumers connect through the App Node; using `@tetherto/mdk-client` without the App Node
is not supported by this monorepo.

The App Node owns three concerns that ORK deliberately **does not** handle:

- Authentication and RBAC: JWT validation, session management, OAuth2 (Google and Microsoft built in), and role-based access before any request reaches ORK
- API surface: REST endpoints, WebSocket telemetry subscriptions, command dispatch, and the MCP endpoint for AI agents
- Fleet aggregation: cross-worker queries that compute site hashrate, average temperature, and cross-rack efficiency — resolved in controller code, not in ORK

[ORK](/v0-4-0/concepts/stack/ork) is a pass-through kernel. It routes commands to [Workers](/v0-4-0/concepts/architecture#workers), collects telemetry, and maintains
the device registry. Everything above the kernel — authentication, business logic, API surface — is owned by the caller:
the App Node (which wraps [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) internally) when using the toolkit.

## Extension model

The App Node offers two ways to add routes, in order of preference.

### 1. Plugin system

The recommended path. A plugin is a directory with an `mdk-plugin.json` manifest and one or more controller files.
Pass the directory path to `startAppNode()` via `extraPluginDirs`.

Controllers receive a `services` bag on every request — `mdkClient`, `dataProxy`, `authLib`, and `conf` — with no protocol knowledge required.
The [default plugins](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md#default-plugins) (`auth`, `telemetry`, `site-hashrate`) load the same way as any plugin you write.

The [plugin authoring guide](/v0-4-0/how-to/app-node/plugins) covers the build process end to end.
The [plugin reference](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md) documents the manifest schema, controller contract, and loader errors.

### 2. Raw Fastify routes

For one-off handlers that do not need a manifest, pass `additionalRoutes` to `startAppNode()`. These are plain Fastify route objects —
no `services` injection, no manifest validation, no auth wiring. Use this path sparingly; a plugin is easier to test in isolation
and easier for a later maintainer to follow.

## Use an alternative gateway

If your use case does not need the App Node's HTTP surface, RBAC, or plugin system — for example, a background service that only
dispatches commands — you can use [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) directly against ORK without running the App Node at all.
This is the direct path. Such an approach is not directly supported by this monorepo, as most applications build on the App Node.

## Data access

Two services are available inside every plugin controller.

**`services.mdkClient`** gives access to live ORK data: pull a telemetry snapshot, dispatch a command, list registered workers.
It's `null` when the App Node starts without a live ORK connection, so guard it before use.

**`services.dataProxy`** reads from persisted worker tail-logs: time-series aggregation, historical hashrate, efficiency trends.
Use this for data that does not require a live ORK round-trip.

The split exists because the two sources have different latency and availability characteristics. `mdkClient` calls are network
operations that can fail if ORK is unreachable. `dataProxy` reads from local storage and remains available whether ORK is online or not.

## Authentication design

The App Node validates a JWT Bearer token before proxying any request to ORK. By design, ORK does not perform user-level authentication.
For IPC (same-host), the socket itself provides implicit trust and no key exchange is required.
For HRPC (remote ORK), [ORK maintains an allowlist](https://github.com/tetherto/mdk/blob/main/backend/core/ork/README.md#transports); pre v1.0 it is opt-in (the default `auth.whitelist` is empty and
admits any caller), but when configured the App Node's DHT public key must be added before the connection is accepted.
Either way, once the transport is established, ORK trusts all messages from the App Node without inspecting user identity.

User authentication and RBAC are entirely the App Node's responsibility. RBAC is enforced at the route level via the `permissions`
field in `mdk-plugin.json`. Routes with `"auth": false` are public — no JWT is required. Routes with `"auth": true` but no `permissions`
array are accessible to any authenticated user.
[A `permissions` array](/v0-4-0/how-to/app-node/plugins#auth-permissions-and-caching) restricts access further to users with matching roles.

## ORK connection

The App Node is the **active** side of this connection — it dials ORK. [ORK](/v0-4-0/concepts/stack/ork) is the passive listener; it does not
initiate contact with the App Node.

Two transports are available:

- **IPC** (default): App Node dials ORK over a Unix socket on the same host. Low-latency, implicit trust — no allowlisting required.
  The default for single-host deployments
- **HRPC**: App Node dials ORK over encrypted Hyperswarm streams. The App Node's DHT public key must be added to
  [ORK's `auth.whitelist`](https://github.com/tetherto/mdk/blob/main/backend/core/ork/README.md#transports) before the connection is accepted. Used when ORK and the App Node run on separate hosts

<Callout type="info">
Pre v1.0, the allowlist is opt-in. ORK's `auth.whitelist` defaults to empty, which admits any HRPC caller. When an allowlist
is configured, the App Node's DHT public key must appear in it before ORK accepts the connection.
</Callout>

## Next steps

- [Run the App Node for the first time](/v0-4-0/how-to/app-node/run)
- [Add routes with the plugin system](/v0-4-0/how-to/app-node/plugins)
- Review the [full API and configuration reference](https://github.com/tetherto/mdk/blob/main/backend/core/app-node/README.md)
- Choose a [deployment shape](/v0-4-0/concepts/deployment-topologies)

# MDK App Toolkit (/v0-4-0/concepts/stack/app-toolkit)

## Overview

The MDK App Toolkit is the recommended development path for teams building MDK-powered applications. It is composed of
three coordinated layers:

- App Node backend
- Plugin system
- Frontend packages

Not every layer is required for every consumer type.

MDK supports two primary consumer patterns:

- **Human operator UI**: a frontend application connects to the App Node's REST and WebSocket APIs. The full three-layer
  toolkit applies — App Node, plugin system, and frontend packages
- **AI agent / headless consumer**: an AI agent connects to the App Node's MCP endpoint and subscribes to telemetry feeds
  directly. The frontend packages are not required; the App Node and plugin system
  alone are sufficient

<Callout type="info">
Using [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) without the App Node runtime is technically possible — you write your own auth,
routing, and middleware — but it is not supported by this monorepo. Most applications build on the App Node.
</Callout>

## App Node layer

`@tetherto/mdk-app-node` is the backend component of the toolkit. It wraps [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) — the ORK protocol connector —
and delivers an authenticated HTTP, WebSocket, and MCP interface for consumers that need those capabilities. Read the
[App Node concept page](/v0-4-0/concepts/stack/app-node) for the full developer model: extension patterns, data access, auth design, and ORK connection.

As a toolkit component, the App Node provides out of the box:

- Fastify-based HTTP server and WebSocket endpoint
- JWT authentication, session management, and OAuth2 (Google and Microsoft)
- RBAC enforcement at the route level
- Command proxying and telemetry subscriptions to ORK via `@tetherto/mdk-client`
- MCP endpoint for AI agents

## Plugin system

`@tetherto/mdk-plugins` is the extension mechanism. A plugin is a directory containing an [`mdk-plugin.json`](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md#manifest-format) manifest and one or
more controller files. The App Node discovers and loads plugins from directories passed via [`extraPluginDirs`](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md#mounting-plugins).

The toolkit ships defaults plugins, e.g., `auth` (user authentication routes), `telemetry` (hashrate, efficiency, temperature
metrics), and `site-hashrate` (aggregated site history). Any plugin you write loads identically.

<Callout type="idea">
- [Plugin authoring guide](/v0-4-0/how-to/app-node/plugins) — build process, manifest schema, controller contract
- [Plugin reference](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md) — manifest schema, default routes, loader errors
</Callout>

## Frontend packages

These packages are for the **human operator UI** pattern — the application layer that connects to the App Node's REST and
WebSocket APIs. If your consumer is an AI agent connecting via the MCP endpoint, this layer is not required.

<Callout type="info">
Early versions of MDK ship three layered workspace packages within the monorepo.
npm packages will be published as the tooling matures.
</Callout>

Consuming applications add the workspace dependencies directly. Consuming the whole chain is the recommended path for operator UIs.

<Callout type="idea">
The [UI architecture reference](https://github.com/tetherto/mdk/blob/main/ui/docs/ARCHITECTURE.md) covers the full dependency graph, build strategy, and package internals.
</Callout>

**[`@tetherto/mdk-ui-core`](https://github.com/tetherto/mdk/blob/main/ui/packages/ui-core/README.md)**: framework-agnostic headless core. No React imports. Provides Zustand vanilla stores
(`authStore`, `devicesStore`, `notificationStore`, `timezoneStore`, `actionsStore`), a TanStack `QueryClient` factory with
environment-aware base URL resolution, and shared type contracts.

**[`@tetherto/mdk-react-adapter`](https://github.com/tetherto/mdk/blob/main/ui/packages/react-adapter/README.md)**: React bindings for the core. Provides `<MdkProvider apiBaseUrl={...}>`
(required at the app root) and store hooks (`useAuth`, `useDevices`, `useTimezone`, `useNotifications`, `useActions`).

**[`@tetherto/mdk-react-devkit`](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/README.md)**: React UI library. `src/core/` ships generic UI primitives built on Radix UI
(Button, Dialog, Switch, Select, Data Table, Charts). `src/foundation/` ships mining-domain components, features, and presentation hooks.

### Developer entry points

The toolkit can be adopted at any of the following entry points, from most batteries-included to least.

| Entry point | Package | What ships | What you write | When to choose |
|---|---|---|---|---|
| UI Kit | `@tetherto/mdk-react-devkit` (`/core` + `/foundation` entrypoints) | Pre-built React components, shell layout, ready-made ops dashboard | Data wiring, optional theming | You want a dashboard up fast |
| Framework adapter | `@tetherto/mdk-react-adapter` (React today; Vue/Svelte/WC planned) | `<MdkProvider>`, store hooks, TanStack Query re-exports | Your own components and layout | You have a design system already |
| UI Core | [`@tetherto/mdk-ui-core`](/v0-4-0/reference/app-toolkit/ui-core) | Zustand vanilla stores, `QueryClient` factory | Framework bindings or headless utilities | You need store access outside React or are building a new adapter |
| Raw SDK | `@tetherto/mdk-client` | MDK Protocol client, connection management, reconnection | Everything above the wire: state, framework, UI | You are building a non-UI consumer (CLI, agent, backend service) |

## Architecture overview

```mermaid
flowchart TD
    subgraph frontend ["Frontend packages"]
        direction TB
        UI_CORE["@tetherto/mdk-ui-core (headless stores)"]
        FRAMEWORKS["@tetherto/mdk-react-adapter (React bindings)"]
        UI_COMPS["@tetherto/mdk-react-devkit (UI Kit)"]

        UI_COMPS -->|consumes adapter hooks| FRAMEWORKS
        FRAMEWORKS -->|binds headless stores| UI_CORE
    end

    subgraph backend ["App Node + plugins (server)"]
        direction TB
        PLUGINS["@tetherto/mdk-plugins (default + custom routes)"]
        ROUTER["@tetherto/mdk-app-node (HTTP / WS / MCP)"]
        CLIENT["@tetherto/mdk-client (protocol connector)"]

        PLUGINS -->|registers routes into| ROUTER
        ROUTER -->|proxies to ORK via| CLIENT
    end

    UI_CORE <-->|"HTTP / WebSocket"| ROUTER
    CLIENT -->|"MDK Protocol"| ORK["@tetherto/mdk-ork (kernel)"]

    style frontend fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A
    style backend fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A
```

## Next steps

- Understand the [App Node surface](/v0-4-0/concepts/stack/app-node)
- [Build or extend with the plugin system](/v0-4-0/how-to/app-node/plugins)
- Explore the [frontend package architecture](https://github.com/tetherto/mdk/blob/main/ui/docs/ARCHITECTURE.md)

# ORK (/v0-4-0/concepts/stack/ork)

## Overview

[`@tetherto/mdk-ork`](https://github.com/tetherto/mdk/blob/main/backend/core/ork/README.md) is the trusted coordination kernel at the heart of MDK. It routes commands, monitors device
health, registers Workers, and pulls telemetry — without performing user authentication, business logic, or aggregation.

ORK is a pass-through kernel: it receives commands from any caller using [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) — most commonly
the [App Node](/v0-4-0/concepts/stack/app-node) — and dispatches them to [Workers](/v0-4-0/concepts/stack/workers); it pulls telemetry from Workers and routes
it back to callers. Everything else is the caller's responsibility.

## What ORK owns

ORK is decomposed into six single-responsibility modules. Modules communicate only through their declared interfaces.

**`WorkerRegistry`**: maps `deviceId → workerId → RPC channel`. Source of truth for worker-to-device routing. Workers progress
through a state machine as ORK discovers and registers them — Unregistered → Discovered → IdentitySaved → Ready → Terminated.

**`CommandDispatcher`**: validates incoming command envelopes, resolves the owning worker from the registry, checks that the
command exists in the worker's declared capabilities, then passes it to the state machine.

**`CommandStateMachine`**: tracks every command's full execution lifecycle. Backed by a Write-Ahead Log (WAL) in Hyperbee —
every state transition is persisted before it takes effect. On restart, `recover()` sweeps non-terminal states and retries or
fails them — QUEUED → DISPATCHED → EXECUTING → SUCCESS (or FAILED / TIMEOUT).

**`TelemetryCollector`**: stateless proxy. Routes `telemetry.pull` queries to the appropriate worker and passes the response
back to the caller. Workers own all aggregation and storage — ORK is a thin router.

**`Scheduler`**: system metronome. Runs non-overlapping interval jobs for telemetry pulls, health pings, and state pulls on
configurable cadences. Jobs are idempotent — safe to restart with no state loss.

**`HealthMonitor`**: ping-based liveness checker. Sends `health.ping` to every registered worker on a configurable cadence
and updates the registry — UNKNOWN → HEALTHY → SICK → DEAD (with reconnect path back to HEALTHY).

## The pull-only model

ORK never receives unsolicited data from Workers. It always initiates — pulling telemetry, pinging health, and pulling state on
cadences set in `opts.cadences`. Workers become reachable and wait; ORK reaches out on its own schedule.

This is what prevents the kernel from being overwhelmed by upstream pressure and is why Workers are described as passive.
Callers — typically the [App Node](/v0-4-0/concepts/stack/app-node#ork-connection) — do send command requests to ORK (ORK is the receiver for those),
but ORK then dispatches each command to the owning Worker via its own initiated call.

## Transports

ORK is the **passive listener** on both transports — the [caller always initiates the connection](/v0-4-0/concepts/stack/app-node#ork-connection).

<Callout type="idea">
The [deployment topologies connection model](/v0-4-0/concepts/deployment-topologies#connection-model) details the active/passive components.
</Callout>

- **IPC** (default): the caller/App Node dials ORK over a Unix socket on the same host. Implicit trust — no allowlist required.
  The default for single-host and development deployments
- **HRPC**: the caller/App Node dials ORK over encrypted Hyperswarm Noise streams. ORK maintains an allowlist — the caller/App Node's DHT
  public key must be added to `opts.auth.whitelist` before the connection is accepted. Used for remote or multi-host deployments

<Callout type="idea">
The [ORK transport reference](https://github.com/tetherto/mdk/blob/main/backend/core/ork/README.md#transports) covers the allowlist key exchange and configuration options.
</Callout>

## What ORK does not own

ORK deliberately excludes these concerns and delegates them to other layers:

- **User authentication and RBAC**: JWT validation, session management, and role-based access are the App Node's responsibility.
  ORK trusts all messages from any established [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) connection without inspecting user identity.
  The App Node is the only caller that enforces RBAC before reaching ORK; using `@tetherto/mdk-client` without the App Node
  carries no access control layer
- **Business logic and aggregation**: cross-worker queries, fleet statistics, and site-level aggregation belong in App Node
  controllers, not in the kernel
- **UI and consumer interfaces**: ORK has no HTTP surface. Consumers connect through the App Node's REST, WebSocket, or MCP
  endpoints

## Next steps

- Understand the [App Node's role as ORK's consumer](/v0-4-0/concepts/stack/app-node)
- Understand [Workers as ORK's downstream](/v0-4-0/concepts/stack/workers)
- Choose a [deployment shape](/v0-4-0/concepts/deployment-topologies)
- Review the full [ORK API and configuration](https://github.com/tetherto/mdk/blob/main/backend/core/ork/README.md)

# Workers (/v0-4-0/concepts/stack/workers)

## Overview

This page introduces the [Worker](https://github.com/tetherto/mdk/blob/main/backend/workers/base/README.md) as a development component. It explains what a Worker owns, how ORK discovers it,
what the capability contract is, and how to build a Worker for new hardware.
Read this before integrating new hardware, configuring discovery, or building on top of the Worker protocol.

## What a worker owns

A Worker wraps a device library and exposes it to ORK via the MDK Protocol. Workers are the integration handlers between physical
hardware and `@tetherto/mdk-ork`, and the unyielding source of truth for that hardware. `@tetherto/mdk-ork` operates purely as
a synchronized state machine over Worker-reported state — it never reads hardware directly.

Workers are **passive**: they become a reachable endpoint and wait. ORK initiates every RPC call; Workers only ever respond.
See the [deployment topologies connection model](/v0-4-0/concepts/deployment-topologies#connection-model) for how this directionality shapes transport choices.

## Discovery model

How ORK finds a Worker depends on whether they share a machine.

<Callout type="info">
In all cases, the post-discovery sequence is identical — ORK requests identity, registers the Worker, then queries its
capabilities. The full sequence and state machine are described in the [ORK — What ORK owns](/v0-4-0/concepts/stack/ork#what-ork-owns) section.
</Callout>

| Mode | How ORK finds the worker | When to use |
| --- | --- | --- |
| **DHT** | Worker joins a Hyperswarm DHT topic; ORK listens on the same topic and connects automatically | [Production microservices](/v0-4-0/how-to/deployment/run-microservices-site), workers on separate hosts or networks |
| **Local** | Worker publishes itself to a shared directory; ORK watches the directory: no DHT needed | All components on one machine, restricted outbound networking |
| **Same-process** | `startWorker(W, { ork })` passes the ORK instance directly: no network lookup | [Getting started](/v0-4-0/tutorials/backend-stack/run), [single-process sites](/v0-4-0/how-to/deployment/run-single-process-site) |

<Callout type="info">
Discovery is a startup concern only — it determines how ORK obtains the worker's RPC public key, nothing more. Once connected,
all three modes use the same HyperswarmRPC transport and the same MDK Protocol envelope (`command.request`, `telemetry.pull`, and so on).
Local and same-process modes route traffic over the local network interface; DHT mode routes over the public internet.
The available commands, telemetry, and operations are identical in all three.
</Callout>

### DHT mode

In multi-process DHT mode, ORK and the worker must join the same Hyperswarm topic. Two options:

**Auto-generated (default)** — `startWorker()` generates a random 32-byte hex topic, writes it to `os.tmpdir()/mdk/.dht-topic`,
and joins the DHT; `getOrk()` reads the same file and joins the matching topic.

<Callout type="warn">
The auto-generated topic file is local to the machine or container that writes it. When ORK and the worker run in separate
containers or on separate hosts they cannot share this file and discovery stops responding. Use an explicit topic in any
multi-host or multi-container setup.
</Callout>

**Explicit topic** — pass the same value to both calls directly:

```js
const ork = await getOrk({ topic: '<32-byte-hex>' })
const { manager } = await startWorker(WorkerClass, { orkTopic: '<32-byte-hex>' })
```

<Callout type="warn">
The worker must join the topic before ORK starts listening. Start the worker process first, then start ORK.
`waitForDiscovery()` polls the registry until discovered workers reach `READY` state.
</Callout>

The DHT pattern is demonstrated end-to-end in [`dht-worker.js`](https://github.com/tetherto/mdk/blob/main/examples/backend/mdk-e2e/dht-worker.js) and [`dht-ork.js`](https://github.com/tetherto/mdk/blob/main/examples/backend/mdk-e2e/dht-ork.js).

### Local mode

In local mode, ORK and workers coordinate through a shared directory on the same machine (default `<root>/.worker-keys/`).
No Hyperswarm topic is joined and no outbound internet connection is required.

**Worker side**: on startup, `startWorker` publishes the worker's identity to the shared directory. The entry is stable across
restarts, so restarting a worker is a no-op from ORK's perspective.

**ORK side**: `getOrk` watches the directory with `fs.watch` and runs a full scan every four seconds. Each entry found triggers
the normal discovery listener (Identity → Capability → Ready), the same sequence used in DHT mode.

```js
const ork = await getOrk({ discovery: { mode: 'local' } })
const { manager } = await startWorker(WorkerClass, { discovery: { mode: 'local' } })
```

A custom directory can be passed when the default path is not suitable:

```js
const ork = await getOrk({ discovery: { mode: 'local', dir: '/shared/mdk-keys' } })
const { manager } = await startWorker(WorkerClass, { discovery: { mode: 'local', dir: '/shared/mdk-keys' } })
```

Keys persist across restarts and the directory is read again each time ORK starts, so workers and ORK can start in any order
without coordination.

<Callout type="warn">
All processes must share the same filesystem path. Local mode requires every component to run on the same machine — use DHT
mode for workers on separate hosts.
</Callout>

The [full-site example](https://github.com/tetherto/mdk/blob/main/examples/full-site/README.md#how-out-of-process-workers-find-the-ork) demonstrates local mode as its default multi-process setup — `up --discovery local`
starts all workers in local mode, and `up --discovery dht` switches to DHT without any other code change.

### Same-process mode

Same-process mode skips all network discovery. Pass the live ORK instance to `startWorker()` and registration happens as a
direct in-process call — no topic, no directory, no network lookup:

```js
const ork = await getOrk(opts)
const { manager } = await startWorker(WorkerClass, { ork })
```

Two behaviors differ from DHT and local mode:

- **Registration**: `startWorker` calls `ork.registerWorker()` directly with the adapter's public key. The worker reaches `READY`
  synchronously before `startWorker` returns — no `waitForDiscovery()` required
- **Lifecycle coupling**: the worker's stop handler is pushed onto ORK's internal `_cleanup` queue. When ORK shuts down, it stops
  the worker automatically. In DHT and local modes you own the worker's shutdown via your process signal handlers

Use the same-process mode for the [get-started tutorial](/v0-4-0/tutorials/backend-stack/run) and [single-process deployments](/v0-4-0/how-to/deployment/run-single-process-site).
For multi-process, use DHT or local mode instead.

## Capability contract

[`mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/whatsminer/mdk-contract.json) is the canonical source of truth for a Worker's programmatic capabilities **and** its AI context. MDK
deliberately merges formal validation and semantic guidance into a single JSON contract:

- `description` does double duty as the human UI label and AI edge-case rule (for example, *"Outlet temperature > 85C requires intervention"*)
- `constraints` governs orchestration limits
- `troubleshooting` provides if/then recovery behaviors alongside the payload it evaluates

The exhaustive JSON Schema is `mdk-contract.schema.json`, with a [reference instance at `mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/whatsminer/mdk-contract.json).

## Add hardware

External integrators add new hardware by building a Worker package that conforms to the strict Device-Lib Contract:

1. Reference [`mdk-contract.schema.json`](https://github.com/tetherto/mdk/blob/main/backend/workers/base/mdk-contract.schema.json) to author the [`mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/whatsminer/mdk-contract.json), validating strict data schemas while injecting
   explanations, constraints, and troubleshooting directly into the relevant nodes.
2. Subclass [`@tetherto/worker-base`](https://github.com/tetherto/mdk/blob/main/backend/workers/base/README.md) and implement the two translation hooks, `onTelemetryPull` and `onCommand`,
   in `src/hardware.js`. All HRPC plumbing is inherited from [the base class](https://github.com/tetherto/mdk/blob/main/backend/workers/base/README.md#mdkworkeradapter).
3. Boot the Worker instance, connect to devices, and register with `@tetherto/mdk-ork` using the appropriate
   [discovery mode](#discovery-model). `@tetherto/mdk-ork` detects the peer and pulls its identity and capabilities.

<Callout type="idea">
(See [`mdk-whatsminer-worker.js`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/whatsminer/lib/mdk-whatsminer-worker.js) for a reference subclass implementing both hooks against a real device.)
</Callout>

## Next steps

- [Configure how often ORK polls discovered workers](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/orchestrator.md#configuration-cadences)
- [Diagnose startup hangs when outbound network is restricted](/v0-4-0/how-to/miners/troubleshooting)
- Review how workers [publish their RPC key and register with ORK](https://github.com/tetherto/mdk/blob/main/backend/workers/base/README.md#mdkworkeradapter)

# Terminology (/v0-4-0/concepts/terminology)

This page explains the terms you need to familiarize yourself with, using an Antminer rack as an example.

## Terms you need to name

| Term | What it is | Lives at |
| --- | --- | --- |
| **ORK** (Orchestration Kernel) | The pull-only kernel that owns the device registry, routes commands, and aggregates telemetry. | [`backend/core/ork/index.js`](https://github.com/tetherto/mdk/blob/main/backend/core/ork/index.js) |
| **App Node** | The developer-owned gateway between non-Node clients (UI, AI agents) and ORK. Mandatory whenever a non-Node consumer reaches the kernel; not used in the in-process Antminer-rack example below. | [`backend/core/app-node/`](https://github.com/tetherto/mdk/blob/main/backend/core/app-node/worker.js) |
| **Worker** | A device-family translator. Speaks the MDK Protocol upward to ORK and the vendor's native API downward to one device family (one miner brand, one container type, one pool API). | [`backend/workers/v0-4-0/install-pattern.md`](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/install-pattern.md) |
| **Manager class** | The JavaScript class a worker exports, one per supported device model. Instances drive a single rack of devices. | e.g. `AM_S19XP`, `AM_S21` in [`backend/workers/miners/antminer/index.js`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/index.js) |
| **Thing** | One registered device instance. Created by calling `manager.registerThing({ info, opts })`. Identified by a generated `deviceId`. | runtime, in `manager.mem.things` |

## How they compose, for an Antminer rack

```mermaid
flowchart TB
    subgraph clientLayer ["Your code"]
        Client["your script (e.g., client.js)"]
    end

    subgraph kernel ["Kernel"]
        Ork["ORK<br/>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 or IPC"| Ork
    Ork -->|"Hyperswarm DHT"| AntminerWorker
    AntminerWorker --> Miners
```

The same shape repeats for every other device family (Whatsminer, container vendors, pool APIs). For a multi-worker view, parallel workers, and multi-site deployments, see [`architecture.md#scaling`](/v0-4-0/concepts/architecture#scaling).

## What this section does NOT cover

- Multi-process discovery across machines — [worker discovery](/v0-4-0/concepts/stack/workers).
- App Node implementation details (HTTP routing, JWT auth, RBAC) — see [`backend/core/app-node/worker.js`](https://github.com/tetherto/mdk/blob/main/backend/core/app-node/worker.js).
- Building your own worker for a new device family — see [`backend/workers/v0-4-0/install-pattern.md`](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/install-pattern.md).
- Per-device contract details (telemetry units, command shapes, error codes) — those live in each worker's `mdk-contract.json`, e.g. [`backend/workers/miners/antminer/mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/mdk-contract.json).

## Next steps

- You are ready to run the example in [Run the stack](/v0-4-0/tutorials/backend-stack/run).

# Get started with MDK (/v0-4-0/get-started)

## New to MDK? Go from stack to dashboard

Three short tutorials take you from a running MDK stack to a browser dashboard demo. First you watch the stack run, 
then you drive it from the CLI, then you launch the demo dashboard on top. Climb straight through, or jump to the 
rung you need.

<Cards className="grid-cols-1 md:grid-cols-3">
  <Card
    icon={<Play />}
    className="@max-lg:col-span-1"
    title={<span style={cardTitle}>1. Run the stack</span>}
    href="/v0-4-0/tutorials/backend-stack/run"
    description={
      <span style={cardProse}>
        <strong>Observe</strong> — one command brings up ORK with a mock device registered
      </span>
    }
  />
  <Card
    icon={<Terminal />}
    className="@max-lg:col-span-1"
    title={<span style={cardTitle}>2. Control from the CLI</span>}
    href="/v0-4-0/tutorials/backend-stack/cli"
    description={
      <span style={cardProse}>
        <strong>Interact</strong> — drive a running stack from a REPL over IPC
      </span>
    }
  />
  <Card
    icon={<LayoutDashboard />}
    className="@max-lg:col-span-1"
    title={<span style={cardTitle}>3. Run a dashboard demo</span>}
    href="/v0-4-0/tutorials/full-stack/dashboard"
    description={
      <span style={cardProse}>
        <strong>Demo</strong> — run a React dashboard with live charts on the stack
      </span>
    }
  />
</Cards>

## Build with MDK

<Cards>
  <Card
    icon={<Layers />}
    title={<span style={cardTitle}>Add MDK data to an existing app</span>}
    href="#framework-quickstarts"
    description={
      <span style={cardProse}>
        Use the <strong>frontend tools</strong> to bring MDK telemetry and controls into a React, Vue, or Svelte app
      </span>
    }
  />
  <Card
    icon={<Terminal />}
    title={<span style={cardTitle}>Connect directly to ORK from any client</span>}
    href="/v0-4-0/tutorials/backend-stack"
    description={
      <>
        <span style={cardProse}>
          Start with the backend stack tutorials: run ORK with a mock device, then control it from a CLI
        </span>
        <span style={cardCta}>Start the backend stack path →</span>
      </>
    }
  />
  <Card
    icon="🚧"
    title={<span style={cardTitle}>Build a full MDK app from scratch</span>}
    href="/v0-4-0/resources/roadmap"
    description={
      <>
        <span style={cardProse}>
          Coming soon: Adopt the App Toolkit shell. Frontend and backend wired together, extended via plugins
        </span>
        <span style={cardCta}>Learn about the release schedule →</span>
      </>
    }
  />
  <Card
    icon="🚧"
    title={<span style={cardTitle}>Align the MDK backend with your business logic</span>}
    href="/v0-4-0/resources/roadmap"
    description={
      <>
        <span style={cardProse}>
          Coming soon: Use the <strong>backend tools</strong> to plug a library of custom routes and aggregations into the MDK App Node
        </span>
        <span style={cardCta}>Learn about the release schedule →</span>
      </>
    }
  />
</Cards>

## Framework quickstarts

<Callout type="info">
  Building with Cursor or Claude? [Build with your AI agent](/v0-4-0/agents).
</Callout>

<Cards>
  <Card
    icon={<Atom />}
    title={<span style={cardTitle}>React</span>}
    href="/v0-4-0/ui/react/get-started"
    description={
      <span style={cardProse}>
        Pick a path: lean Quickstart, full Tutorial, or browse the component packages
      </span>
    }
  />
  <Card
    icon="🚧"
    title={<span style={cardTitle}>Vue</span>}
    href="/v0-4-0/resources/roadmap"
    description={
      <>
        <span style={cardProse}>
          Reactive hooks for Vue
        </span>
        <span style={cardCta}>Learn about the release schedule →</span>
      </>
    }
  />
  <Card
    icon="🚧"
    title={<span style={cardTitle}>Svelte</span>}
    href="/v0-4-0/resources/roadmap"
    description={
      <>
        <span style={cardProse}>
          Reactive hooks for Svelte
        </span>
        <span style={cardCta}>Learn about the release schedule →</span>
      </>
    }
  />
  <Card
    icon="🚧"
    title={<span style={cardTitle}>Web Components</span>}
    href="/v0-4-0/resources/roadmap"
    description={
      <>
        <span style={cardProse}>
          Framework-agnostic Web Components
        </span>
        <span style={cardCta}>Learn about the release schedule →</span>
      </>
    }
  />
</Cards>

## Next steps

- Learn more about the high-level [architecture](/v0-4-0/concepts/architecture): runtime stack and deployment modes
- Compare [deployment topologies](/v0-4-0/concepts/deployment-topologies): single process or supervised services
- Run a site from the [deployment how-tos](/v0-4-0/how-to/deployment)
- [Contribute](/v0-4-0/community/contributing)

# App Node how-to guides (/v0-4-0/how-to/app-node)

## Overview

The App Node wraps [`@tetherto/mdk-client`](https://github.com/tetherto/mdk/blob/main/backend/core/client/README.md) to deliver an authenticated HTTP, WebSocket, and MCP interface for your frontend and AI agents. These guides cover how to run it and extend it with the plugin system.

<Callout type="info">
If App Node, ORK, or plugin are unfamiliar, read [terminology](/v0-4-0/concepts/terminology) first. For the full developer model — extension, data access,
auth design — read the [App Node concept page](/v0-4-0/concepts/stack/app-node).
</Callout>

## Choose a guide

| Goal | Guide |
| --- | --- |
| Start the App Node for the first time | [Run the App Node](/v0-4-0/how-to/app-node/run) |
| Use built-in plugins or build your own | [App Node plugins](/v0-4-0/how-to/app-node/plugins) |
| Stop ORK, App Node, and Workers cleanly | [Tear down MDK services](/v0-4-0/how-to/app-node/teardown) |

## Next steps

- [Understand the App Node as a development surface](/v0-4-0/concepts/stack/app-node)
- Read the [App Node API reference](https://github.com/tetherto/mdk/blob/main/backend/core/app-node/README.md)
- Choose a [deployment shape](/v0-4-0/concepts/deployment-topologies)

# App Node plugins (/v0-4-0/how-to/app-node/plugins)

## Overview

The App Node 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.

<Callout type="info">
Plugins call into the ORK kernel through `services.mdkClient` — the same SDK you use anywhere else in MDK. No knowledge of the
MDK Protocol envelope or internal message shapes is required.
</Callout>

## Default plugins

MDK ships plugins that load automatically on App Node startup:

- The `auth` plugin serves identity and token endpoints under `/auth`
- The `telemetry` plugin serves site metrics (hashrate, consumption, efficiency, temperature, and more) under `/auth/metrics`
- The `site-hashrate` plugin serves aggregated site hashrate history

The [plugin reference](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md) lists every default route, its method, and whether it needs a token — those tables are generated
from each plugin's `mdk-plugin.json`. Plugins you mount yourself are documented by their own manifests.

<Steps>

<Step>

### Mount a plugin

Pass an `extraPluginDirs` array to `startAppNode()` to load additional plugins at boot alongside the default plugins:

```js
const { startAppNode } = require('@tetherto/mdk')

await startAppNode({
  ork,
  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.

</Step>

<Step>

### Build a plugin

A plugin is a directory with two things: a manifest and controllers.

#### 1.1 Create the manifest

[`mdk-plugin.json`](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md#manifest-format) declares the plugin identity (`name`, `version`) and a `routes` array. Each route needs an `id`, a `handler` path, and an `http`
block with a `method` and `path`. Rather than copy a synthetic example, start from a real manifest and trim it:

- [`examples/full-site/plugins/site/mdk-plugin.json`](https://github.com/tetherto/mdk/blob/main/examples/full-site/plugins/site/mdk-plugin.json) — three routes including a `GET`, a `POST` with a `requestBody`, and
path parameters
- [`backend/core/plugins/telemetry/mdk-plugin.json`](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/telemetry/mdk-plugin.json) — auth, caching, query parameters, and named-export handlers

Path parameters use `{param}` syntax — the loader normalises them to Fastify's `:param` format. For named exports use `"handler":
"./controllers/foo.js#namedExport"`. The [plugin reference](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md) explains what each field means and what the loader requires.

#### 1.2 Write a controller

Every controller exports an `async function (req, services)`:

```js
// controllers/live.js — read live telemetry
module.exports = async function live (req, services) {
  const deviceId = req.query.deviceId
  const telemetry = await services.mdkClient.pullTelemetry(deviceId, 'metrics')
  return { deviceId, ...telemetry }
}
```

```js
// controllers/command.js — dispatch a command
module.exports = async function command (req, services) {
  const deviceId = req.params.deviceId
  const { mode } = req.body

  const result = await services.mdkClient.sendCommand(deviceId, 'setPowerMode', { mode })

  return {
    deviceId,
    commandId: result.commandId,
    status: result.status
  }
}
```

</Step>

</Steps>

### The `req` object

| Field | Type | Contains |
| --- | --- | --- |
| `req.params` | `object` | Path parameters (e.g. `{ deviceId: 'wm-001' }`) |
| `req.query` | `object` | Query string parameters |
| `req.body` | `object` | Parsed JSON request body |
| `req.headers` | `object` | HTTP headers |
| `req._info` | `object` | Internal request metadata (rarely needed) |

### The `services` object

| Field | Type | Use for |
| --- | --- | --- |
| `services.mdkClient` | `MdkClient` | Live reads and command dispatch — `sendCommand`, `pullTelemetry`, `getCapabilities`, `listWorkers` |
| `services.dataProxy` | `DataProxy` | Historical and aggregated data from worker tail-logs — `requestData`, `requestDataMap` |
| `services.authLib` | `AuthLib` | JWT and session helpers (needed only for advanced auth flows) |
| `services.conf` | `object` | App Node runtime config |

<Callout type="warn">
Always guard `services.mdkClient` — it is `null` when the App Node starts without a live ORK connection:
```js
if (!services.mdkClient) throw new Error('ERR_MDK_CLIENT_UNAVAILABLE')
```
</Callout>

### Read hardware data

For live device data use `mdkClient`:

```js
// Pull a live metrics snapshot
const tel = await services.mdkClient.pullTelemetry(deviceId, 'metrics')

// Pull the declared capabilities (from the worker's mdk-contract.json)
const { capabilities } = await services.mdkClient.getCapabilities(deviceId)

// List all registered workers
const { workers } = await services.mdkClient.listWorkers()
```

For historical or aggregated series from a worker's persisted tail-log use `dataProxy`:

```js
const results = await services.dataProxy.requestData('tailLogRangeAggr', {
  type: 'miner',
  startDate: start,
  endDate: end,
  fields: { hashrate_sum: 1 }
})
```

The [default telemetry controllers](https://github.com/tetherto/mdk/tree/main/backend/core/plugins/telemetry/controllers) show worked examples of both patterns.

### Send a command

`sendCommand` dispatches via the ORK to the worker that owns the device. The command must be declared in
the worker's `mdk-contract.json`. It returns:

| Field | Type | Description |
| --- | --- | --- |
| `commandId` | `string` | Correlation ID generated by ORK. Echo this to the HTTP caller so they can track the operation. |
| `status` | `string` | `'SUCCESS'` or `'FAILED'` |
| `result` | `object` | Command-specific response payload (present when status is `'SUCCESS'`) |
| `error` | `string` | Error message (present when status is `'FAILED'`) |

```js
const result = await services.mdkClient.sendCommand(deviceId, 'reboot', {})
if (result.status === 'FAILED') throw new Error(result.error)
return { commandId: result.commandId, status: result.status }
```

### Auth, permissions, and caching

**Auth** — set `"auth": true` on a route to require a valid Bearer token. The adapter runs `authCheck` before the handler is called.

**Permissions** — add a `"permissions"` array to enforce RBAC:

```json
{
  "id": "site.miners.command",
  "auth": true,
  "permissions": ["device:write"],
  "handler": "./controllers/command.js",
  "http": { "method": "POST", "path": "/site/miners/{deviceId}/command" }
}
```

**Caching** — add a `"cache"` array of dot-path strings to enable request-level caching. The cache key is composed
from the route ID and the resolved values of each path:

```json
{
  "id": "telemetry.hashrate",
  "cache": ["query.start", "query.end", "query.groupBy"],
  ...
}
```

Pass `?overwriteCache=true` to any cached route to bypass and refresh.

### Manifest validation errors

The plugin loader validates every manifest and handler at startup and throws if anything is wrong:

| Error | Cause |
| --- | --- |
| `ERR_PLUGIN_MANIFEST_MISSING` | No `mdk-plugin.json` found in the plugin directory |
| `ERR_PLUGIN_MANIFEST_INVALID` | JSON parse error, or missing required field (`name`, `version`, or `routes`) |
| `ERR_PLUGIN_ROUTE_DUPLICATE_ID` | Two routes in the same manifest share the same `id` |
| `ERR_PLUGIN_HANDLER_NOT_FOUND` | The `handler` file path does not exist or failed to load |
| `ERR_PLUGIN_HANDLER_NOT_FUNCTION` | The handler file exports something other than a function |

### Full example

The [`examples/full-site/plugins/site/`](https://github.com/tetherto/mdk/tree/main/examples/full-site/plugins/site) directory is a complete worked plugin with three routes: a live site overview, a
historical series, and a command endpoint. Read it alongside this guide.

## Troubleshooting

### Migrate from v0.2 to v0.3

In v0.3, `metricsRoutes` and `devicesRoutes` were removed from `backend/core/app-node/workers/lib/server/index.js`. The auth and telemetry
endpoints they registered are now delivered by the default plugins, which load automatically — no action needed for those.

If your v0.2 code patched or monkey-patched those registrations to inject custom logic:

1. Create a plugin directory with an [`mdk-plugin.json`](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md#manifest-format) and controller files for the routes you were injecting.
2. Pass the directory to `startAppNode()` via `extraPluginDirs`.

```js
// Before (v0.2 — no longer works)
const server = require('@tetherto/mdk-app-node/workers/lib/server')
server.metricsRoutes.push(myCustomRoute)

// After (v0.3+)
await startAppNode({
  ork,
  extraPluginDirs: [path.join(__dirname, 'plugins/my-metrics')]
})
```

## Next steps

- Full [manifest and services reference](https://github.com/tetherto/mdk/blob/main/backend/core/plugins/README.md)
- [App Node API and config](https://github.com/tetherto/mdk/blob/main/backend/core/app-node/README.md)
- Complete [worked example](https://github.com/tetherto/mdk/tree/main/examples/full-site/plugins/site)

# Run the App Node (/v0-4-0/how-to/app-node/run)

## Overview

This guide covers three ways to run the App Node: programmatically via `startAppNode()` (the standard production path), connected to
a remote ORK over HRPC (cross-host deployments), and as a standalone process from the source tree (for contributors).

<Callout type="info">
If App Node, ORK, or plugin are unfamiliar, read [terminology](/v0-4-0/concepts/terminology) first. For a deeper explanation of what the App Node
owns and how it connects to ORK, read the [App Node concept page](/v0-4-0/concepts/stack/app-node).
</Callout>

## Prerequisites

- Node.js >=24 (LTS)
- npm >=11
- Commands are run from the repository root
- An ORK instance running and reachable, or `orkIpc: false` to start without a live ORK (development only)

<Steps>

<Step>

### Programmatic path

Most teams embed `startAppNode()` in their own Node.js application rather than running the App Node as a separate process.
This is the standard production path.

#### 1.1 Development (no auth)

Use `noAuth: true` during local development to skip the JWT requirement:

```js
const { getOrk, startAppNode } = require('@tetherto/mdk')

const ork = await getOrk()
const server = await startAppNode({ ork, port: 3000, noAuth: true })
// HTTP server is up at http://localhost:3000
```

<Callout type="warn">
`noAuth: true` disables JWT validation on all routes. Never use this in production.
</Callout>

#### 1.2 Production (OAuth2)

Pass an `auth` block to enable OAuth2. Google and Microsoft providers are built in:

```js
const { getOrk, startAppNode } = require('@tetherto/mdk')

const ork = await getOrk()
const server = await startAppNode({
  ork,
  port: 3000,
  auth: {
    h0: {
      method: 'google',
      credentials: { client: { id: 'YOUR_CLIENT_ID', secret: 'YOUR_CLIENT_SECRET' } },
      users: ['admin@example.com']
    }
  }
})
```

Replace the `users` array with the email addresses that should have access. A copy of the full OAuth2 config format ships in
[`backend/core/app-node/config/facs/httpd-oauth2.config.json.example`](https://github.com/tetherto/mdk/blob/main/backend/core/app-node/config/facs/httpd-oauth2.config.json.example). The generated `httpd-oauth2.config.json`
(written to `opts.root/config/facs/` on first start) persists your settings across restarts — edit that file rather than the code.

The full configuration reference, including all `startAppNode()` options, is in the [App Node API reference](https://github.com/tetherto/mdk/blob/main/backend/core/app-node/README.md).

</Step>

<Step>

### Cross-host path (HRPC)

Use this path when ORK runs on a separate host. Pass the ORK HRPC gateway public key to `startAppNode()` instead of an ORK instance.
The HRPC transport is selected automatically and IPC is disabled.

#### 2.1 Obtain the ORK gateway key

On the host running ORK, start ORK and print its public key:

```js
const { getOrk } = require('@tetherto/mdk')

const ork = await getOrk()
console.log('ORK gateway key:', ork.getPublicKey().toString('hex'))
```

Share that hex string with the App Node host.

#### 2.2 Start the App Node with `orkKey`

```js
const { startAppNode } = require('@tetherto/mdk')

const server = await startAppNode({
  orkKey: '<ork-gateway-pubkey-hex>',
  port: 3000,
  noAuth: true   // replace with auth config for production
})
```

<Callout type="info">
Pre v1.0, ORK's `auth.whitelist` defaults to empty and admits any HRPC caller. For production deployments, add the App Node's
DHT public key to ORK's allowlist — see the [App Node concept page](/v0-4-0/concepts/stack/app-node) and [`opts.orkKey` reference](https://github.com/tetherto/mdk/blob/main/backend/core/mdk/README.md).
</Callout>

</Step>

<Step>

### Standalone path

To run the App Node directly from the source tree without embedding it:

```bash
cd backend/core/app-node
npm install
npm run dev
```

For production mode:

```bash
npm start
```

<Callout type="info">
The standalone path is intended for contributors working on the App Node itself. For application development, embed `startAppNode()`
in your own project rather than running it standalone.
</Callout>

</Step>

</Steps>

## Next steps

- [Add routes with the plugin system](/v0-4-0/how-to/app-node/plugins)
- [Review all configuration options](https://github.com/tetherto/mdk/blob/main/backend/core/app-node/README.md)
- Understand the [extension model, auth design, and ORK connection](/v0-4-0/concepts/stack/app-node)
- Choose a [deployment shape](/v0-4-0/concepts/deployment-topologies)

# Tear down MDK services (/v0-4-0/how-to/app-node/teardown)

## Overview

MDK registers graceful shutdown handlers automatically when you start services with `getOrk()`, `startWorker()`, or `startAppNode()`.
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-getork)
- [Explicit teardown](#explicit-teardown-in-tests-or-scripted-runs)
- [Custom signal handling](#custom-signal-handling-with-onshutdown)

## Prerequisites

- Familiarity with the [App Node](/v0-4-0/concepts/stack/app-node)
- MDK [installed and a working boot sequence](/v0-4-0/how-to/app-node/run)

<Steps>

<Step>

### Automatic teardown with `getOrk()`

`getOrk()` registers `SIGINT`/`SIGTERM` handlers internally. Any Workers or App Node instances started with `opts.ork` are
chained into the cleanup sequence automatically — no extra code needed.

```js
const { getOrk, startWorker, startAppNode } = require('@tetherto/mdk')
const { WM_M56S } = require('@tetherto/miner-whatsminer')

const ork = await getOrk()
const { manager } = await startWorker(WM_M56S, { ork })
await startAppNode({ ork, port: 3000, noAuth: true })

// Press Ctrl+C — MDK stops App Node, Worker, then ORK automatically.
```

See [`getOrk` API reference](https://github.com/tetherto/mdk/blob/main/backend/core/mdk/README.md#getorkopts--promiseorkmanager).

</Step>

<Step>

### Explicit teardown in tests or scripted runs

Short-lived processes — integration tests, one-shot scripts — never receive `SIGINT`. Call `shutdown(ork)` directly
to drain the full cleanup chain. Pass the `ork` object returned by `getOrk()`; passing a server object stops only the App Node.

```js
const { getOrk, startAppNode, shutdown } = require('@tetherto/mdk')

const ork = await getOrk()
await startAppNode({ ork, noAuth: true })

// … run assertions or perform work …

await shutdown(ork) // stops App Node (chained), then stops ORK
```

See [`shutdown` API reference](https://github.com/tetherto/mdk/blob/main/backend/core/mdk/README.md#shutdownhandle--promisevoid).

</Step>

<Step>

### Custom signal handling with `onShutdown`

Use `onShutdown` when you need to close resources outside an MDK boot object — for example, a database connection or a log buffer.

```js
const { onShutdown } = require('@tetherto/mdk')

onShutdown(async () => {
  await db.close()
  await logger.flush()
}, { forceMs: 5000 })
```

See [`onShutdown` API reference](https://github.com/tetherto/mdk/blob/main/backend/core/mdk/README.md#onshutdowncleanupfn-opts--handler).

</Step>

</Steps>

## What just happened

1. **Automatic chain**: `getOrk()`, `startWorker({ ork })`, and `startAppNode({ ork })` wire themselves into `ork._cleanup` so a single signal stops everything in order.
2. **Explicit drain**: `shutdown(ork)` gives you the same ordered teardown on demand, without a signal.
3. **Custom hooks**: `onShutdown(fn)` lets you attach cleanup logic outside the MDK object hierarchy.

## Next steps

- Full API reference — [`@tetherto/mdk` README](https://github.com/tetherto/mdk/blob/main/backend/core/mdk/README.md)
- [Run the App Node](/v0-4-0/how-to/app-node/run)

# Run an MDK site (/v0-4-0/how-to/deployment)

## Overview

Use these guides to choose a site deployment shape.

<Callout type="info">
If ORK, App Node, worker, manager, or thing are unfamiliar, read [terminology](/v0-4-0/concepts/terminology) first.
If you are choosing between topologies, read [deployment topologies](/v0-4-0/concepts/deployment-topologies).
</Callout>

## Choose a guide

| Goal | Guide |
| --- | --- |
| Run ORK, App Node, and workers in one Node.js process | [Run a single-process site](/v0-4-0/how-to/deployment/run-single-process-site) |
| Run App Node and workers as supervised PM2 or Docker services | [Run a microservices site](/v0-4-0/how-to/deployment/run-microservices-site) |

## Next steps

- Understand the trade-offs before you choose — [Deployment topologies](/v0-4-0/concepts/deployment-topologies)
- Run the smallest site shape — [Run a single-process site](/v0-4-0/how-to/deployment/run-single-process-site)
- Run supervised services — [Run a microservices site](/v0-4-0/how-to/deployment/run-microservices-site)

# Run a microservices site (/v0-4-0/how-to/deployment/run-microservices-site)

This thin page directs you to the correct location for the prerequisites, config fields, run command, smoke test, and troubleshooting.

## Overview

Use the **microservices** site example when you want App Node and workers to run as separate OS processes or containers.

<Callout type="info">
This page is the task guide for the microservices topology.
The [deployment topologies](/v0-4-0/concepts/deployment-topologies) concept explains when to choose microservices instead of single-process.
</Callout>

## Use this topology when

- You need supervisor-managed restarts and logs
- You want to restart or scale one service without restarting the others
- You want a production-like layout for App Node and workers

## Run the example

Follow the [microservices site example](https://github.com/tetherto/mdk/tree/main/examples/backend/site):

- Start with the [prerequisites](https://github.com/tetherto/mdk/tree/main/examples/backend/site#prerequisites)
- Use the [PM2 steps](https://github.com/tetherto/mdk/tree/main/examples/backend/site#pm2) for local process supervision on one host
- Use the [Docker steps](https://github.com/tetherto/mdk/tree/main/examples/backend/site#docker) when you want containerized services or Compose-managed startup

## Next steps

- Compare the supported shapes: [Deployment topologies](/v0-4-0/concepts/deployment-topologies)
- Run the simpler local topology — [Run a single-process site](/v0-4-0/how-to/deployment/run-single-process-site)
- Register a single miner before building a site config — [Run a miner worker](/v0-4-0/how-to/miners)

# Run a single-process site (/v0-4-0/how-to/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 ORK, the App Node, and worker to share one Node.js process.

<Callout type="info">
This page is the task guide for the single-process topology.
The [deployment topologies](/v0-4-0/concepts/deployment-topologies) concept explains when to choose single-process instead of microservices.
</Callout>

## Use this topology when

- You are developing locally, running demos, or writing self-contained tests
- You want a minimal-footprint deployment
- You do not need per-service restart isolation

## Run the example

Follow the [single-process site example](https://github.com/tetherto/mdk/tree/main/examples/backend/site-single-process):

- Start with its [prerequisites](https://github.com/tetherto/mdk/tree/main/examples/backend/site-single-process#prerequisites)
- Use the example [quickstart](https://github.com/tetherto/mdk/tree/main/examples/backend/site-single-process#quickstart)

## Next steps

- Compare the supported shapes: [Deployment topologies](/v0-4-0/concepts/deployment-topologies)
- Run the supervised topology — [Run a microservices site](/v0-4-0/how-to/deployment/run-microservices-site)
- Register a single miner before building a site config — [Run a miner worker](/v0-4-0/how-to/miners)

# Run a miner worker (/v0-4-0/how-to/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.

<Callout type="info">
If ORK, worker, manager, or thing are unfamiliar, read [terminology](/v0-4-0/concepts/terminology) first.
</Callout>

## Pick your hardware

The authoritative model list for every worker is the generated [supported-hardware catalogue](/v0-4-0/reference/supported-hardware#miners). For example, you may:

- [Run an Antminer worker](/v0-4-0/how-to/miners/run-antminer-worker)
- [Run a Whatsminer worker](/v0-4-0/how-to/miners/run-whatsminer-worker)
- [Run an Avalon worker](/v0-4-0/how-to/miners/run-avalon-worker)

## Prerequisites

Every guide assumes:

- Node.js >=24 (LTS)
- npm >=11
- Commands are run from the repo root
- Outbound network access for ORK 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

<Callout type="warn">
Each runnable example starts an ORK whose control plane is peer-to-peer over a Hyperswarm DHT. Without outbound network access, startup will stall while the ORK tries to reach DHT bootstrap nodes. See [how workers connect](/v0-4-0/concepts/stack/workers) for the ORK/DHT mechanics.
</Callout>

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](/v0-4-0/reference/supported-hardware)
- New to the moving parts? Read [terminology](/v0-4-0/concepts/terminology) (ORK, worker, manager, thing, mock)
- If an example does not start or a mock port is busy, use [troubleshooting](/v0-4-0/how-to/miners/troubleshooting)
- Drive the registered device from the CLI or dashboard: [Get started](/v0-4-0/tutorials/backend-stack)

# Run an Antminer worker (/v0-4-0/how-to/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](/v0-4-0/how-to/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

<Steps>

<Step>

### Development

<details>
<summary>Run against a mock</summary>

To support development, each model ships a runnable example that starts an ORK, boots a mock device, and registers it. This guide uses the S21 example:

```bash
node backend/workers/miners/antminer/examples/run-s21.js
```

It prints the ORK HRPC key and the registered device ID, then stays running until Ctrl+C. To run another model, use the matching example listed in [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/USAGE.md#runnable-examples).

</details>

</Step>

<Step>

### 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 choose the manager class and mock example for your model. This guide uses `AM_S21` and `run-s21.js` as the example; replace them with the values 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 `registerThing` call for one Antminer device; replace the example IP address and credentials with your miner's values:

```js
const { getOrk, startWorker } = require('@tetherto/mdk')
const { AM_S21 } = require('@tetherto/miner-antminer')

const ork = await getOrk()
const { manager } = await startWorker(AM_S21, { ork })

await manager.registerThing({
  info: { container: 'site-1', serialNum: 'AM-001' },
  opts: { address: '192.168.1.20', port: 80, username: 'root', password: 'root' }
})
```

<Callout type="warning">
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.
</Callout>

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 `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/antminer/USAGE.md) and the shared [install pattern](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/install-pattern.md).

</Step>

</Steps>

## Troubleshooting

The development example on this page uses `run-s21.js`. A working run prints `ORK 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](/v0-4-0/how-to/miners/troubleshooting).

## Next steps

- Decide how to run the worker service — [Deployment topologies](/v0-4-0/concepts/deployment-topologies)
- Review telemetry units, command shapes, and error codes — [`mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/mdk-contract.json)

# Run an Avalon worker (/v0-4-0/how-to/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](/v0-4-0/how-to/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

<Steps>

<Step>

### Development

<details>
<summary>Run against a mock</summary>

To support development, the A1346 ships a runnable example that starts an ORK, boots a mock device, and registers it:

```bash
node backend/workers/miners/avalon/examples/run-a1346.js
```

It prints the ORK HRPC key and the registered device ID, then stays running until Ctrl+C. For the manager class and example file, see [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/avalon/USAGE.md#runnable-example).

</details>

</Step>

<Step>

### Connect a miner

#### 2.1 Pick your model

Use the Avalon worker's [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/avalon/USAGE.md) to confirm the manager class and mock example for your model. This guide uses `AV_A1346` and `run-a1346.js` as the example.

#### 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 `registerThing` call for one Avalon device; replace the example IP address with your miner's value:

```js
const { getOrk, startWorker } = require('@tetherto/mdk')
const { AV_A1346 } = require('@tetherto/miner-avalon')

const ork = await getOrk()
const { manager } = await startWorker(AV_A1346, { ork })

await manager.registerThing({
  info: { container: 'site-1', serialNum: 'AV-001' },
  opts: { address: '192.168.1.30', port: 4028 }
})
```

<Callout type="warning">
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.
</Callout>

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 `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/avalon/USAGE.md) and the shared [install pattern](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/install-pattern.md).

</Step>

</Steps>

## Troubleshooting

The development example on this page uses `run-a1346.js`. A working run prints `ORK 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](/v0-4-0/how-to/miners/troubleshooting).

## Next steps

- Decide how to run the worker service — [Deployment topologies](/v0-4-0/concepts/deployment-topologies)
- Review telemetry units, command shapes, and error codes — [`mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/avalon/mdk-contract.json)

# Run a Whatsminer worker (/v0-4-0/how-to/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](/v0-4-0/how-to/miners#prerequisites) before you start.

Deployment-specific requirements:

- A Node.js service or script in your deployment that runs the MDK worker and registers devices
- A supported Whatsminer device reachable from the machine or container running the worker
- The miner API reachable over encrypted TCP, typically port `14028`
- The Whatsminer API password. The worker negotiates a session token from it; there is no separate username

<Steps>

<Step>

### Development

<details>
<summary>Run against a mock</summary>

To support development, each model ships a runnable example that starts an ORK, boots a mock device, and registers it. This guide uses the M56S example:

```bash
node backend/workers/miners/whatsminer/examples/run-m56s.js
```

It prints the ORK HRPC key and the registered device ID, then stays running until Ctrl+C. To run another model, use the matching example listed in [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/whatsminer/USAGE.md#runnable-examples).

</details>

</Step>

<Step>

### 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 choose the manager class and mock example for your model. This guide uses `WM_M56S` and `run-m56s.js` as the example; replace them with the values for your miner.

#### 2.2 Register your miner

Whatsminer devices use an encrypted TCP API on port 14028 with token-based authentication; the worker negotiates a session token from the device password (there is no separate username). Add this code to the Node.js service or script that runs the MDK worker in your deployment. The snippet shows the minimum `registerThing` call for one Whatsminer device; replace the example IP address and password with your miner's values:

```js
const { getOrk, startWorker } = require('@tetherto/mdk')
const { WM_M56S } = require('@tetherto/miner-whatsminer')

const ork = await getOrk()
const { manager } = await startWorker(WM_M56S, { ork })

await manager.registerThing({
  info: { container: 'site-1', serialNum: 'WM-001' },
  opts: { address: '192.168.1.10', port: 14028, password: 'admin' }
})
```

<Callout type="warning">
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.
</Callout>

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 `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).

</Step>

</Steps>

## Troubleshooting

The development example on this page uses `run-m56s.js`. A working run prints `ORK 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](/v0-4-0/how-to/miners/troubleshooting).

## Next steps

- Decide how to run the worker service — [Deployment topologies](/v0-4-0/concepts/deployment-topologies)
- Review telemetry units, command shapes, and error codes — [`mdk-contract.json`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/whatsminer/mdk-contract.json)

# Troubleshoot miner workers (/v0-4-0/how-to/miners/troubleshooting)

## Overview

This page covers the mock/development examples used by the Antminer, Whatsminer, and Avalon miner guides. The examples start a bundled mock miner, start an ORK, register one device, print the identifiers you need, and keep running until you stop them.

## Expected output

A working example prints an ORK key and a registered device ID:

```text
ORK HRPC key: <hex key>
Device: <device id>

Ctrl+C to stop.
```

If you do not see both `ORK HRPC key:` and `Device:`, use the following checks.

## Find the right port

Mock examples and real miners use different sources for ports.

### Mock examples

Each runnable example starts a mock miner on the port declared in that example file. To find the mock port for your model:

1. Open the worker's `USAGE.md` and choose the runnable example for your model:
   - Antminer: [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/USAGE.md#runnable-examples)
   - Whatsminer: [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/whatsminer/USAGE.md#runnable-examples)
   - Avalon: [USAGE.md](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/avalon/USAGE.md#runnable-example)
2. Open the matching `examples/run-*.js` file.
3. Look for the `createServer({ port: ... })` call.

The cross-worker manifest also records the expected mock type and default port for each variant: [workers manifest](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/workers-manifest.yaml).

### Real miners

Real devices use their native APIs:

- Antminer: HTTP, usually port `80`, with digest-auth credentials.
- Whatsminer: encrypted TCP, usually port `14028`, with the API password.
- Avalon: CGMiner TCP API, usually port `4028`, with no username or password.

Before registering a real miner, confirm the miner is reachable from the machine or container running the worker.

## Clean up a mock port

If an example exits with `EADDRINUSE` or says a port is already in use, find the process using that port:

```bash
lsof -nP -iTCP:<port> -sTCP:LISTEN
```

Replace `<port>` 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 <pid>
```

Run `lsof` again to confirm the port is free before restarting the example.

## Example does not print an ORK key

Each example starts an ORK. The ORK control plane uses Hyperswarm DHT discovery, so the machine running the example needs outbound network access. If outbound access or network-interface inspection is blocked, startup may hang or fail before printing `ORK HRPC key:`.

Check:

- The machine has outbound network access.
- Local security tooling, containers, or sandboxes are not blocking UDP/network-interface access.
- You are running the command from the repository root.
- Dependencies have been installed for `backend/core` and `backend/workers`.

## File lock or socket errors

The examples call `getOrk()` with default local paths. By default, the topic file is `os.tmpdir()/mdk/.dht-topic` and the IPC socket is `os.tmpdir()/mdk/ork.sock`. If another ORK, app-node, or example is already running with the same defaults, you may see file lock or local socket errors.

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 ORK gets separate local state:

```bash
TMPDIR=/tmp/mdk-antminer-s21 node backend/workers/miners/antminer/examples/run-s21.js
```

Keep the temporary path short on macOS and Linux because ORK also creates a Unix socket under that directory.

## 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

# Use UI Core headlessly (/v0-4-0/how-to/ui/core/use-ui-core-headlessly)

<PackageBadge>@tetherto/mdk-ui-core</PackageBadge>

[`@tetherto/mdk-ui-core`](/v0-4-0/reference/app-toolkit/ui-core) 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 &mdash; 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 Core 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 &mdash; logging, websocket bridges, metrics.

For a React app, the [React adapter](/v0-4-0/ui/react/get-started) wraps UI Core with `<MdkProvider>` and adapter hooks. Use that path instead so most React code never touches `@tetherto/mdk-ui-core` directly.

## Install

`@tetherto/mdk-ui-core` has no peer dependencies on React or any UI framework.

```bash
npm install @tetherto/mdk-ui-core
```

## Subpath imports

Pull only the pieces you need from the relevant subpath. Subpath imports give tree-shakers a smaller surface than the top-level barrel:

```ts
```

The [subpath exports table](/v0-4-0/reference/app-toolkit/ui-core#subpath-exports) on the reference lists every supported entry.

## Create a QueryClient

`createMdkQueryClient` returns a TanStack Query Core client wired to your App Node. Pass an explicit `baseUrl`, or let the factory resolve one from environment variables:

```ts

const queryClient = createMdkQueryClient({
  baseUrl: 'https://app-node.example.com',
})
```

Without an explicit `baseUrl`, the factory checks `VITE_MDK_API_URL` then `MDK_API_URL` before falling back to `http://localhost:3000`. The [resolution order](/v0-4-0/reference/app-toolkit/ui-core#queryclient-factory) on the reference covers every case.

## Read store state

Each store is a Zustand vanilla singleton. `getState()` returns the current snapshot:

```ts

const { token, permissions } = authStore.getState()
console.log('current token', token)
```

## Write store state

`setState()` accepts either a partial object or a function that receives the previous state:

```ts

devicesStore.setState({ selectedDeviceId: 'wm-002' })

devicesStore.setState((prev) => ({
  devices: [...prev.devices, newDevice],
}))
```

## Subscribe to changes

`subscribe()` runs a callback on every state change and returns an unsubscribe function:

```ts

const unsubscribe = notificationStore.subscribe((state) => {
  console.log('unread notifications:', state.count)
})

unsubscribe()
```

## A complete Node example

A small Node script that authenticates against the App Node, fetches the device list once, and then tails unread notification count changes:

```ts
  authStore,
  devicesStore,
  notificationStore,
} from '@tetherto/mdk-ui-core/store'

async function main() {
  const queryClient = createMdkQueryClient({
    baseUrl: process.env.MDK_API_URL ?? 'http://localhost:3000',
  })

  authStore.setState({ token: process.env.MDK_TOKEN ?? '' })

  const devices = await queryClient.fetchQuery({
    queryKey: ['devices', 'list'],
    queryFn: async () => {
      const res = await fetch(`${process.env.MDK_API_URL}/api/devices`, {
        headers: { Authorization: `Bearer ${authStore.getState().token}` },
      })
      return res.json()
    },
  })
  devicesStore.setState({ devices })

  console.log(`Found ${devices.length} devices`)

  const unsubscribe = notificationStore.subscribe((state) => {
    console.log(`unread notifications: ${state.count}`)
  })

  process.on('SIGINT', () => {
    unsubscribe()
    process.exit(0)
  })
}

main().catch((err) => {
  console.error(err)
  process.exit(1)
})
```

Run it with:

```bash
MDK_TOKEN=ey... MDK_API_URL=https://app-node.example.com node script.ts
```

For the prebuilt query and mutation factories (`authQuery`, `devicesQuery`, `deviceQuery`, `telemetryQuery`), check the [QueryClient factory section](/v0-4-0/reference/app-toolkit/ui-core#queryclient-factory) on the reference.

## Next steps

- [UI Core reference](/v0-4-0/reference/app-toolkit/ui-core): full store list, query helpers, and the `createMdkQueryClient` resolution order.
- [MDK App Toolkit](/v0-4-0/concepts/stack/app-toolkit): where UI Core fits in the frontend stack.
- [React adapter](/v0-4-0/ui/react/get-started): if you decide to layer React on top.

# Compose reporting layouts (/v0-4-0/how-to/ui/react/compose-reporting-layouts)

<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

The reporting composites — [`Cost`](/v0-4-0/ui/react/foundation/reporting/financial#cost), [`Ebitda`](/v0-4-0/ui/react/foundation/reporting/financial#ebitda), [`EnergyBalance`](/v0-4-0/ui/react/foundation/reporting/financial#energybalance), [`HashBalance`](/v0-4-0/ui/react/foundation/reporting/financial#hashbalance), and [`Hashrate`](/v0-4-0/ui/react/foundation/reporting/operational#hashrate) — render fixed, opinionated layouts. When you need a different arrangement (a custom grid, a subset of charts, your own tabs), compose the page yourself from the **same building blocks** those composites are made of.

Every building block receives pre-shaped data as props and does no fetching — wire your own data layer (RTK Query, TanStack, fixtures).

## When to use a building block vs the composite

- Reach for the **composite** (for example `<Cost />`) 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](/v0-4-0/how-to/ui/react/compose-reporting-layouts/cost)
- [Compose EBITDA layouts](/v0-4-0/how-to/ui/react/compose-reporting-layouts/ebitda)
- [Compose Energy balance layouts](/v0-4-0/how-to/ui/react/compose-reporting-layouts/energy-balance)
- [Compose Hash balance layouts](/v0-4-0/how-to/ui/react/compose-reporting-layouts/hash-balance)
- [Compose Hashrate layouts](/v0-4-0/how-to/ui/react/compose-reporting-layouts/hashrate)

## Shared building blocks

These power the week selector inside [`TimeframeControls`](/v0-4-0/ui/react/foundation/reporting/financial#timeframecontrols), shared across the financial reporting surfaces.

<ComponentTable category="Reporting composition" pkg="foundation" />

### `TimeframeWeekFlatContent`

<ComponentDescription name="TimeframeWeekFlatContent" />

```tsx
```

Renders inside the week selector of [`TimeframeControls`](/v0-4-0/ui/react/foundation/reporting/financial#timeframecontrols).

### `TimeframeWeekTreeContent`

<ComponentDescription name="TimeframeWeekTreeContent" />

```tsx
```

Renders inside the week selector of [`TimeframeControls`](/v0-4-0/ui/react/foundation/reporting/financial#timeframecontrols).

# Compose Cost layouts (/v0-4-0/how-to/ui/react/compose-reporting-layouts/cost)

<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

The [`Cost`](/v0-4-0/ui/react/foundation/reporting/financial#cost) composite renders a fixed 2x2 cost-summary layout. To build a custom arrangement, compose it from the building blocks below — each takes pre-shaped data as props and does no fetching.

## Building blocks

<ComponentTable category="Cost composition" pkg="foundation" />

### `CostContent`

<ComponentDescription name="CostContent" />

```tsx
```

Renders inside the [`Cost`](/v0-4-0/ui/react/foundation/reporting/financial#cost) composite.

### `CostMetrics`

<ComponentDescription name="CostMetrics" />

```tsx
```

Renders inside the [`Cost`](/v0-4-0/ui/react/foundation/reporting/financial#cost) composite.

### `AvgAllInCostChart`

<ComponentDescription name="AvgAllInCostChart" />

```tsx
```

Renders inside the [`Cost`](/v0-4-0/ui/react/foundation/reporting/financial#cost) composite.

### `ProductionCostChart`

<ComponentDescription name="ProductionCostChart" />

```tsx
```

Renders inside the [`Cost`](/v0-4-0/ui/react/foundation/reporting/financial#cost) composite.

### `OperationsEnergyChart`

<ComponentDescription name="OperationsEnergyChart" />

```tsx
```

Renders inside the [`Cost`](/v0-4-0/ui/react/foundation/reporting/financial#cost) composite.

# Compose EBITDA layouts (/v0-4-0/how-to/ui/react/compose-reporting-layouts/ebitda)

<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

The [`Ebitda`](/v0-4-0/ui/react/foundation/reporting/financial#ebitda) composite renders a fixed EBITDA layout (metric row plus chart panel). To build a custom arrangement, compose it from the building blocks below — each takes pre-shaped data as props and does no fetching.

## Building blocks

<ComponentTable category="EBITDA composition" pkg="foundation" />

### `EbitdaMetrics`

<ComponentDescription name="EbitdaMetrics" />

```tsx
```

Renders inside the [`Ebitda`](/v0-4-0/ui/react/foundation/reporting/financial#ebitda) composite.

### `EbitdaCharts`

<ComponentDescription name="EbitdaCharts" />

```tsx
```

Renders inside the [`Ebitda`](/v0-4-0/ui/react/foundation/reporting/financial#ebitda) composite.

### `ActualEbitdaCard`

<ComponentDescription name="ActualEbitdaCard" />

```tsx
```

Renders inside the [`EbitdaMetrics`](#ebitdametrics) row.

### `EbitdaHodlCard`

<ComponentDescription name="EbitdaHodlCard" />

```tsx
```

Renders inside the [`EbitdaMetrics`](#ebitdametrics) row.

### `EbitdaSellingCard`

<ComponentDescription name="EbitdaSellingCard" />

```tsx
```

Renders inside the [`EbitdaMetrics`](#ebitdametrics) row.

### `MonthlyEbitdaChart`

<ComponentDescription name="MonthlyEbitdaChart" />

```tsx
```

Renders inside the [`EbitdaCharts`](#ebitdacharts) panel.

### `BitcoinPriceCard`

<ComponentDescription name="BitcoinPriceCard" />

```tsx
```

Renders inside the [`Ebitda`](/v0-4-0/ui/react/foundation/reporting/financial#ebitda) composite.

### `BitcoinProducedCard`

<ComponentDescription name="BitcoinProducedCard" />

```tsx
```

Renders inside the [`Ebitda`](/v0-4-0/ui/react/foundation/reporting/financial#ebitda) composite.

### `BitcoinProducedChart`

<ComponentDescription name="BitcoinProducedChart" />

```tsx
```

Renders inside the [`Ebitda`](/v0-4-0/ui/react/foundation/reporting/financial#ebitda) composite.

### `BitcoinProductionCostCard`

<ComponentDescription name="BitcoinProductionCostCard" />

```tsx
```

Renders inside the [`Ebitda`](/v0-4-0/ui/react/foundation/reporting/financial#ebitda) composite.

# Compose Energy balance layouts (/v0-4-0/how-to/ui/react/compose-reporting-layouts/energy-balance)

<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

The [`EnergyBalance`](/v0-4-0/ui/react/foundation/reporting/financial#energybalance) composite renders a fixed two-tab layout (revenue and cost). To build a custom arrangement, compose it from the building blocks below — each takes pre-shaped data as props and does no fetching.

## Building blocks

<ComponentTable category="Energy balance composition" pkg="foundation" />

### `EnergyBalanceRevenueCharts`

<ComponentDescription name="EnergyBalanceRevenueCharts" />

```tsx
```

Renders inside the revenue tab of [`EnergyBalance`](/v0-4-0/ui/react/foundation/reporting/financial#energybalance).

### `EnergyBalanceRevenueMetrics`

<ComponentDescription name="EnergyBalanceRevenueMetrics" />

```tsx
```

Renders inside the revenue tab of [`EnergyBalance`](/v0-4-0/ui/react/foundation/reporting/financial#energybalance).

### `EnergyBalanceCostCharts`

<ComponentDescription name="EnergyBalanceCostCharts" />

```tsx
```

Renders inside the cost tab of [`EnergyBalance`](/v0-4-0/ui/react/foundation/reporting/financial#energybalance).

### `EnergyBalanceCostMetrics`

<ComponentDescription name="EnergyBalanceCostMetrics" />

```tsx
```

Renders inside the cost tab of [`EnergyBalance`](/v0-4-0/ui/react/foundation/reporting/financial#energybalance).

### `EnergyBalancePowerChart`

<ComponentDescription name="EnergyBalancePowerChart" />

```tsx
```

Renders inside both tabs of [`EnergyBalance`](/v0-4-0/ui/react/foundation/reporting/financial#energybalance).

### `EnergyRevenueChart`

<ComponentDescription name="EnergyRevenueChart" />

```tsx
```

Renders inside [`EnergyBalanceRevenueCharts`](#energybalancerevenuecharts).

### `EnergyCostChart`

<ComponentDescription name="EnergyCostChart" />

```tsx
```

Renders inside [`EnergyBalanceCostCharts`](#energybalancecostcharts).

### `EnergyMetricCard`

<ComponentDescription name="EnergyMetricCard" />

```tsx
```

Renders inside the energy-balance metric grids.

# Compose Hash balance layouts (/v0-4-0/how-to/ui/react/compose-reporting-layouts/hash-balance)

<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

The [`HashBalance`](/v0-4-0/ui/react/foundation/reporting/financial#hashbalance) composite renders a fixed two-tab layout (revenue and cost). To build a custom arrangement, compose it from the two tab panels below — each takes pre-shaped data as props and does no fetching.

## Building blocks

<ComponentTable category="Hash balance composition" pkg="foundation" />

### `HashBalanceRevenuePanel`

<ComponentDescription name="HashBalanceRevenuePanel" />

```tsx
```

Renders inside the revenue tab of [`HashBalance`](/v0-4-0/ui/react/foundation/reporting/financial#hashbalance).

### `HashBalanceCostPanel`

<ComponentDescription name="HashBalanceCostPanel" />

```tsx
```

Renders inside the cost tab of [`HashBalance`](/v0-4-0/ui/react/foundation/reporting/financial#hashbalance).

# Compose Hashrate layouts (/v0-4-0/how-to/ui/react/compose-reporting-layouts/hashrate)

<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

The [`Hashrate`](/v0-4-0/ui/react/foundation/reporting/operational#hashrate) composite renders a fixed three-tab layout. To build a custom arrangement, compose it from the tab views below — each takes pre-shaped data as props and does no fetching.

## Building blocks

<ComponentTable category="Hashrate composition" pkg="foundation" />

### `HashrateSiteView`

<ComponentDescription name="HashrateSiteView" />

```tsx
```

Renders inside the Site View tab of [`Hashrate`](/v0-4-0/ui/react/foundation/reporting/operational#hashrate).

### `HashrateMinerTypeView`

<ComponentDescription name="HashrateMinerTypeView" />

```tsx
```

Renders inside the Miner Type View tab of [`Hashrate`](/v0-4-0/ui/react/foundation/reporting/operational#hashrate).

### `HashrateMiningUnitView`

<ComponentDescription name="HashrateMiningUnitView" />

```tsx
```

Renders inside the Mining Unit View tab of [`Hashrate`](/v0-4-0/ui/react/foundation/reporting/operational#hashrate).

# Reference (/v0-4-0/reference)

The Reference section indexes the canonical specs for everything MDK exposes: field semantics, signatures, transition
rules, and contracts. Reach for it when you need exact shapes. For narrative explanations, see [Architecture](/v0-4-0/concepts/architecture);
for step-by-step instructions, see [Get started](/v0-4-0/get-started).

## Browse by stack area

### App Toolkit

- **[UI Kit](/v0-4-0/reference/app-toolkit/ui-kit)**: constants, hooks, types, and utilities for the React UI Kit
- **[UI Core](/v0-4-0/reference/app-toolkit/ui-core)**: framework-agnostic headless Zustand stores and TanStack Query factory
- **[Hooks](/v0-4-0/reference/app-toolkit/hooks)**: complete hook catalog: data, state, component, and utility hooks across 
the adapter and devkit packages
- **[UI CLI](/v0-4-0/reference/app-toolkit/ui-cli)**: command reference for the UI CLI (agent registry, scaffold, and verify)

### ORK

- **[ORK](/v0-4-0/reference/ork)**: kernel module specs, state machines, transition tables, and recovery behavior

### MDK Protocol

- **[Protocol](/v0-4-0/reference/protocol)**: envelope schema, request/response examples, action catalogue, and 
base command set
- *Capability contract*: coming soon

### Hardware

- **[Supported hardware](/v0-4-0/reference/supported-hardware)**: miners, containers, power meters, sensors, 
and mining-pool integrations

# Hooks (/v0-4-0/reference/app-toolkit/hooks)

Complete hook catalog for the MDK App Toolkit, covering hooks from both `@tetherto/mdk-react-adapter` (data, state, utilities) and `@tetherto/mdk-react-devkit` (component hooks). Grouped by what each hook depends on rather than which package ships it — this matches the
[Developer entry points](/v0-4-0/concepts/stack/app-toolkit#developer-entry-points) model where the adapter and the UI Kit are siblings, so you can adopt only the layers you need.

## At a glance

| Bucket | Page | What it covers | Needs |
|---|---|---|---|
| Components | [Component hooks](/v0-4-0/reference/app-toolkit/hooks/components) | Hooks coupled to MDK styled components or shell layout (notifications, forms, charts, dashboards, filters, widgets, tables, reporting) | `@tetherto/mdk-react-devkit` and (for some) `<MdkProvider>` |
| Data | [Data hooks](/v0-4-0/reference/app-toolkit/hooks/data) | Adapter hooks that fetch and shape site, pool, dashboard, chart, and auth data | `<MdkProvider>` from `@tetherto/mdk-react-adapter` |
| State | [State hooks](/v0-4-0/reference/app-toolkit/hooks/state) | React-bound views of the headless `@tetherto/mdk-ui-core` Zustand stores | `<MdkProvider>` from `@tetherto/mdk-react-adapter` |
| Utilities | [Utility hooks](/v0-4-0/reference/app-toolkit/hooks/utilities) | Generic React helpers, mining-domain transforms, permission checks, and TanStack Query re-exports | `@tetherto/mdk-react-adapter` and (for some) `<MdkProvider>` |

## All hooks

### Components

<PackageBadge>@tetherto/mdk-react-devkit</PackageBadge>

| Sub-group | Hooks |
|---|---|
| Charts | [`useChartDataCheck`](/v0-4-0/reference/app-toolkit/hooks/components#usechartdatacheck), [`useEbitda`](/v0-4-0/reference/app-toolkit/hooks/components#useebitda), [`useEnergyBalanceViewModel`](/v0-4-0/reference/app-toolkit/hooks/components#useenergybalanceviewmodel) |
| Dashboards | [`usePoolConfigs`](/v0-4-0/reference/app-toolkit/hooks/components#usepoolconfigs), [`useSiteOverviewDetailsData`](/v0-4-0/reference/app-toolkit/hooks/components#usesiteoverviewdetailsdata) |
| Filters | [`useReportTimeFrameSelectorState`](/v0-4-0/reference/app-toolkit/hooks/components#usereporttimeframeselectorstate), [`useTimeframeControls`](/v0-4-0/reference/app-toolkit/hooks/components#usetimeframecontrols) |
| Forms | [`useFormField`](/v0-4-0/reference/app-toolkit/hooks/components#useformfield), [`useFormReset`](/v0-4-0/reference/app-toolkit/hooks/components#useformreset) |
| Notifications | [`useNotification`](/v0-4-0/reference/app-toolkit/hooks/components#usenotification) |
| Reporting | [`useHashrate`](/v0-4-0/reference/app-toolkit/hooks/components#usehashrate), [`useEnergyReportSite`](/v0-4-0/reference/app-toolkit/hooks/components#useenergyreportsite) |
| Shell | [`useHeaderControls`](/v0-4-0/reference/app-toolkit/hooks/components#useheadercontrols), [`useSidebarExpandedState`](/v0-4-0/reference/app-toolkit/hooks/components#usesidebarexpandedstate), [`useSidebarSectionState`](/v0-4-0/reference/app-toolkit/hooks/components#usesidebarsectionstate) |
| Tables | [`useGetAvailableDevices`](/v0-4-0/reference/app-toolkit/hooks/components#usegetavailabledevices) |
| Widgets | [`useFinancialDateRange`](/v0-4-0/reference/app-toolkit/hooks/components#usefinancialdaterange) |

### Data

<PackageBadge>@tetherto/mdk-react-adapter</PackageBadge>

| Sub-group | Hooks |
|---|---|
| Auth and token | [`useAuthToken`](/v0-4-0/reference/app-toolkit/hooks/data#useauthtoken), [`useTokenPolling`](/v0-4-0/reference/app-toolkit/hooks/data#usetokenpolling) |
| Chart data | [`useConsumptionChartData`](/v0-4-0/reference/app-toolkit/hooks/data#useconsumptionchartdata), [`useHashrateChartData`](/v0-4-0/reference/app-toolkit/hooks/data#usehashratechartdata), [`usePowerModeTimelineData`](/v0-4-0/reference/app-toolkit/hooks/data#usepowermodetimelinedata) |
| Dashboard | [`useDashboardDateRange`](/v0-4-0/reference/app-toolkit/hooks/data#usedashboarddaterange), [`useDashboardExport`](/v0-4-0/reference/app-toolkit/hooks/data#usedashboardexport), [`useDashboardTimeRange`](/v0-4-0/reference/app-toolkit/hooks/data#usedashboardtimerange) |
| Incidents | [`useActiveIncidents`](/v0-4-0/reference/app-toolkit/hooks/data#useactiveincidents) |
| Pool data | [`usePoolRows`](/v0-4-0/reference/app-toolkit/hooks/data#usepoolrows), [`usePoolStats`](/v0-4-0/reference/app-toolkit/hooks/data#usepoolstats) |
| Site data | [`useSiteConsumption`](/v0-4-0/reference/app-toolkit/hooks/data#usesiteconsumption), [`useSiteConsumptionChartData`](/v0-4-0/reference/app-toolkit/hooks/data#usesiteconsumptionchartdata), [`useSiteContainerCapacity`](/v0-4-0/reference/app-toolkit/hooks/data#usesitecontainercapacity), [`useSiteEfficiency`](/v0-4-0/reference/app-toolkit/hooks/data#usesiteefficiency), [`useSiteHashrate`](/v0-4-0/reference/app-toolkit/hooks/data#usesitehashrate), [`useSiteMinerCounts`](/v0-4-0/reference/app-toolkit/hooks/data#usesiteminercounts), [`useSiteMinerStats`](/v0-4-0/reference/app-toolkit/hooks/data#usesiteminerstats), [`useSitePowerMeter`](/v0-4-0/reference/app-toolkit/hooks/data#usesitepowermeter), [`useSitesOverviewData`](/v0-4-0/reference/app-toolkit/hooks/data#usesitesoverviewdata) |

### State

<PackageBadge>@tetherto/mdk-react-adapter</PackageBadge>

| Hook | Summary |
|---|---|
| [`useAuth`](/v0-4-0/reference/app-toolkit/hooks/state#useauth) | React view of the headless `authStore` (token, permissions) |
| [`useDevices`](/v0-4-0/reference/app-toolkit/hooks/state#usedevices) | React view of the headless `devicesStore` (device list, selection) |
| [`useTimezone`](/v0-4-0/reference/app-toolkit/hooks/state#usetimezone) | React view of the headless `timezoneStore` (operator timezone) |
| [`useNotifications`](/v0-4-0/reference/app-toolkit/hooks/state#usenotifications) | React view of the headless `notificationStore` (unread counter) |
| [`useActions`](/v0-4-0/reference/app-toolkit/hooks/state#useactions) | React view of the headless `actionsStore` (pending submissions) |

### Utilities

<PackageBadge>@tetherto/mdk-react-adapter</PackageBadge> + <PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

| Sub-group | Hooks |
|---|---|
| Device and IP | [`usePduViewer`](/v0-4-0/reference/app-toolkit/hooks/utilities#usepduviewer) |
| Domain transforms | [`useCostSummary`](/v0-4-0/reference/app-toolkit/hooks/utilities#usecostsummary), [`useHashBalance`](/v0-4-0/reference/app-toolkit/hooks/utilities#usehashbalance), [`useSubsidyFees`](/v0-4-0/reference/app-toolkit/hooks/utilities#usesubsidyfees), [`useUpdateExistedActions`](/v0-4-0/reference/app-toolkit/hooks/utilities#useupdateexistedactions) |
| Generic React | [`useLocalStorage`](/v0-4-0/reference/app-toolkit/hooks/utilities#uselocalstorage), [`useKeyDown`](/v0-4-0/reference/app-toolkit/hooks/utilities#usekeydown), [`useWindowSize`](/v0-4-0/reference/app-toolkit/hooks/utilities#usewindowsize), [`usePlatform`](/v0-4-0/reference/app-toolkit/hooks/utilities#useplatform), [`useDeviceResolution`](/v0-4-0/reference/app-toolkit/hooks/utilities#usedeviceresolution), [`useBeepSound`](/v0-4-0/reference/app-toolkit/hooks/utilities#usebeepsound), [`usePagination`](/v0-4-0/reference/app-toolkit/hooks/utilities#usepagination), [`useSubtractedTime`](/v0-4-0/reference/app-toolkit/hooks/utilities#usesubtractedtime), [`useTimezoneFormatter`](/v0-4-0/reference/app-toolkit/hooks/utilities#usetimezoneformatter) |
| Permissions | [`useCheckPerm`](/v0-4-0/reference/app-toolkit/hooks/utilities#usecheckperm), [`useHasPerms`](/v0-4-0/reference/app-toolkit/hooks/utilities#usehasperms), [`useIsFeatureEditingEnabled`](/v0-4-0/reference/app-toolkit/hooks/utilities#useisfeatureeditingenabled) |
| TanStack Query re-exports | [Re-exports table](/v0-4-0/reference/app-toolkit/hooks/utilities#tanstack-query-re-exports) |

## Imports

```tsx
// Data hooks
```

# Component hooks (/v0-4-0/reference/app-toolkit/hooks/components)

<PackageBadge>@tetherto/mdk-react-devkit</PackageBadge>

Hooks that wrap or compose styled MDK components — notifications, sidebar/header shell, forms, filters, widgets, tables, charts, and dashboards. 
Adopt these when you are using `@tetherto/mdk-react-devkit` for your UI.

If you bring your own components, you may not need anything here. Start with [State hooks](/v0-4-0/reference/app-toolkit/hooks/state) or 
[Utility hooks](/v0-4-0/reference/app-toolkit/hooks/utilities) instead.

## At a glance

| Sub-group | Hooks |
|---|---|
| [Notifications](#notifications) | `useNotification` |
| [Shell](#shell) | `useHeaderControls`, `useSidebarExpandedState`, `useSidebarSectionState` |
| [Forms](#forms) | `useFormField`, `useFormReset` |
| [Filters](#filters) | `useReportTimeFrameSelectorState`, `useTimeframeControls` |
| [Widgets](#widgets) | `useFinancialDateRange` |
| [Tables](#tables) | `useGetAvailableDevices` |
| [Charts](#charts) | `useChartDataCheck`, `useEbitda`, `useEnergyBalanceViewModel` |
| [Dashboards](#dashboards) | `usePoolConfigs`, `useSiteOverviewDetailsData` |
| [Reporting](#reporting) | `useOperationsDashboard`, `useHashrate`, `useEnergyReportSite` |

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- For hooks that read from headless stores (`useNotification`, view-model hooks): 
[wrap your app in `<MdkProvider>`](/v0-4-0/ui/react/quickstart#wrap-your-app-in-mdkprovider)

## Import

```tsx
  useChartDataCheck,
  useEbitda,
  useEnergyBalanceViewModel,
  useFinancialDateRange,
  useGetAvailableDevices,
  useHeaderControls,
  useNotification,
  useOperationsDashboard,
  usePoolConfigs,
  useReportTimeFrameSelectorState,
  useSiteOverviewDetailsData,
  useTimeframeControls,
} from '@tetherto/mdk-react-devkit/foundation'

  useFormField,
  useFormReset,
  useSidebarExpandedState,
  useSidebarSectionState,
} from '@tetherto/mdk-react-devkit/core'
```

## Notifications

### `useNotification`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Show toast notifications backed by the headless `notificationStore`. Supports success, error, info, and warning variants.

```tsx
```

#### Returns

| Member | Type | Description |
|--------|------|-------------|
| `notifySuccess` | `function` | Show success toast |
| `notifyError` | `function` | Show error toast |
| `notifyInfo` | `function` | Show info toast |
| `notifyWarning` | `function` | Show warning toast |

#### Method signature

```tsx
notifySuccess(message: string, description?: string, options?: NotificationOptions)
```

#### Options

Notification methods accept an optional third `options` argument:

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `duration` | Optional | `number` | `3000` | Duration in milliseconds (`0` = no autoclose) |
| `position` | Optional | `ToastPosition` | `'top-left'` | Toast position on screen |
| `dontClose` | Optional | `boolean` | `false` | When `true`, prevents autoclose |

#### Example

```tsx
function SaveButton() {
  const { notifySuccess, notifyError } = useNotification()

  const handleSave = async () => {
    try {
      await saveData()
      notifySuccess('Saved', 'Your changes have been saved.')
    } catch (error) {
      notifyError('Error', 'Failed to save changes.', { dontClose: true })
    }
  }

  return <Button onClick={handleSave}>Save</Button>
}
```

## Shell

### `useHeaderControls`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Read/write hook for the global header-controls store (toggles, sticky flag, theme).

```tsx
```

#### Returns

| Member | Type | Description |
|--------|------|-------------|
| `preferences` | `HeaderPreferences` | Current visibility state for each header item |
| `isLoading` | `boolean` | Loading state |
| `error` | `Error \| null` | Error state |
| `handleToggle` | `function` | Toggle a header item visibility |
| `handleReset` | `function` | Reset to default preferences |

<Callout type="warn">
`handleToggle` and `handleReset` both call `notifySuccess` internally. Every invocation produces a toast notification; avoid calling them in response to fast-changing state.
</Callout>

#### Example

```tsx
function HeaderSettings() {
  const { preferences, handleToggle, handleReset } = useHeaderControls()

  return (
    <div>
      {Object.entries(preferences).map(([key, visible]) => (
        <Toggle
          key={key}
          label={key}
          checked={visible}
          onChange={(value) => handleToggle(key, value)}
        />
      ))}
      <Button onClick={handleReset}>Reset to Default</Button>
    </div>
  )
}
```

### `useSidebarExpandedState`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Persist sidebar expanded/collapsed state in `localStorage` so the layout survives reloads.

```tsx
```

#### Example

```tsx
function AppSidebar() {
  const [expanded, setExpanded] = useSidebarExpandedState(false)

  return (
    <aside className={expanded ? 'sidebar--expanded' : 'sidebar--collapsed'}>
      <Button onClick={() => setExpanded(!expanded)}>Toggle sidebar</Button>
    </aside>
  )
}
```

### `useSidebarSectionState`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Persist individual sidebar section open/closed states in `localStorage`.

```tsx
```

#### Example

```tsx
function SidebarSection({ id, title, children }) {
  const [open, setOpen] = useSidebarSectionState(id, true)

  return (
    <section>
      <button type="button" onClick={() => setOpen(!open)}>{title}</button>
      {open ? children : null}
    </section>
  )
}
```

## Forms

### `useFormField`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Read-only context hook for form field children — returns the field's id, error state, and ARIA attributes.

```tsx
```

#### Example

```tsx

// Custom component that reads field context — must be rendered inside <FormField> / <FormItem>
function FieldStatusDot() {
  const { invalid, isDirty } = useFormField()
  return (
    <span className={invalid ? 'dot--error' : isDirty ? 'dot--dirty' : 'dot--clean'} />
  )
}
```

### `useFormReset`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Hook to handle form reset with callbacks.

```tsx
```

#### Example

```tsx

type MinerFields = { name: string; ip: string }

function MinerEditForm({ onSubmit }: { onSubmit: (v: MinerFields) => void }) {
  const form = useForm<MinerFields>({ defaultValues: { name: '', ip: '' } })
  const { resetForm, isDirty } = useFormReset({
    form,
    onAfterReset: () => console.log('Form reset'),
  })

  return (
    <Form form={form} onSubmit={form.handleSubmit(onSubmit)}>
      <FormInput control={form.control} name="name" label="Name" />
      <FormInput control={form.control} name="ip" label="IP address" />
      <Button type="submit">Save</Button>
      <Button type="button" onClick={resetForm} disabled={!isDirty}>
        Reset
      </Button>
    </Form>
  )
}
```

## Filters

### `useReportTimeFrameSelectorState`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

State hook backing the reporting time-frame selector — exposes the active window and setters.

```tsx
```

#### Example

```tsx

function ReportDateBar() {
  const { start, end, presetTimeFrame, setPresetTimeFrame } = useReportTimeFrameSelectorState()

  return (
    <div>
      <p>{start.toLocaleDateString()} – {end.toLocaleDateString()}</p>
      <button onClick={() => setPresetTimeFrame(7)}  className={presetTimeFrame === 7  ? 'active' : ''}>Last 7 days</button>
      <button onClick={() => setPresetTimeFrame(30)} className={presetTimeFrame === 30 ? 'active' : ''}>Last 30 days</button>
      <button onClick={() => setPresetTimeFrame(null)}>Custom range</button>
    </div>
  )
}
```

### `useTimeframeControls`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Core state machine for TimeframeControls — owns year / month / week selection and resolves the date-range output.

```tsx
```

#### Example

```tsx

function YearMonthPicker({ dateRange, onRangeChange, onTimeframeTypeChange }) {
  const {
    selectedYear,
    selectedMonth,
    handleYearChange,
    handleMonthTreeChange,
    yearSelectValue,
    monthSelectValue,
  } = useTimeframeControls({
    dateRange,
    timeframeType: null,
    onRangeChange,
    onTimeframeTypeChange,
    isWeekSelectVisible: false,
    weekTree: false,
  })

  return (
    <div>
      <select value={yearSelectValue} onChange={(e) => handleYearChange(e.target.value)}>
        <option value={String(selectedYear)}>{selectedYear}</option>
      </select>
      <select value={monthSelectValue} onChange={(e) => handleMonthTreeChange(e.target.value)}>
        <option value={monthSelectValue}>Month {selectedMonth + 1}</option>
      </select>
    </div>
  )
}
```

## Widgets

### `useFinancialDateRange`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Resolves the active financial date range (start/end) used by every reporting-section query.

```tsx
```

#### Example

```tsx

function ReportingToolbar({ timezone }: { timezone: string }) {
  const { datePicker, dateRange, onDateRangeReset } = useFinancialDateRange({ timezone })

  return (
    <div>
      {datePicker}
      <Button onClick={onDateRangeReset}>Reset to current month</Button>
      {dateRange && (
        <p>
          {new Date(dateRange.start).toLocaleDateString()} –{' '}
          {new Date(dateRange.end).toLocaleDateString()}
        </p>
      )}
    </div>
  )
}
```

## Tables

### `useGetAvailableDevices`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Transforms the host's device list into the available container and miner type sets used by device explorer. Pass `data` from your query result.

```tsx
```

#### Example

```tsx

function DeviceTypeFilter({ devices }) {
  const { availableContainerTypes, availableMinerTypes } = useGetAvailableDevices({ data: devices })

  return (
    <div>
      <select aria-label="Container type">
        <option value="">All containers</option>
        {availableContainerTypes.map((type) => <option key={type}>{type}</option>)}
      </select>
      <select aria-label="Miner type">
        <option value="">All miners</option>
        {availableMinerTypes.map((type) => <option key={type}>{type}</option>)}
      </select>
    </div>
  )
}
```

## Charts

### `useChartDataCheck`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Check if chart data is empty or unavailable. Returns `true` if empty (show empty state), `false` if data exists (show chart).

```tsx
```

Pass chart input in one of two shapes:

1. **`dataset`**: direct dataset for BarChart-style usage.
2. **`data`**: Chart.js-shaped object with `datasets` (LineChart) or a `dataset` property.

Provide at least one of `dataset` or `data` for a meaningful empty check.

#### Options

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `dataset` | Optional | `object \| array` | none | Direct dataset for BarChart; set `dataset` or `data` (at least one) for a meaningful check |
| `data` | Optional | `object` | none | Chart.js-shaped object with `datasets` (LineChart) or `dataset` property; set `dataset` or `data` (at least one) |

#### Returns

| Type | Description |
|------|-------------|
| `boolean` | `true` if data is empty, `false` if data exists |

#### Example

```tsx
function HashrateChart({ dataset }) {
  const isEmpty = useChartDataCheck({ dataset })

  if (isEmpty) {
    return <EmptyState message="No hashrate data available" />
  }

  return <BarChart data={dataset} />
}
```

```tsx
function TemperatureChart({ data }) {
  const isEmpty = useChartDataCheck({ data })

  return isEmpty ? (
    <EmptyState message="No temperature data" />
  ) : (
    <LineChart data={data} />
  )
}
```

#### Chart utility integration

`useChartDataCheck` expects **Chart.js-shaped** `data` (`{ labels, datasets }`), not raw `{ labels, series }` from app hooks. Convert hook output with
the [**`buildBarChartData` utility**](/v0-4-0/ui/react/core/components/charts/composition#chart-utilities) from `@tetherto/mdk-react-devkit/core`,
then pass the result to **`useChartDataCheck`**.

```tsx

function RevenueBarChart({ hookOutput }) {
  const chartData = buildBarChartData(hookOutput)
  const isEmpty = useChartDataCheck({ data: chartData })

  return (
    <ChartContainer title="Revenue" empty={isEmpty}>
      <BarChart data={chartData} />
    </ChartContainer>
  )
}
```

For the full `BarChartInput` shape, per-dataset `datalabels` merge, and all-zero empty rules, see
[Hook-shaped bar data (`buildBarChartData`)](/v0-4-0/ui/react/core/components/charts/composition#hook-shaped-bar-data).

### `useEbitda`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Transforms an `EbitdaResponse` and date-range options into query params and a chart-ready EBITDA view-model.

```tsx
```

#### Example

```tsx

// Wire your query result in; consume queryParams to drive the fetch
function EbitdaSection({ ebitdaResponse, isLoading, fetchErrors }) {
  const { datePicker, dateRange, queryParams, errors } = useEbitda({
    ebitda: ebitdaResponse,
    isLoading,
    fetchErrors,
  })

  // Pass queryParams to your data-fetching layer whenever the date range changes
  // e.g. useGetEbitdaQuery(queryParams, { skip: !queryParams })

  return (
    <div>
      {datePicker}
      {errors.length > 0 && <p role="alert">{errors.join(', ')}</p>}
    </div>
  )
}
```

### `useEnergyBalanceViewModel`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Computes the full EnergyBalance view model from raw API data, managing tab selection and display-mode state.

```tsx
```

#### Example

```tsx

function EnergyBalancePanel({ data, isLoading, fetchErrors, dateRange, availablePowerMW }) {
  const { viewModel, onTabChange, onRevenueDisplayModeChange } = useEnergyBalanceViewModel({
    data,
    isLoading,
    fetchErrors,
    dateRange,
    availablePowerMW,
  })

  return (
    <div>
      <div>
        <Button onClick={() => onTabChange('revenue')} disabled={viewModel.activeTab === 'revenue'}>Revenue</Button>
        <Button onClick={() => onTabChange('cost')}    disabled={viewModel.activeTab === 'cost'}>Cost</Button>
      </div>
      {viewModel.isLoading && <p>Loading…</p>}
      {viewModel.errors.length > 0 && <p role="alert">{viewModel.errors.join(', ')}</p>}
    </div>
  )
}
```

## Dashboards

### `usePoolConfigs`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Transforms raw pool-configuration rows from your API into `PoolSummary` objects for the Pool Manager UI. Fetch with TanStack Query in the host app, then pass `data`, `isLoading`, and `error` into this hook.

Typical usage: fetch with TanStack Query in the host app, then pass `data`, `isLoading`, and `error` into this hook. Foundation components such as [`PoolManagerPools`](/v0-4-0/ui/react/foundation/pool-manager/pools) and [`Miner explorer`](/v0-4-0/ui/react/foundation/pool-manager/miner-explorer) expect data shaped this way.

```tsx
```

#### Options

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `data` | Optional | `PoolConfigData[]` | none | Raw pool configuration rows from your API |
| `isLoading` | Optional | `boolean` | `false` | When `true`, the host should show a loading state |
| `error` | Optional | `unknown` | none | Error from your query; surfaced to pool-manager components |

#### Returns

| Member | Type | Description |
|--------|------|-------------|
| `pools` | `PoolSummary[]` | Normalized pool list for lists and accordions |
| `poolIdMap` | `Record<string, PoolSummary>` | Lookup by pool `id` |
| `isLoading` | `boolean` | Same as the option you passed in |
| `error` | `unknown` | Same as the option you passed in |

#### Example

```tsx

  const { data, isLoading, error } = useGetPoolConfigsQuery({})
  return usePoolConfigs({ data, isLoading, error })
}
```

```tsx
function PoolsPage({ poolConfig }: { poolConfig: PoolConfigData[] }) {
  const { pools, isLoading, error } = usePoolConfigs({ data: poolConfig })

  if (isLoading) return <Loader />
  if (error) return <CoreAlert variant="error">Failed to load pools</CoreAlert>

  return (
    <ul>
      {pools.map((pool) => (
        <li key={pool.id}>{pool.name}</li>
      ))}
    </ul>
  )
}
```

### `useSiteOverviewDetailsData`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Composes the per-site overview view-model: pools, performance series, and recent activity.

```tsx
```

#### Example

```tsx

function SiteOverviewCard({ unit, pdus, connectedMiners, isLoading }) {
  const {
    containerHashRate,
    isContainerRunning,
    minersHashmap,
    segregatedPduSections,
  } = useSiteOverviewDetailsData(unit, { pdus, connectedMiners, isLoading })

  return (
    <div>
      <p>Hashrate: {containerHashRate}</p>
      <p>Status: {isContainerRunning ? 'Running' : 'Offline'}</p>
      <p>Miners mapped: {Object.keys(minersHashmap).length}</p>
      <p>PDU sections: {Object.keys(segregatedPduSections).join(', ')}</p>
    </div>
  )
}
```

## Reporting

### `useOperationsDashboard`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Shapes raw operational metric logs into chart-ready payloads for the four [`OperationalDashboard`](/v0-4-0/ui/react/foundation/reporting/operational#operationaldashboard) cards. It never fetches — wire your data layer (TanStack Query, RTK Query, fixtures) and pass the results in. All unit conversion and series shaping (MH/s → TH/s, W → MW) happens inside the hook so the dashboard components stay purely presentational.

```tsx
```

#### Options

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `hashrate` | Optional | `OperationsTrendInput` | none | Hashrate log and loading/error state; `log` entries carry `{ ts, value }` with `value` in MH/s (converted to TH/s internally) |
| `consumption` | Optional | `OperationsTrendInput` | none | Power consumption log; `value` in Watts (converted to MW internally) |
| `efficiency` | Optional | `OperationsTrendInput` | none | Site efficiency log; `value` in W/TH/s (no conversion) |
| `miners` | Optional | `OperationsMinersInput` | none | Per-day miner status counts; `log` entries carry `{ ts, online, error, offline, sleep, maintenance }` |

Each `OperationsTrendInput` entry also accepts `nominalValue` (same base unit as `log`) which renders a flat reference line on the chart, and `isLoading` / `error` passed through to the card.

#### Returns

| Member | Type | Description |
|--------|------|-------------|
| `hashrate` | `{ data, isLoading, error }` | `LineChartCardData` payload for the hashrate card |
| `consumption` | `{ data, isLoading, error }` | `LineChartCardData` payload for the power-consumption card |
| `efficiency` | `{ data, isLoading, error }` | `LineChartCardData` payload for the site-efficiency card |
| `miners` | `{ data, isLoading, error }` | `{ labels, datasets }` stacked-bar payload for the miners-status card |

#### Example

```tsx

function OperationsDashboardPage({ hashrateLog, consumptionLog, efficiencyLog, minersLog, isLoading }) {
  const vm = useOperationsDashboard({
    hashrate: { log: hashrateLog, nominalValue: 150_000, isLoading },
    consumption: { log: consumptionLog, isLoading },
    efficiency: { log: efficiencyLog, isLoading },
    miners: { log: minersLog, isLoading },
  })

  return (
    <OperationalDashboard
      hashrate={vm.hashrate}
      consumption={vm.consumption}
      efficiency={vm.efficiency}
      miners={vm.miners}
    />
  )
}
```

### `useHashrate`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Base hook for a single Hashrate tab in single-site mode. Normalises a grouped-hashrate query result into the `{ log, isLoading, error }` shape consumed by `<HashrateSiteView>`, `<HashrateMinerTypeView>`, and `<HashrateMiningUnitView>`. Call once per tab — each tab fetches independently because they use different `groupBy` axes.

```tsx
```

#### Options

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `query` | Optional | `HashrateQueryState` | none | Result of fetching the v2 `/auth/metrics/hashrate?groupBy=…` endpoint. Wire your data layer (TanStack Query, RTK Query, fixtures) and pass the result here — this hook never fetches directly. |

#### Returns

| Member | Type | Description |
|--------|------|-------------|
| `log` | `HashrateGroupedLog \| undefined` | Normalised grouped-hashrate log; `undefined` while loading. |
| `isLoading` | `boolean` | Loading state from the upstream query. |
| `error` | `unknown` | Error from the upstream query, if any. |

#### Example

```tsx

function HashrateTab({ groupBy }) {
  const query = useQuery({ queryKey: ['hashrate', groupBy], queryFn: fetchHashrate })
  const { log, isLoading, error } = useHashrate({ query })
  return <HashrateSiteView log={log} isLoading={isLoading} error={error} />
}
```

---

### `useEnergyReportSite`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Merges site energy consumption data (from the v2 `/auth/metrics/consumption` endpoint) with snapshot tail-log and container list data for the Energy report site tab. Returns the combined view-model consumed by the energy report components.

```tsx
```

#### Options

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `dateRange` | Required | `object` | none | **Required.** Active date range `{ start, end }` in ms epoch. |
| `consumptionLog` | Required | `object` | none | **Required.** Raw consumption log from the `/auth/metrics/consumption` response. |
| `consumptionLoading` | Required | `boolean` | none | **Required.** Loading state of the consumption query. |
| `consumptionFetching` | Required | `boolean` | none | **Required.** Background-refetch state of the consumption query. |
| `consumptionError` | Required | `unknown` | none | **Required.** Error state of the consumption query. |
| `nominalPowerAvailabilityMw` | Required | `number \| undefined` | none | **Required.** Site nominal power capacity in MW from the nominal config. |
| `nominalConfigLoading` | Required | `boolean` | none | **Required.** Loading state of the nominal config query. |
| `tailLog` | Required | `object` | none | **Required.** Raw tail-log snapshot data. |
| `tailLogLoading` | Required | `boolean` | none | **Required.** Loading state of the tail-log query. |
| `containers` | Required | `object[]` | none | **Required.** Container list from the device query. |
| `containersLoading` | Required | `boolean` | none | **Required.** Loading state of the container query. |

#### Returns

Returns a `UseEnergyReportSiteResult` object containing the merged power-consumption view-model, power-mode table rows, and combined loading/error states for each data source. Pass directly to the Energy report site-tab components.

# Data hooks (/v0-4-0/reference/app-toolkit/hooks/data)

<PackageBadge>@tetherto/mdk-react-adapter</PackageBadge>

TanStack Query–backed hooks that fetch live data from the MDK backend and project it into view-model shapes ready for MDK foundation components. These hooks sit in the adapter layer so that tag names, aggregate field keys, and unit conversions stay out of the devkit component layer.

All hooks require [`<MdkProvider>`](/v0-4-0/ui/react/quickstart#wrap-your-app-in-mdkprovider) to be mounted in the tree.

## At a glance

| Sub-group | Hooks |
|---|---|
| [Alerts](#alerts) | `useCurrentAlertDevices`, `useHistoricalAlerts` |
| [Auth and token](#auth-and-token) | `useAuthToken`, `useTokenPolling` |
| [Chart data](#chart-data) | `useConsumptionChartData`, `useHashrateChartData`, `usePowerModeTimelineData` |
| [Dashboard](#dashboard) | `useDashboardDateRange`, `useDashboardExport`, `useDashboardTimeRange` |
| [Incidents](#incidents) | `useActiveIncidents` |
| [Pool data](#pool-data) | `usePoolRows`, `usePoolStats` |
| [Site data](#site-data) | `useSiteConsumption`, `useSiteConsumptionChartData`, `useSiteContainerCapacity`, `useSiteEfficiency`, `useSiteHashrate`, `useSiteMinerCounts`, `useSiteMinerStats`, `useSitePowerMeter`, `useSitesOverviewData` |

## Prerequisites

- [Wrap your app in `<MdkProvider>`](/v0-4-0/ui/react/quickstart#wrap-your-app-in-mdkprovider)
- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)

## Import

```tsx
  useActiveIncidents,
  useAuthToken,
  useConsumptionChartData,
  useCurrentAlertDevices,
  useDashboardDateRange,
  useDashboardExport,
  useDashboardTimeRange,
  useHashrateChartData,
  useHistoricalAlerts,
  usePoolRows,
  usePoolStats,
  usePowerModeTimelineData,
  useSiteConsumption,
  useSiteConsumptionChartData,
  useSiteContainerCapacity,
  useSiteEfficiency,
  useSiteHashrate,
  useSiteMinerCounts,
  useSiteMinerStats,
  useSitePowerMeter,
  useSitesOverviewData,
  useTokenPolling,
} from '@tetherto/mdk-react-adapter'
```

## Site data

### `useSitesOverviewData`

Projects raw site-overview container rows and pool stats into a `<PoolManagerSitesOverview />`-ready shape. Each container receives its per-container hashrate in MH/s, an attached pool-stats row, and a `mining` / `offline` status derived from the underlying snapshot.

```tsx
```

#### Options

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `units` | Required | `ContainerUnit[]` | none | **Required.** Raw container rows from the site overview query. |
| `poolStats` | Required | `ContainerPoolStat[]` | none | **Required.** Per-container pool stat rows keyed by container id. |
| `isLoading` | Required | `boolean` | none | **Required.** Combined loading state from the upstream queries. |
| `tailLogItem` | Required | `SitesOverviewTailLogItem` | none | **Required.** Latest tail-log row from a `stat-1m` miner query; pass `_head(_head(rawResponse))`. |

#### Returns

| Member | Type | Description |
|--------|------|-------------|
| `units` | `ProcessedContainerUnit[]` | Processed containers with `hashrateMhs`, `status`, and `poolStats` attached. |
| `isLoading` | `boolean` | Passes through the combined loading state from the options. |

#### Example

```tsx

function SiteOverview({ rawUnits, rawPoolStats, loading, latestTailLog }) {
  const { units, isLoading } = useSitesOverviewData({
    units: rawUnits,
    poolStats: rawPoolStats,
    isLoading: loading,
    tailLogItem: latestTailLog,
  })
  return <PoolManagerSitesOverview units={units} isLoading={isLoading} />
}
```

### `useSiteConsumption`

Projects the freshest power sample from the dashboard's existing tail-log query into a scalar MW value for the header stats strip (`<HeaderConsumptionBox />`). Delegates fetching to [`useSiteConsumptionChartData`](#usesiteconsumptionchartdata).

```tsx
```

#### Options

Accepts the same `UseConsumptionChartDataParams` object as [`useSiteConsumptionChartData`](#usesiteconsumptionchartdata).

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `timeline` | Required | `string` | none | **Required.** Stat key suffix, e.g. `'5m'`. |
| `start` | Optional | `number` | none | Lower time bound (ms epoch). |
| `end` | Optional | `number` | none | Upper time bound (ms epoch). |
| `tag` | Optional | `string` | `'t-miner'` | Thing tag. Use `'t-powermeter'` for transformer-level readings. |
| `powerAttribute` | Optional | `string` | `'power_w_sum_aggr'` | Aggregate field name to read from the tail-log row. |
| `refetchInterval` | Optional | `number` | `60000` | Polling interval in ms. Pass `0` to disable. |

#### Returns

| Member | Type | Description |
|--------|------|-------------|
| `valueMw` | `number \| undefined` | Latest aggregate value in MW; `undefined` while loading or with no data. |
| `valueW` | `number \| undefined` | Raw backend value in watts. |
| `isLoading` | `boolean` | Loading state from the upstream query. |

### `useSiteConsumptionChartData`

TanStack Query hook that fetches site consumption tail-log samples and returns a `<LineChartCard>`-ready `ChartCardData` payload. Applies site-powermeter defaults and converts watts to MW.

```tsx
```

#### Options

Same `UseConsumptionChartDataParams` as [`useSiteConsumption`](#usesiteconsumption).

#### Returns

A TanStack `UseQueryResult` whose `data` field is `ChartCardData` — the object accepted by `<LineChartCard data={...} />`.

#### Example

```tsx

function ConsumptionChart() {
  const { data, isLoading } = useSiteConsumptionChartData({ timeline: '5m' })
  return <LineChartCard data={data} isLoading={isLoading} />
}
```

### `useSiteContainerCapacity`

Reads the aggregated nominal miner capacity across all site containers from the 5-minute tail-log aggregate, giving the denominator shown in the header (e.g. the "2,188" in "MOS / 2,188").

```tsx
```

#### Options

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `refetchInterval` | Optional | `number` | `300000` | Polling interval in ms. Pass `0` to disable. |

#### Returns

| Member | Type | Description |
|--------|------|-------------|
| `value` | `number \| undefined` | Total nominal miner slots across all site containers; `undefined` while loading. |
| `isLoading` | `boolean` | Loading state. |

### `useSiteEfficiency`

Derives site efficiency in W/TH/s by dividing the latest site consumption by the latest hashrate. Both upstream hooks are called internally; pass `powerW` to override the consumption source (e.g. to use a power-meter reading instead).

```tsx
```

#### Options

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `timeline` | Required | `string` | none | **Required.** Stat key suffix forwarded to both upstream hooks. |
| `start` | Optional | `number` | none | Lower time bound (ms epoch). |
| `end` | Optional | `number` | none | Upper time bound (ms epoch). |
| `tag` | Optional | `string` | none | Thing tag override forwarded to the consumption hook. |
| `powerAttribute` | Optional | `string` | none | Aggregate field override forwarded to the consumption hook. |
| `refetchInterval` | Optional | `number` | none | Polling interval in ms forwarded to both upstream hooks. |
| `powerW` | Optional | `number` | none | When provided, skips the internal `useSiteConsumption` call and uses this watts value as the numerator directly. Pair with `useSitePowerMeter().valueW` for meter-level efficiency. |

#### Returns

| Member | Type | Description |
|--------|------|-------------|
| `valueWthS` | `number \| undefined` | Watts per TH/s; `undefined` while loading or when hashrate is zero. |
| `isLoading` | `boolean` | `true` while either upstream hook is loading. |

### `useSiteHashrate`

TanStack Query hook that reads the latest aggregate hashrate from the site tail-log and returns both a PH/s display value and the raw MH/s backend value.

```tsx
```

#### Options

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `timeline` | Required | `string` | none | **Required.** Stat key suffix, e.g. `'5m'`. |
| `start` | Optional | `number` | none | Lower time bound (ms epoch). |
| `end` | Optional | `number` | none | Upper time bound (ms epoch). |
| `refetchInterval` | Optional | `number` | `60000` | Polling interval in ms. Pass `0` to disable. |

#### Returns

| Member | Type | Description |
|--------|------|-------------|
| `valuePhs` | `number \| undefined` | Latest aggregate value in PH/s; `undefined` while loading or with no data. |
| `valueMhs` | `number \| undefined` | Latest aggregate value in MH/s (raw backend unit). |
| `isLoading` | `boolean` | Loading state. |

### `useSiteMinerCounts`

Polls the device list for miners and aggregates them into online / offline / error counts. Uses the `last.status` field on each device row.

```tsx
```

#### Options

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `refetchInterval` | Optional | `number` | none | Polling interval in ms. |

#### Returns

| Member | Type | Description |
|--------|------|-------------|
| `total` | `number` | Total miner device count. |
| `online` | `number` | Miners with status `online` or `on`. |
| `offline` | `number` | Miners not online and not in error. |
| `error` | `number` | Miners with status `error` or `alert`. |

### `useSiteMinerStats`

Polls the `stat-rtd` realtime aggregate for the live miner stat summary — the MOS total, online/offline breakdown, and active pool worker count shown in the header.

```tsx
```

#### Options

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `refetchInterval` | Optional | `number` | none | Polling interval in ms. |

#### Returns

| Member | Type | Description |
|--------|------|-------------|
| `mosTotal` | `number` | Count of miners that reported hashrate in the last minute (the MOS denominator). |
| `online` | `number` | Miners online or with minor errors (green column). |
| `offline` | `number` | Offline or sleeping miners. |
| `notMining` | `number` | Miners not currently mining. |
| `isLoading` | `boolean` | Loading state. |

### `useSitePowerMeter`

Reads the current power-meter reading in watts from the site-level device tagged `t-powermeter`. Returns a MW conversion alongside the raw watts value.

```tsx
```

#### Options

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `tag` | Optional | `string` | `'t-powermeter'` | Device tag to filter by. |
| `refetchInterval` | Optional | `number` | none | Polling interval in ms. |

#### Returns

| Member | Type | Description |
|--------|------|-------------|
| `valueMw` | `number \| undefined` | Latest power-meter reading in MW; `undefined` when no device or no data. |
| `valueW` | `number \| undefined` | Raw value in watts. |
| `isLoading` | `boolean` | Loading state. |

## Pool data

### `usePoolRows`

TanStack Query hook that fetches per-pool stats and transforms them into `PoolRow[]` objects shaped for the pool manager table. Hashrate is normalised from raw H/s to PH/s.

```tsx
```

#### Options

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `refetchInterval` | Optional | `number` | `120000` | Polling interval in ms. Pass `0` to disable. |

#### Returns

A TanStack `UseQueryResult` whose `data` is `PoolRow[]`.

| Member | Type | Description |
|--------|------|-------------|
| `id` | `string` | Stable React key derived from `poolType`. |
| `name` | `string` | Display name in Moria style — `minerpool-{poolType}-shelf-0`. |
| `poolType` | `string` | Raw pool type string, e.g. `'f2pool'`, `'ocean'`. |
| `revenue24hBtc` | `number \| undefined` | 24 h revenue in BTC if reported. |
| `hashrateHs` | `number \| undefined` | Hashrate in raw H/s. |
| `hashratePhsDisplay` | `number \| undefined` | Hashrate in PH/s for display. |
| `details` | `PoolDetail[]` | Array of label/value pairs for the expanded row. |

### `usePoolStats`

TanStack Query hook that fetches per-pool stats and aggregates them into site-level totals — total workers, online workers, mismatch count, and aggregate hashrate in PH/s.

```tsx
```

#### Options

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `refetchInterval` | Optional | `number` | `120000` | Polling interval in ms. Pass `0` to disable. |

#### Returns

| Member | Type | Description |
|--------|------|-------------|
| `total` | `number` | Total miners reported across all configured pools. |
| `online` | `number` | Pool-reported active workers. |
| `mismatch` | `number` | Difference between configured and active workers. |
| `hashratePhs` | `number \| undefined` | Aggregate pool hashrate in PH/s; `undefined` while loading or with no data. |
| `hashrateHs` | `number \| undefined` | Aggregate pool hashrate in raw H/s. |
| `isLoading` | `boolean` | Loading state. |

## Dashboard

### `useDashboardDateRange`

Owns the single source of truth for the dashboard's date-range picker: a `start` / `end` ms-epoch pair with setters and a reset helper that restores the default 24-hour window.

```tsx
```

#### Options

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `initial` | Optional | `{ start: number; end: number }` | last 24 h | Initial date range. Defaults to the last 24 hours ending now. |

#### Returns

| Member | Type | Description |
|--------|------|-------------|
| `start` | `number` | Lower bound (ms epoch). |
| `end` | `number` | Upper bound (ms epoch). |
| `setRange` | `function` | Replace the current range. |
| `reset` | `function` | Convenience: restore the default 24-hour window ending now. |

#### Example

```tsx

function DashboardHeader() {
  const { start, end, setRange, reset } = useDashboardDateRange()
  return <DateRangePicker start={start} end={end} onChange={setRange} onReset={reset} />
}
```

### `useDashboardExport`

Reads cached query data for hashrate, consumption, incidents, and pool stats from the TanStack Query client and exposes `exportCsv`, `exportJson`, and a unified `export(format)` callable. Does not trigger refetches — the export represents exactly what is currently displayed.

```tsx
```

#### Options

Accepts a `UseDashboardExportOptions` object containing `DashboardQueryRange` fields (`timeline`, `start`, `end`, `tag`) needed to reconstruct the query keys.

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `timeline` | Required | `string` | none | **Required.** Stat key suffix used to locate the cached chart queries. |
| `start` | Optional | `number` | none | Lower time bound (ms epoch). |
| `end` | Optional | `number` | none | Upper time bound (ms epoch). |
| `tag` | Optional | `string` | none | Thing tag override. |

#### Returns

| Member | Type | Description |
|--------|------|-------------|
| `exportCsv` | `function` | Triggers a browser file-download of the dashboard data as CSV. |
| `exportJson` | `function` | Triggers a browser file-download as JSON. |
| `export` | `function` | Unified callable — `export('csv')` or `export('json')`. |

### `useDashboardTimeRange`

Tiny piece of scoped state for the dashboard's timeline selector. Owns the current `timeline` string and the canonical option list. Chart hooks (`useHashrateChartData`, `useSiteConsumptionChartData`, etc.) consume `timeline` as a prop.

```tsx
```

#### Options

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `initial` | Optional | `string` | `'5m'` | Initial timeline value. |
| `options` | Optional | `TimelineOption[]` | canonical list | Custom option list; defaults to the canonical set from `getTimelineOptions`. |

#### Returns

| Member | Type | Description |
|--------|------|-------------|
| `timeline` | `string` | Currently selected timeline string, e.g. `'5m'`. |
| `options` | `TimelineOption[]` | Available options, ready for `<TimelineSelector options={...} />`. |
| `setTimeline` | `function` | Setter for the current timeline. |

#### Example

```tsx

function DashboardControls() {
  const { timeline, options, setTimeline } = useDashboardTimeRange()
  const { valuePhs } = useSiteHashrate({ timeline })
  return (
    <>
      <TimelineSelector options={options} value={timeline} onChange={setTimeline} />
      <span>{valuePhs?.toFixed(2)} PH/s</span>
    </>
  )
}
```

## Chart data

### `useConsumptionChartData`

TanStack Query hook returning raw consumption tail-log samples. Most dashboards should use the higher-level [`useSiteConsumptionChartData`](#usesiteconsumptionchartdata), which wraps this hook and returns a `<LineChartCard>`-ready payload.

```tsx
```

#### Options

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `timeline` | Required | `string` | none | **Required.** Stat key suffix, e.g. `'1m'`, `'5m'`. |
| `start` | Optional | `number` | none | Lower time bound (ms epoch). |
| `end` | Optional | `number` | none | Upper time bound (ms epoch). |
| `tag` | Optional | `string` | `'t-miner'` | Thing tag. Use `'t-powermeter'` for transformer-level consumption. |
| `powerAttribute` | Optional | `string` | `'power_w_sum_aggr'` | Aggregate field name to read. |
| `refetchInterval` | Optional | `number` | `60000` | Polling interval in ms. Pass `0` to disable. |

#### Returns

A TanStack `UseQueryResult<TailLogEntry[][], Error>` — raw tail-log matrix before projection.

### `useHashrateChartData`

TanStack Query hook combining the site tail-log hashrate series with the per-pool hashrate history into a single `ChartCardData` payload for `<LineChartCard />`. Handles unit conversion from both MH/s (tail-log) and raw H/s (pool API) into PH/s for display.

```tsx
```

#### Options

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `timeline` | Required | `string` | none | **Required.** Stat key suffix, e.g. `'5m'`. |
| `start` | Optional | `number` | none | Lower time bound (ms epoch). |
| `end` | Optional | `number` | none | Upper time bound (ms epoch). |
| `refetchInterval` | Optional | `number` | none | Polling interval in ms. |

#### Returns

A TanStack `UseQueryResult` whose `data` is `ChartCardData` — ready for `<LineChartCard data={...} />`.

### `usePowerModeTimelineData`

TanStack Query hook returning power-mode and status samples shaped for `<PowerModeTimelineChart data={...} />`.

```tsx
```

#### Options

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `timeline` | Required | `string` | none | **Required.** Stat key suffix, e.g. `'1m'`. |
| `start` | Optional | `number` | none | Lower time bound (ms epoch). |
| `end` | Optional | `number` | none | Upper time bound (ms epoch). |
| `tag` | Optional | `string` | none | Thing tag override. |
| `refetchInterval` | Optional | `number` | none | Polling interval in ms. |

#### Returns

A TanStack `UseQueryResult<PowerModeTimelineEntry[], Error>`.

## Auth and token

### `useAuthToken`

Reads `?authToken=…` from `window.location.search`, persists it into the headless `authStore`, and strips the parameter from the URL via `history.replaceState` so the token never lingers in the address bar. Router-agnostic by design. Returns the current token from the store so callers can gate navigation on authentication.

```tsx
```

#### Returns

| Type | Description |
|------|-------------|
| `string \| null` | Current auth token from the `authStore`, or `null` when no token is present. |

#### Example

```tsx

function ProtectedRoute() {
  const token = useAuthToken()
  return token ? <Outlet /> : <Navigate to="/signin" replace />
}
```

### `useTokenPolling`

Polls the backend token-refresh endpoint at a fixed interval (default 250 s) and writes the new token back into the `authStore`. Fires the optional `onSessionEnded` callback on a 401 or 500, which MDK UI Shell uses to redirect back to `/signin`.

```tsx
```

#### Options

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `intervalMs` | Optional | `number` | `250000` | Polling interval in ms. Backend token TTL defaults to 5 min (300 s); 250 s gives comfortable headroom. |
| `enabled` | Optional | `boolean` | `true` when token present | Pass `false` to pause polling (e.g. on the sign-in page). |
| `onSessionEnded` | Optional | `function` | none | Called on 401 or 500; use to navigate to the sign-in page. |

#### Example

```tsx

function App() {
  const navigate = useNavigate()
  useTokenPolling({ onSessionEnded: () => navigate('/signin') })
  return <Outlet />
}
```

## Incidents

### `useActiveIncidents`

TanStack Query hook returning the list of currently-firing alerts, shaped for `<ActiveIncidentsCard items={...} />`. Queries devices that have a non-null `last.alerts` field and maps them via the `mapDevicesToIncidents` utility.

```tsx
```

#### Options

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `refetchInterval` | Optional | `number` | `20000` | Polling interval in ms. Matches Moria's production cadence. Pass `0` to disable. |
| `formatDate` | Optional | `function` | ISO string | Date formatter applied to the row body timestamp. |

#### Returns

A TanStack `UseQueryResult<IncidentRow[], Error>`.

#### Example

```tsx

function IncidentsFeed() {
  const { data: items = [], isLoading } = useActiveIncidents({ refetchInterval: 20_000 })
  return <ActiveIncidentsCard items={items} isLoading={isLoading} />
}
```

## Alerts

Data hooks that back the full alerts surface: `useCurrentAlertDevices` feeds the `<CurrentAlerts>` / `<Alerts>` table with raw device rows; `useHistoricalAlerts` fetches and shapes the historical-alerts log for `<HistoricalAlerts>`. Both require [`<MdkProvider>`](/v0-4-0/ui/react/quickstart#wrap-your-app-in-mdkprovider) in the tree.

<Callout type="info">See also [`useActiveIncidents`](#useactiveincidents) in the Incidents section, which hits the same `list-things` endpoint but maps results to the dashboard card's `IncidentRow[]` shape.</Callout>

### `useCurrentAlertDevices`

TanStack Query hook returning the raw devices that currently carry one or more alerts, as the nested `ListThingsDevice[][]` envelope the devkit `<Alerts>` / `<CurrentAlerts>` table expects. Unlike `useActiveIncidents`, this hook leaves the payload unshaped so the table can derive its own filter tokens and per-row status. Both hit `/auth/list-things`; this hook requests a wider field set and uses a distinct cache key.

```tsx
```

#### Options

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `refetchInterval` | Optional | `number` | `20000` | Polling interval in ms. Matches the default production polling cadence. Pass `0` to disable. |
| `filterTags` | Optional | `string[]` | none | Alerts search chips. Folded into the backend `list-things` selector — triggers a re-fetch that narrows the dataset server-side. |

#### Returns

A TanStack `UseQueryResult<ListThingsDevice[][], Error>`.

#### Example

```tsx

function AlertsTable({ filterTags }) {
  const { data, isLoading } = useCurrentAlertDevices({ filterTags })
  return <CurrentAlerts devices={data ?? []} isLoading={isLoading} filterTags={filterTags} />
}
```

### `useHistoricalAlerts`

TanStack Query hook for the historical-alerts log. Fetches the `[start, end]` range as successive `history-log` windows, merges results by `uuid`, and shapes rows for the devkit `<HistoricalAlerts>` table via `mapHistoryLogToAlerts`. Range changes abort the in-flight chunk loop through the query's `AbortSignal`.

By default the entire range is fetched in a single request. Pass a smaller `intervalMs` (e.g. `ONE_DAY_MS`) only for wide ranges where the backend would otherwise cap or slow a single query — that splits the fetch into sequential 24-hour windows.

```tsx
```

#### Options

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `start` | Required | `number` | none | Lower bound of the look-back window (ms epoch). |
| `end` | Required | `number` | none | Upper bound of the look-back window (ms epoch). |
| `intervalMs` | Optional | `number` | whole range | Per-request window size in ms. Defaults to the whole range in one request; set a smaller value to chunk wide ranges into sequential fetches. |
| `enabled` | Optional | `boolean` | `true` when range valid | Pass `false` to force-disable the query. Defaults to enabled when `start` and `end` are finite and `end > start`. |

#### Returns

A TanStack `UseQueryResult<HistoricalAlert[], Error>`.

#### Example

```tsx

function AlertHistory({ start, end, filterTags, localFilters, onDateRangeChange }) {
  const { data: alerts = [], isLoading } = useHistoricalAlerts({ start, end })
  return (
    <HistoricalAlerts
      alerts={alerts}
      isLoading={isLoading}
      filterTags={filterTags}
      localFilters={localFilters}
      dateRange={{ start, end }}
      onDateRangeChange={onDateRangeChange}
    />
  )
}
```

#### Behavior notes

- Wide ranges fan out into many requests. The MDK UI Shell page caps the default window at 14 days; pass a tighter `[start, end]` or set `intervalMs` to limit per-request payload size.
- The hook removes duplicate rows by `uuid` after merging chunks, before passing them to `mapHistoryLogToAlerts`.

# State hooks (/v0-4-0/reference/app-toolkit/hooks/state)

<PackageBadge>@tetherto/mdk-react-adapter</PackageBadge>

State hooks bind the framework-agnostic [`@tetherto/mdk-ui-core`](/v0-4-0/reference/app-toolkit/ui-core) Zustand stores into React. Each hook subscribes the component to its store and re-renders only when the selected slice changes.

Use these when you want headless state with your own components. The [Developer entry points](/v0-4-0/concepts/stack/app-toolkit#developer-entry-points) table compares adoption layers.

## Prerequisites

- [Wrap your app in `<MdkProvider>`](/v0-4-0/ui/react/quickstart#wrap-your-app-in-mdkprovider)
- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)

## Import

```tsx
  useActions,
  useAuth,
  useDevices,
  useNotifications,
  useTimezone,
} from '@tetherto/mdk-react-adapter'
```

## `useAuth`
<PackageBadge>@tetherto/mdk-react-adapter</PackageBadge>

React-bound view of the headless [`authStore`](/v0-4-0/reference/app-toolkit/ui-core). Exposes `token` and `permissions`; equivalent to `useStore(authStore)` with React subscription semantics.

```tsx
```

### Example

```tsx
function SessionStatus() {
  const { token } = useAuth()

  if (!token) return <p>Sign in to continue</p>
  return <p>Active session</p>
}
```

## `useDevices`
<PackageBadge>@tetherto/mdk-react-adapter</PackageBadge>

React-bound view of the headless [`devicesStore`](/v0-4-0/reference/app-toolkit/ui-core). Exposes the device list, the currently selected devices, and helpers to mutate selection.

```tsx
```

### Example

```tsx
function DeviceToolbar() {
  const { selectedDevices, setSelectedDevices } = useDevices()

  return (
    <div>
      <p>Selected: {selectedDevices.length}</p>
      <Button onClick={() => setSelectedDevices([])}>Clear selection</Button>
    </div>
  )
}
```

## `useTimezone`
<PackageBadge>@tetherto/mdk-react-adapter</PackageBadge>

React-bound view of the headless [`timezoneStore`](/v0-4-0/reference/app-toolkit/ui-core). Read or update the operator's timezone preference (IANA identifier). Use [`useTimezoneFormatter`](/v0-4-0/reference/app-toolkit/hooks/utilities#usetimezoneformatter) when you also need date-formatting helpers.

```tsx
```

### Example

```tsx
function TimezonePicker() {
  const { timezone, setTimezone } = useTimezone()
  return (
    <select value={timezone} onChange={(e) => setTimezone(e.target.value)}>
      <option value="UTC">UTC</option>
      <option value="America/New_York">America/New_York</option>
      <option value="Europe/London">Europe/London</option>
    </select>
  )
}
```

## `useNotifications`
<PackageBadge>@tetherto/mdk-react-adapter</PackageBadge>

React-bound view of the headless [`notificationStore`](/v0-4-0/reference/app-toolkit/ui-core). Exposes the unread counter (`count`) plus `increment`, `decrement`, and `reset`. For the user-facing toast surface use [`useNotification`](/v0-4-0/reference/app-toolkit/hooks/components#usenotification) from foundation.

```tsx
```

### Example

```tsx

function UnreadBadge() {
  const { count } = useNotifications()
  return <Badge count={count} />
}
```

## `useActions`
<PackageBadge>@tetherto/mdk-react-adapter</PackageBadge>

React-bound view of the headless [`actionsStore`](/v0-4-0/reference/app-toolkit/ui-core). Exposes the pending operator submission queue plus helpers like `setAddPendingSubmissionAction` and `removePendingSubmissionAction`.

```tsx
```

### Example

```tsx
function SubmissionTracker() {
  const { pendingSubmissions, setAddPendingSubmissionAction } = useActions()

  return (
    <div>
      <p>{pendingSubmissions.length} pending submissions</p>
      <Button onClick={() => setAddPendingSubmissionAction({ id: 'demo', kind: 'restart' })}>
        Queue restart
      </Button>
    </div>
  )
}
```

# Utility hooks (/v0-4-0/reference/app-toolkit/hooks/utilities)

<PackageBadge>@tetherto/mdk-react-adapter</PackageBadge> + <PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Generic React helpers, mining-domain transforms, permission checks, device/IP utilities, and a curated set of TanStack Query re-exports. Use these alongside [State hooks](/v0-4-0/reference/app-toolkit/hooks/state) to wire app concerns (permissions, devices, viewport, formatting) without depending on MDK styled components.

## At a glance

| Sub-group | Hooks |
|---|---|
| [Permissions](#permissions) | `useCheckPerm`, `useHasPerms`, `useIsFeatureEditingEnabled` |
| [Generic React](#generic-react) | `useLocalStorage`, `useKeyDown`, `useWindowSize`, `usePlatform`, `useDeviceResolution`, `useBeepSound`, `usePagination`, `useSubtractedTime`, `useTimezoneFormatter` |
| [Device and IP](#device-and-ip) | `usePduViewer` |
| [Domain transforms](#domain-transforms) | `useCostSummary`, `useHashBalance`, `useSubsidyFees`, `useUpdateExistedActions` |
| [TanStack Query re-exports](#tanstack-query-re-exports) | `useQuery`, `useMutation`, `useQueries`, `useInfiniteQuery`, `useIsFetching`, `useIsMutating`, `useQueryClient` |

## Prerequisites

- [Wrap your app in `<MdkProvider>`](/v0-4-0/ui/react/quickstart#wrap-your-app-in-mdkprovider)
- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)

## Import

```tsx
  useBeepSound,
  useCheckPerm,
  useDeviceResolution,
  useHasPerms,
  useIsFeatureEditingEnabled,
  useKeyDown,
  useLocalStorage,
  usePagination,
  usePduViewer,
  usePlatform,
  useSubtractedTime,
  useTimezoneFormatter,
  useWindowSize,
} from '@tetherto/mdk-react-adapter'

  useCostSummary,
  useHashBalance,
  useSubsidyFees,
  useUpdateExistedActions,
} from '@tetherto/mdk-react-devkit/foundation'
```

## Permissions

### `useCheckPerm`
<PackageBadge>@tetherto/mdk-react-adapter</PackageBadge>

Check if the current user has a specific permission. Reads `permissions` from the adapter [`authStore`](/v0-4-0/reference/app-toolkit/hooks/state#useauth). Prefer this over `useHasPerms` for single-permission gates.

```tsx
```

#### Example

```tsx
function EditDevicesButton() {
  const canEdit = useCheckPerm('devices:edit')
  return canEdit ? <Button>Edit devices</Button> : null
}
```

### `useHasPerms`
<PackageBadge>@tetherto/mdk-react-adapter</PackageBadge>

Returns a callable that accepts a permission request — a string, a string array (first match wins), or a check object. Reads from the adapter [`authStore`](/v0-4-0/reference/app-toolkit/hooks/state#useauth).

```tsx
```

#### Example

```tsx
function ContextMenu({ device }) {
  const hasPerms = useHasPerms()
  return (
    <Menu>
      {hasPerms(['devices:edit', 'devices:admin']) && <MenuItem>Edit</MenuItem>}
      {hasPerms('devices:delete') && <MenuItem>Delete</MenuItem>}
    </Menu>
  )
}
```

### `useIsFeatureEditingEnabled`
<PackageBadge>@tetherto/mdk-react-adapter</PackageBadge>

Returns whether the current user has the `features` capability to edit feature flags.

```tsx
```

#### Example

```tsx
function FeatureFlagRow({ flag }) {
  const canEdit = useIsFeatureEditingEnabled()
  return <Switch disabled={!canEdit} checked={flag.enabled} />
}
```

## Generic React

### `useLocalStorage`
<PackageBadge>@tetherto/mdk-react-adapter</PackageBadge>

Type-safe `localStorage` access with cross-tab synchronization. Returns `[value, setValue, removeValue]` and stays in sync across browser tabs via the `storage` event.

```tsx
```

#### Example

```tsx
function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('app:theme', 'light')
  return <Button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle</Button>
}
```

### `useKeyDown`
<PackageBadge>@tetherto/mdk-react-adapter</PackageBadge>

Track whether a specific keyboard key is currently pressed. Attaches global `keydown`/`keyup` listeners. Useful for modifier-key interactions like shift-click multi-select.

```tsx
```

#### Example

```tsx
function MinerGrid({ miners }) {
  const shiftHeld = useKeyDown('Shift')
  return miners.map((m) => (
    <MinerCard key={m.id} miner={m} onClick={() => select(m, { range: shiftHeld })} />
  ))
}
```

### `useWindowSize`
<PackageBadge>@tetherto/mdk-react-adapter</PackageBadge>

Track window width and height, refreshing on `resize`. Returns `{ windowWidth, windowHeight }`. Use [`useDeviceResolution`](#usedeviceresolution) when you only need a device-class branch rather than raw pixels.

```tsx
```

#### Example

```tsx
function ResponsiveChart() {
  const { windowWidth } = useWindowSize()
  return <LineChart width={Math.min(windowWidth - 32, 960)} />
}
```

### `usePlatform`
<PackageBadge>@tetherto/mdk-react-adapter</PackageBadge>

Detect the host platform (iOS, Android, Mac, Windows, Linux) from the user agent. Returns the value matching the exported `OS_TYPES` constant; pair with `detectPlatform` for one-off checks outside React.

```tsx
```

#### Example

```tsx
function PlatformBadge() {
  const platform = usePlatform()
  return <span>Running on {platform}</span>
}
```

### `useDeviceResolution`
<PackageBadge>@tetherto/mdk-react-adapter</PackageBadge>

Map window width to a device class (`mobile`, `tablet`, `desktop`) using the shared `BREAKPOINTS` constant. Cheaper than re-reading pixels in every render.

```tsx
```

#### Example

```tsx
function Layout({ children }) {
  const device = useDeviceResolution()
  return device === 'mobile' ? <Stack>{children}</Stack> : <SidebarLayout>{children}</SidebarLayout>
}
```

### `useBeepSound`
<PackageBadge>@tetherto/mdk-react-adapter</PackageBadge>

Play a repeating beep when `isAllowed` is true. Configurable volume and interval (`delayMs`). The alarm is synthesised via the Web Audio API — no audio asset is bundled or fetched. Designed for audible critical alerts (overheating containers, equipment failure).

```tsx
```

#### Example

```tsx
function CriticalAlertChime({ active }) {
  useBeepSound({ isAllowed: active, volume: 0.6, delayMs: 1500 })
  return null
}
```

### `usePagination`
<PackageBadge>@tetherto/mdk-react-adapter</PackageBadge>

Manage pagination state and produce `{ limit, offset }` query arguments for API calls. Returns state shaped for the devkit `<Pagination>` component plus helpers to change page, page size, and total count.

```tsx
```

#### Options

| Option | Status | Type | Default | Description |
|--------|--------|------|---------|-------------|
| `current` | Optional | `number` | `1` | Initial 1-indexed page |
| `pageSize` | Optional | `number` | `20` | Initial rows per page |
| `total` | Optional | `number` | none | Initial total row count |
| `showSizeChanger` | Optional | `boolean` | `true` | Whether the UI exposes a page-size selector |

#### Returns

| Member | Type | Description |
|--------|------|-------------|
| `pagination` | `PaginationState` | `{ current, pageSize, showSizeChanger, total }` — spread onto `<Pagination>` |
| `queryArgs` | `{ limit, offset }` | Query arguments for API calls |
| `handleChange` | `function` | `(page, pageSize) => void` — pass to `<Pagination onChange={…}>` |
| `setPagination` | `function` | Imperative pagination state update |
| `reset` | `function` | Reset to initial state |
| `setTotal` | `function` | Update total row count |
| `hideNextPage` | `function` | Hide next page when the current page has fewer rows than `pageSize` |

#### Example

```tsx
function MinerList() {
  const { pagination, queryArgs, handleChange } = usePagination({ current: 1, pageSize: 25 })
  const { data } = useQuery(['miners', queryArgs], () => fetchMiners(queryArgs))

  return (
    <>
      <Table rows={data?.rows ?? []} />
      <Pagination {...pagination} onChange={handleChange} />
    </>
  )
}
```

### `useSubtractedTime`
<PackageBadge>@tetherto/mdk-react-adapter</PackageBadge>

Returns `Date.now() - diff`, refreshing on a fixed interval (default 5s). Useful for "synced N seconds ago" labels without forcing tree-wide re-renders.

```tsx
```

#### Example

```tsx
function LastSyncedLabel({ lastSyncOffsetMs }) {
  const now = useSubtractedTime(lastSyncOffsetMs)
  return <small>Synced {formatDistanceToNow(now)} ago</small>
}
```

### `useTimezoneFormatter`
<PackageBadge>@tetherto/mdk-react-adapter</PackageBadge>

Read the app timezone from the adapter [`timezoneStore`](/v0-4-0/reference/app-toolkit/hooks/state#usetimezone) and format dates for display. Use when foundation components or app code need timestamps in the operator-selected timezone (alerts, pool manager, dashboard widgets).

```tsx
```

#### Example

```tsx
function AlertTimestamp({ raisedAt }) {
  const { getFormattedDate } = useTimezoneFormatter()
  return <time>{getFormattedDate(raisedAt)}</time>
}
```

## Device and IP

### `usePduViewer`
<PackageBadge>@tetherto/mdk-react-adapter</PackageBadge>

Pan/zoom controller for the PDU floor-plan viewer. Wraps `react-zoom-pan-pinch` with viewport-aware reset logic and a debounced "back to content" indicator that appears when the user pans the layout off-screen.

```tsx
```

#### Example

```tsx

function PduFloorPlan() {
  const { onViewerInit, showBackToContent, handleBackToContent } = usePduViewer()
  return (
    <>
      <TransformWrapper onInit={onViewerInit}>
        <TransformComponent>{/* …diagram… */}</TransformComponent>
      </TransformWrapper>
      {showBackToContent && <Button onClick={handleBackToContent}>Back to content</Button>}
    </>
  )
}
```

## Domain transforms

### `useCostSummary`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Base hook for the cost-summary reporting page (single-site mode).

```tsx
```

#### Example

```tsx

// Wire your query result in; consume queryParams to drive the fetch
function CostSummaryPage({ query }) {
  const { datePicker, queryParams, isLoading, error } = useCostSummary({ query })

  // Pass queryParams to your data-fetching layer whenever the date range changes
  // e.g. useGetCostSummaryQuery(queryParams, { skip: !queryParams })

  return (
    <div>
      {datePicker}
      {isLoading && <p>Loading…</p>}
      {error  && <p role="alert">Failed to load cost data</p>}
    </div>
  )
}
```

### `useHashBalance`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Derives hash-balance metrics and chart datasets from finance log entries for the active date range, currency, and timeframe type. Used by hash balance panels.

```tsx
```

#### Example

```tsx

function HashBalancePanel({ data, log, currency, dateRange }) {
  const {
    siteHashRevenueChartData,
    networkHashpriceChartData,
    combinedCostChartData,
  } = useHashBalance({ data, log, currency, dateRange })

  return (
    <div>
      {/* Pass chart datasets to your BarChart components */}
      <pre>{JSON.stringify(siteHashRevenueChartData?.labels, null, 2)}</pre>
      <pre>{JSON.stringify(networkHashpriceChartData?.labels, null, 2)}</pre>
      <pre>{JSON.stringify(combinedCostChartData?.labels, null, 2)}</pre>
    </div>
  )
}
```

### `useSubsidyFees`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Aggregates raw subsidy-fee log entries into chart-ready datasets keyed by the active period type (day / week / month / year) and surfaces a summary for the matching reporting widgets. Used by `Subsid…

```tsx
```

#### Example

```tsx

function SubsidyFeesPanel({ data, dateRange }) {
  const { summary, subsidyFeesChartData, averageFeesChartData, isEmpty } = useSubsidyFees({
    data,
    dateRange,
  })

  if (isEmpty) return <p>No subsidy fee data for this period.</p>

  return (
    <div>
      <p>Total fees: {(summary as any)?.total ?? '—'}</p>
      {/* Pass chart datasets to your BarChart components */}
      <pre>{JSON.stringify(subsidyFeesChartData?.labels, null, 2)}</pre>
      <pre>{JSON.stringify(averageFeesChartData?.labels, null, 2)}</pre>
    </div>
  )
}
```

### `useUpdateExistedActions`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Mutation hook that updates only the changed fields of an existing action record.

```tsx
```

#### Example

```tsx

function DeviceActionBar({ actionType, pendingSubmissions, selectedDevices }) {
  const { updateExistedActions } = useUpdateExistedActions()

  const handleApply = () => {
    updateExistedActions({ actionType, pendingSubmissions, selectedDevices })
  }

  return (
    <Button onClick={handleApply} disabled={selectedDevices.length === 0}>
      Apply to selected ({selectedDevices.length})
    </Button>
  )
}
```

## TanStack Query re-exports

The adapter re-exports a curated set of [TanStack Query](https://tanstack.com/query/latest/docs/framework/react/overview) hooks so you can import data-fetching primitives from a single package alongside MDK helpers. The re-exports are unmodified — refer to the upstream TanStack Query documentation for full API details.

| Hook | TanStack docs |
| --- | --- |
| `useQuery` | [TanStack `useQuery`](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery) |
| `useMutation` | [TanStack `useMutation`](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation) |
| `useQueries` | [TanStack `useQueries`](https://tanstack.com/query/latest/docs/framework/react/reference/useQueries) |
| `useInfiniteQuery` | [TanStack `useInfiniteQuery`](https://tanstack.com/query/latest/docs/framework/react/reference/useInfiniteQuery) |
| `useIsFetching` | [TanStack `useIsFetching`](https://tanstack.com/query/latest/docs/framework/react/reference/useIsFetching) |
| `useIsMutating` | [TanStack `useIsMutating`](https://tanstack.com/query/latest/docs/framework/react/reference/useIsMutating) |
| `useQueryClient` | [TanStack `useQueryClient`](https://tanstack.com/query/latest/docs/framework/react/reference/useQueryClient) |

# UI CLI reference (/v0-4-0/reference/app-toolkit/ui-cli)

The **UI CLI** (`mdk-ui`, package `@tetherto/mdk-ui-cli`) is the command surface your AI agent uses to build with MDK. You usually never run it yourself, your agent does, after you [wire your IDE](/v0-4-0/agents). This page documents the commands for when you want to drive or inspect the tooling by hand.

<Callout type="info">
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.
</Callout>

## 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-core`:

```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](/v0-4-0/agents): the two-step flow most developers use
- [UI Kit](/v0-4-0/ui): the component library these commands draw from
- [MDK repositories](/v0-4-0/resources/repositories): source for the UI CLI and the agent-ready contract

# UI Core (/v0-4-0/reference/app-toolkit/ui-core)

<PackageBadge>@tetherto/mdk-ui-core</PackageBadge>

<Callout type="info">
If you are building a React app with the MDK kit, start with the [React adapter](/v0-4-0/ui/react/get-started) instead. `<MdkProvider>` and 
adapter hooks such as `useAuth` and `useDevices` wrap this package so most React code never imports `@tetherto/mdk-ui-core` directly.

Use this reference when you need headless store access outside React (logging, websocket setup, test helpers) or when authoring a future framework adapter.
</Callout>

`@tetherto/mdk-ui-core` is the framework-agnostic headless layer of the [MDK App Toolkit](/v0-4-0/concepts/stack/app-toolkit). It ships 
Zustand vanilla stores and a TanStack Query Core `QueryClient` factory. There are **no React imports** in this package.

## Subpath exports

| Subpath | Purpose |
|---------|---------|
| `.` | Top-level barrel |
| `./store` | Zustand vanilla stores |
| `./query` | `QueryClient` factory, query keys, and query/mutation factories |
| `./types` | Shared type contracts |
| `./stores.json` | Machine-readable store manifest (generated at build time) |

## Stores

Each store is a Zustand vanilla singleton. In a React app, read and update state through the matching adapter hook instead of importing stores directly.

| Store | Summary | Adapter hook |
|-------|---------|--------------|
| `authStore` | Session token and permission payload | [`useAuth`](/v0-4-0/reference/app-toolkit/hooks/state#useauth) |
| `devicesStore` | Fleet device list and current selection | [`useDevices`](/v0-4-0/reference/app-toolkit/hooks/state#usedevices) |
| `timezoneStore` | Active operator timezone | [`useTimezone`](/v0-4-0/reference/app-toolkit/hooks/state#usetimezone) |
| `notificationStore` | Unread notification counter (`count`, `increment`, `decrement`, `reset`) | [`useNotifications`](/v0-4-0/reference/app-toolkit/hooks/state#usenotifications) |
| `actionsStore` | Pending device and pool action submissions | [`useActions`](/v0-4-0/reference/app-toolkit/hooks/state#useactions) |

Import from `@tetherto/mdk-ui-core/store` (or the top-level barrel).

## QueryClient factory

`createMdkQueryClient` builds a TanStack Query Core client with environment-aware App Node base URL resolution:

1. Explicit override (typically from `<MdkProvider>`)
2. Build-time env: `VITE_MDK_API_URL` (Vite) or `MDK_API_URL` (Node)
3. Default: `http://localhost:3000`

The `./query` subpath also exports query key helpers and factories (`authQuery`, `devicesQuery`, `deviceQuery`, `telemetryQuery`). 
See [TanStack Query](https://tanstack.com/query/latest) for general usage.

## Headless read outside React

Utility code can subscribe to a store without React:

```ts

const token = authStore.getState().token

const unsubscribe = authStore.subscribe((state) => {
  console.log('token changed', state.token)
})

// later: unsubscribe()
```

`<MdkProvider>` wires these singleton stores into React and creates the shared `QueryClient` for adapter hooks.

## What's not here yet

Throttled telemetry subscriptions, stale detection, and history ring buffers are not yet present. They will be added alongside the consuming code that requires them.

## Next steps

- [React get started](/v0-4-0/ui/react/get-started): three-package install and `<MdkProvider>`
- [Wire a React app](/v0-4-0/tutorials/ui/react/tutorial): full adapter wiring walkthrough
- [MDK App Toolkit](/v0-4-0/concepts/stack/app-toolkit): where UI Core fits in the frontend stack

# UI Kit (/v0-4-0/reference/app-toolkit/ui-kit)

The UI Kit is the frontend tools half of the [MDK App Toolkit](/v0-4-0/concepts/stack/app-toolkit). This reference documents
its constants, React hooks, TypeScript types, and helper utilities.

## Browse by topic

| Topic | What's there |
|-------|--------------|
| [Constants](/v0-4-0/reference/app-toolkit/ui-kit/constants) | Colors, units, currency, chart configs and permissions, roles, header preferences|
| [Hooks](/v0-4-0/reference/app-toolkit/ui-kit/hooks) | Discover hooks for monitoring and UI patterns; full reference at [Hooks](/v0-4-0/reference/app-toolkit/hooks) |
| [Types](/v0-4-0/reference/app-toolkit/ui-kit/types) | TypeScript type exports. UI primitives and domain models like `Device`, `Alert` |
| [Utilities](/v0-4-0/reference/app-toolkit/ui-kit/utilities) | Helper functions for formatting, validation, conversions, and settings persistence |

## Browse by package

If you're working with a specific package, use these per-package shortcuts:

### `@tetherto/mdk-react-devkit/core`

- [Constants](/v0-4-0/reference/app-toolkit/ui-kit/constants#core-constants)
- [Types](/v0-4-0/reference/app-toolkit/ui-kit/types#core-types)
- [Utilities](/v0-4-0/reference/app-toolkit/ui-kit/utilities#core-utilities)

### `@tetherto/mdk-react-devkit/foundation`

- [Constants](/v0-4-0/reference/app-toolkit/ui-kit/constants#foundation-constants)
- [Types](/v0-4-0/reference/app-toolkit/ui-kit/types#foundation-types)
- [Utilities](/v0-4-0/reference/app-toolkit/ui-kit/utilities#foundation-utilities)
- [Hooks](/v0-4-0/reference/app-toolkit/hooks) — every App Toolkit hook (state, components, utilities) lives on a single overview page now.

### `@tetherto/mdk-ui-core`

- [Alert utilities](/v0-4-0/reference/app-toolkit/ui-kit/utilities#ui-core-alert-utilities) — alert query-parameter builders and time-range chunking helpers

# Constants (/v0-4-0/reference/app-toolkit/ui-kit/constants)

This page documents constants exported by the MDK packages. Constants live in two distinct domains:

- [Core constants](#core-constants) ships UI primitives: colors, units, currency symbols, chart defaults
- [Foundation constants](#foundation-constants) ships mining-domain values: permissions, roles, header preferences, error codes

## Core constants

UI primitive constants exported by `@tetherto/mdk-react-devkit/core` — colors, units, currency symbols, and chart defaults.

### Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/core#prerequisites)

### Import
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

```tsx
  COLOR,
  UNITS,
  CURRENCY,
  CHART_COLORS,
  TABLE_COLORS,
  HASHRATE_LABEL_DIVISOR,
} from '@tetherto/mdk-react-devkit/core'
```

### Color constants

#### `COLOR`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Comprehensive color palette with 80+ named colors.

```tsx

COLOR.GREEN        // '#72F59E'
COLOR.RED          // '#EF4444'
COLOR.COLD_ORANGE  // '#F7931A'
```

##### Base colors

| Constant | Value | Description |
|----------|-------|-------------|
| `WHITE` | `#FFFFFF` | Pure white |
| `BLACK` | `#17130F` | Standard black |
| `DARK_BACK` | `#1A1815` | Dark background |
| `EBONY` | `#0f0f0f` | Chart background |
| `TRANSPARENT` | `transparent` | Transparent |

##### Status colors

| Constant | Value | Description |
|----------|-------|-------------|
| `GREEN` | `#72F59E` | Success/online |
| `RED` | `#EF4444` | Error/danger |
| `YELLOW` | `#FFC107` | Warning |
| `BRIGHT_YELLOW` | `#EAB308` | Bright warning |
| `LIGHT_BLUE` | `#22AFFF` | Info |
| `SLEEP_BLUE` | `#3B82F6` | Sleep/standby |

##### Brand colors

| Constant | Value | Description |
|----------|-------|-------------|
| `COLD_ORANGE` | `#F7931A` | Bitcoin orange |
| `ORANGE` | `#FF6A00` | Primary orange |
| `EMERALD` | `#009393` | Teal accent |
| `INDIGO` | `#5B5FFB` | Purple accent |

#### `TABLE_COLORS`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Colors for table styling.

```tsx

TABLE_COLORS.HEADER_BG
TABLE_COLORS.ROW_HOVER
```

#### `HEATMAP`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

`HEATMAP` color scale for temperature and intensity displays.

```tsx
```

#### `CHART_COLORS`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Default chart color palette.

```tsx
```

#### `PIE_CHART_COLORS`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Color palette for pie and doughnut charts.

```tsx
```

#### `CATEGORICAL_COLORS`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

25-color categorical palette for multi-series charts.

```tsx

CATEGORICAL_COLORS[0]  // First color
CATEGORICAL_COLORS[24] // Last color
```

#### `TEMPERATURE_COLORS`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Color scale for temperature displays.

```tsx
```

#### `SOCKET_BORDER_COLOR`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Colors for socket status indicators.

```tsx
```

### Unit constants

#### `UNITS`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Physical and measurement units.

```tsx

UNITS.POWER_W         // 'W'
UNITS.POWER_KW        // 'kW'
UNITS.ENERGY_MWH      // 'MWh'
UNITS.TEMPERATURE_C   // '°C'
UNITS.HASHRATE_TH_S   // 'TH/s'
```

| Constant | Value | Description |
|----------|-------|-------------|
| `POWER_W` | `W` | Watts |
| `POWER_KW` | `kW` | Kilowatts |
| `ENERGY_WH` | `Wh` | Watt-hours |
| `ENERGY_KWH` | `kWh` | Kilowatt-hours |
| `ENERGY_MW` | `MW` | Megawatts |
| `ENERGY_MWH` | `MWh` | Megawatt-hours |
| `ENERGY_GWH` | `GWh` | Gigawatt-hours |
| `TEMPERATURE_C` | `°C` | Celsius |
| `VOLTAGE_V` | `V` | Volts |
| `AMPERE` | `A` | Amperes |
| `PERCENT` | `%` | Percentage |
| `PRESSURE_BAR` | `bar` | Pressure (bar) |
| `HASHRATE_MH_S` | `MH/s` | Megahash/second |
| `HASHRATE_TH_S` | `TH/s` | Terahash/second |
| `HASHRATE_PH_S` | `PH/s` | Petahash/second |
| `HASHRATE_EH_S` | `EH/s` | Exahash/second |
| `FREQUENCY_MHZ` | `MHz` | Megahertz |
| `FREQUENCY_HERTZ` | `Hz` | Hertz |
| `HUMIDITY_PERCENT` | `%RH` | Relative humidity |
| `EFFICIENCY_W_PER_TH` | `W/TH` | Watts per terahash |
| `FLOW_M3H` | `m3/h` | Flow rate |
| `SATS` | `Sats` | Satoshis |
| `VBYTE` | `vByte` | Virtual bytes |

#### `CURRENCY`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Currency symbols and labels.

```tsx

CURRENCY.BTC       // '₿'
CURRENCY.USD       // '$'
CURRENCY.EUR       // '€'
CURRENCY.SATS      // 'Sats'
CURRENCY.BTC_LABEL // 'BTC'
CURRENCY.USD_LABEL // 'USD'
```

#### `MAX_UNIT_VALUE`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Maximum values for certain units.

```tsx

MAX_UNIT_VALUE.HUMIDITY_PERCENT     // 100
MAX_UNIT_VALUE.TEMPERATURE_PERCENT  // 100
```

#### `HASHRATE_LABEL_DIVISOR`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Divisors for converting hashrate units.

```tsx

HASHRATE_LABEL_DIVISOR['TH/s']  // 1e6
HASHRATE_LABEL_DIVISOR['PH/s']  // 1e9
HASHRATE_LABEL_DIVISOR['EH/s']  // 1e12
```

### Chart constants

#### `defaultChartColors`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Default color array for chart datasets.

```tsx
```

#### `defaultChartOptions`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Default [Chart.js](https://www.chartjs.org/) options.

```tsx
```

#### `CHART_LEGEND_OPACITY`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Opacity values for chart legends.

```tsx
```

#### `CHART_PERFORMANCE`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Performance threshold constants for charts.

```tsx
```

#### `getChartAnimationConfig`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Get animation configuration based on data count.

```tsx

const animConfig = getChartAnimationConfig(dataPointCount)
```

#### `getDataDecimationConfig`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Get data decimation configuration for large datasets.

```tsx

const decimationConfig = getDataDecimationConfig(dataPointCount)
```

### Type exports

The constants module also exports TypeScript types:

```tsx
  UnitKey,
  UnitValue,
  CurrencyKey,
  CurrencyValue,
} from '@tetherto/mdk-react-devkit/core'
```

## Foundation constants

Constants exported by `@tetherto/mdk-react-devkit/foundation`: app identity, dialog flows, header preferences, permissions, roles, and error codes.

### Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)

### Import
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

```tsx
  WEBAPP_NAME,
  WEBAPP_SHORT_NAME,
  WEBAPP_DISPLAY_NAME,
  POSITION_CHANGE_DIALOG_FLOWS,
  HEADER_ITEMS,
  DEFAULT_HEADER_PREFERENCES,
  AUTH_PERMISSIONS,
  AUTH_LEVELS,
  USER_ROLE,
  USER_ROLES,
  PERM_LEVEL_LABELS,
  getRoleBadgeColors,
} from '@tetherto/mdk-react-devkit/foundation'
```

### App identity

Brand strings the foundation UI uses to label the running web app in header rows, chart legends, settings export errors, and confirmation copy. The kit ships placeholder defaults; consumers read them via the named imports.

#### `WEBAPP_NAME`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Inline name used in confirmations and the `parseSettingsFile` import-error message.

```tsx

WEBAPP_NAME  // 'Appl.'
```

#### `WEBAPP_SHORT_NAME`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Compact label embedded in [`HEADER_ITEMS`](#header_items) for the in-app miner and hashrate header rows.

```tsx

WEBAPP_SHORT_NAME  // 'APP'
```

#### `WEBAPP_DISPLAY_NAME`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Display name used in chart legends, including the hash-rate line chart series label.

```tsx

WEBAPP_DISPLAY_NAME  // 'Application'
```

### Dialog flows

#### `POSITION_CHANGE_DIALOG_FLOWS`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Flow keys consumed by [`PositionChangeDialog`](/v0-4-0/ui/react/foundation/operations/details-view/fleet-management#positionchangedialog) and its sibling dialogs to route between step-specific surfaces.

```tsx

POSITION_CHANGE_DIALOG_FLOWS.MAINTENANCE       // 'maintenance'
POSITION_CHANGE_DIALOG_FLOWS.REPLACE_MINER     // 'replaceMiner'
```

| Constant | Value | Description |
|----------|-------|-------------|
| `CONFIRM_REMOVE` | `remove` | Render the remove-miner confirmation surface |
| `CHANGE_INFO` | `changeInfo` | Edit info for an existing miner |
| `MAINTENANCE` | `maintenance` | Move a miner to (or back from) the maintenance container |
| `REPLACE_MINER` | `replaceMiner` | Replace the miner currently occupying a socket |
| `CONFIRM_CHANGE_POSITION` | `confirmChange` | Confirm a position change between sockets |
| `CONTAINER_SELECTION` | `containerSelection` | Pick a destination container and socket |

#### `PositionChangeDialogFlowKey` type
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

```tsx

type PositionChangeDialogFlowKey = keyof typeof POSITION_CHANGE_DIALOG_FLOWS
```

#### `PositionChangeDialogFlowValue` type
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

```tsx

type PositionChangeDialogFlowValue =
  (typeof POSITION_CHANGE_DIALOG_FLOWS)[PositionChangeDialogFlowKey]
```

### Header controls

#### `HEADER_ITEMS`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Array of header metric options for the [`HeaderControlsSettings`](/v0-4-0/ui/react/foundation/settings/header-controls) component.

```tsx
```

| Key | Label |
|-----|-------|
| `poolMiners` | `Pool Miners` |
| `miners` | `` `${WEBAPP_SHORT_NAME} Miners` `` |
| `poolHashrate` | `Pool Hashrate` |
| `hashrate` | `` `${WEBAPP_SHORT_NAME} Hashrate` `` |
| `consumption` | `Consumption` |
| `efficiency` | `Efficiency` |

The two in-app rows compose their labels from [`WEBAPP_SHORT_NAME`](#webapp_short_name); with the shipped default that renders as `APP Miners` and `APP Hashrate`.

#### `DEFAULT_HEADER_PREFERENCES`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Default visibility state for all header items (all `true`).

```tsx

DEFAULT_HEADER_PREFERENCES.poolMiners    // true
DEFAULT_HEADER_PREFERENCES.consumption   // true
```

#### `HeaderPreferences` type
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

```tsx

type HeaderPreferences = {
  poolMiners: boolean
  miners: boolean
  poolHashrate: boolean
  hashrate: boolean
  consumption: boolean
  efficiency: boolean
}
```

### Permissions

#### `AUTH_PERMISSIONS`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Permission resource identifiers.

```tsx

AUTH_PERMISSIONS.USERS      // 'users'
AUTH_PERMISSIONS.SETTINGS   // 'settings'
AUTH_PERMISSIONS.MINER      // 'miner'
```

| Constant | Value | Description |
|----------|-------|-------------|
| `USERS` | `users` | User management |
| `SETTINGS` | `settings` | Application settings |
| `MINER` | `miner` | Miner operations |
| `ALERTS` | `alerts` | Alert management |
| `ACTIONS` | `actions` | Action execution |
| `EXPLORER` | `explorer` | Device explorer |
| `INVENTORY` | `inventory` | Inventory management |
| `CONTAINER` | `container` | Container management |
| `PRODUCTION` | `production` | Production data |
| `REPORTING` | `reporting` | Reports |

#### `AUTH_LEVELS`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Permission access levels.

```tsx

AUTH_LEVELS.READ   // 'r'
AUTH_LEVELS.WRITE  // 'w'
```

#### `USER_ROLE`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

User role identifiers.

```tsx

USER_ROLE.ADMIN           // 'admin'
USER_ROLE.SITE_MANAGER    // 'site_manager'
USER_ROLE.READ_ONLY       // 'read_only_user'
```

| Constant | Value |
|----------|-------|
| `ADMIN` | `admin` |
| `SITE_MANAGER` | `site_manager` |
| `SITE_OPERATOR` | `site_operator` |
| `FIELD_OPERATOR` | `field_operator` |
| `REPAIR_TECHNICIAN` | `repair_technician` |
| `REPORTING_TOOL_MANAGER` | `reporting_tool_manager` |
| `READ_ONLY` | `read_only_user` |

### Settings

#### `USER_ROLES`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Array of role options for select dropdowns.

```tsx

// [{ label: 'Admin', value: 'admin' }, ...]
```

#### `PERM_LEVEL_LABELS`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Human-readable labels for permission levels.

```tsx

PERM_LEVEL_LABELS.rw    // 'Read & Write'
PERM_LEVEL_LABELS.r     // 'Read Only'
PERM_LEVEL_LABELS.none  // 'No Access'
```

#### `SETTINGS_ERROR_CODES`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Error code to message mapping.

```tsx

SETTINGS_ERROR_CODES.ERR_USER_EXISTS  // 'User already exists'
SETTINGS_ERROR_CODES.DEFAULT          // 'An error occurred'
```

### Role styling

#### `getRoleBadgeColors`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Get badge colors for a role.

```tsx

const { color, bgColor } = getRoleBadgeColors('admin')
// { color: '#e8833a', bgColor: 'rgba(232, 131, 58, 0.1)' }
```

| Role | Color | Background |
|------|-------|------------|
| `admin` | `#e8833a` | Orange tint |
| `site_manager` | `#52c41a` | Green tint |
| `site_operator` | `#faad14` | Yellow tint |
| `read_only_user` | `#8c8c8c` | Gray tint |

# Hooks (/v0-4-0/reference/app-toolkit/ui-kit/hooks)

Hooks shipped by `@tetherto/mdk-react-devkit`. For the full catalog — including adapter data hooks and state hooks from `@tetherto/mdk-react-adapter` — see the [Hooks reference](/v0-4-0/reference/app-toolkit/hooks). Use the groups below to browse by concern; each hook name links to its entry in that reference.

## Available hooks

| Group | Hooks |
|---|---|
| [Monitoring](/v0-4-0/reference/app-toolkit/hooks) | [`useChartDataCheck`](/v0-4-0/reference/app-toolkit/hooks/components#usechartdatacheck), [`useBeepSound`](/v0-4-0/reference/app-toolkit/hooks/utilities#usebeepsound), [`useEnergyReportSite`](/v0-4-0/reference/app-toolkit/hooks/components#useenergyreportsite), [`useHashrate`](/v0-4-0/reference/app-toolkit/hooks/components#usehashrate) |
| [UI](/v0-4-0/reference/app-toolkit/hooks) | [`useNotification`](/v0-4-0/reference/app-toolkit/hooks/components#usenotification), [`useHeaderControls`](/v0-4-0/reference/app-toolkit/hooks/components#useheadercontrols), [`useSidebarExpandedState`](/v0-4-0/reference/app-toolkit/hooks/components#usesidebarexpandedstate), [`useSidebarSectionState`](/v0-4-0/reference/app-toolkit/hooks/components#usesidebarsectionstate), [`useHasPerms`](/v0-4-0/reference/app-toolkit/hooks/utilities#usehasperms), [`useCheckPerm`](/v0-4-0/reference/app-toolkit/hooks/utilities#usecheckperm), [`useLocalStorage`](/v0-4-0/reference/app-toolkit/hooks/utilities#uselocalstorage), [`usePagination`](/v0-4-0/reference/app-toolkit/hooks/utilities#usepagination), [`useIsFeatureEditingEnabled`](/v0-4-0/reference/app-toolkit/hooks/utilities#useisfeatureeditingenabled) |

# Types (/v0-4-0/reference/app-toolkit/ui-kit/types)

This page documents the TypeScript types exported by the MDK packages. The two packages cover different territory:

- [Core types](#core-types) ships **UI primitive types**: sizes, variants, colors, status, common API and chart shapes. These are the building blocks
consumed by core components and re-used in your own component prop types.
- [Foundation types](#foundation-types) ships **mining-domain models**: `Device`, `Container`, `Alert`, `MinerStats`, settings shapes. These describe the
data flowing through foundation components and the API responses they consume.

## Core types

UI primitive types exported by `@tetherto/mdk-react-devkit/core` — sizes, variants, colors, status, and common API and chart shapes. These are the 
building blocks consumed by core components and re-used in your own component prop types.

### Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/core#prerequisites)

### Import
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

```tsx
  ComponentSize,
  ButtonVariant,
  ColorVariant,
  Status,
  ApiResponse,
} from '@tetherto/mdk-react-devkit/core'
```

### Common types

#### `ComponentSize`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Standard size variants used across multiple components.

```tsx
type ComponentSize = 'sm' | 'md' | 'lg'
```

Used by: `Button`, `Badge`, `Checkbox`, `Switch`, `Radio`, `Spinner`, `Indicator`, `EmptyState`, `Pagination`.

#### `ButtonSize`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Extends `ComponentSize` with an icon-only variant.

```tsx
type ButtonSize = ComponentSize | 'icon'
```

#### `BorderRadius`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Border radius variants for form components.

```tsx
type BorderRadius = 'none' | 'small' | 'medium' | 'large' | 'full'
```

Used by: `Checkbox`, `Switch`, `Radio`.

#### `ColorVariant`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Comprehensive color variants for components.

```tsx
type ColorVariant =
  | 'default'
  | 'primary'
  | 'secondary'
  | 'success'
  | 'warning'
  | 'error'
  | 'info'
```

#### `StatusVariant`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Status variants for state indication.

```tsx
type StatusVariant = 'success' | 'processing' | 'error' | 'warning' | 'default' | 'idle'
```

Used by: badges, notifications, status indicators.

#### `ComponentColor`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Color options for form components.

```tsx
type ComponentColor = 'default' | 'primary' | 'success' | 'warning' | 'error'
```

Used by: `Checkbox`, `Switch`, `Radio`, `Typography`.

#### `Position`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Position/side options for UI elements.

```tsx
type Position = 'top' | 'right' | 'bottom' | 'left'
```

Used by: `Tooltip`, `Popover`, chart legends.

#### `TextAlign`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Text alignment options.

```tsx
type TextAlign = 'left' | 'center' | 'right' | 'justify'
```

#### `FlexAlign`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Flex/grid alignment options.

```tsx
type FlexAlign = 'start' | 'center' | 'end'
```

### Component types

#### `ButtonVariant`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Button visual variants.

```tsx
type ButtonVariant =
  | 'primary'
  | 'secondary'
  | 'danger'
  | 'tertiary'
  | 'link'
  | 'icon'
  | 'outline'
  | 'ghost'
```

#### `ButtonIconPosition`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Where icons appear in buttons.

```tsx
type ButtonIconPosition = 'left' | 'right'
```

#### `NotificationVariant`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Toast/notification variants.

```tsx
type NotificationVariant = 'success' | 'error' | 'warning' | 'info'
```

#### `BadgeStatus`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Badge status options.

```tsx
type BadgeStatus = 'success' | 'processing' | 'error' | 'warning' | 'default'
```

#### `TypographyColor`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Typography color options including muted.

```tsx
type TypographyColor = 'default' | 'primary' | 'success' | 'warning' | 'error' | 'muted'
```

### Utility types

#### `UnknownRecord`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Generic type for objects with unknown structure.

```tsx
type UnknownRecord = Record<string, unknown>
```

#### `Nullable` / `Optional` / `Maybe`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Null/undefined wrapper types.

```tsx
type Nullable<T> = T | null
type Optional<T> = T | undefined
type Maybe<T> = T | null | undefined
```

#### `Status`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Async operation status.

```tsx
type Status = 'idle' | 'loading' | 'success' | 'error'
```

### API types

#### `PaginationParams`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Pagination request parameters.

```tsx
type PaginationParams = {
  limit?: number
  offset?: number
  page?: number
}
```

#### `PaginatedResponse`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Paginated response wrapper.

```tsx
type PaginatedResponse<T> = {
  data: T[]
  page: number
  total: number
  totalPages: number
}
```

#### `ApiResponse`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

API response wrapper.

```tsx
type ApiResponse<T> = {
  data: T
  message?: string
  status: number
}
```

#### `ApiError`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

API error response structure.

```tsx
type ApiError = {
  error: string
  message: string
  status: number
  data?: {
    message?: string
  }
}
```

### Data table types
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Re-exported from TanStack Table for convenience.

```tsx
  DataTableColumnDef,
  DataTableExpandedState,
  DataTablePaginationState,
  DataTableRow,
  DataTableRowSelectionState,
  DataTableSortingState,
} from '@tetherto/mdk-react-devkit/core'
```

### Value types

#### `ValueUnit`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

A value paired with a unit, used for display formatting.

```tsx
type ValueUnit = {
  value: number | string | null
  unit: string
  realValue: number
}
```

#### `HashrateUnit` / `CurrencyUnit`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Specialized value-unit aliases.

```tsx
type HashrateUnit = ValueUnit
type CurrencyUnit = ValueUnit
```

#### `UnitLabel`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

SI-prefix unit labels.

```tsx
type UnitLabel = 'decimal' | 'k' | 'M' | 'G' | 'T' | 'P'
```

### Time types

#### `TimeRangeFormatted`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

A formatted time range.

```tsx
type TimeRangeFormatted = {
  start: string
  end: string
  formatted: string
}
```

#### `TimeInterval`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

A time interval with start/end timestamps.

```tsx
type TimeInterval = {
  start: number
  end: number
}
```

### Chart types

#### `ChartLegendPosition`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Chart legend position options.

```tsx
type ChartLegendPosition = 'top' | 'bottom' | 'left' | 'right' | 'center' | 'chartArea'
```

#### `WeightedAverageResult`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Result from weighted average calculation.

```tsx
type WeightedAverageResult = {
  avg: number
  totalWeight: number
  weightedValue: number
}
```

#### `ErrorWithTimestamp`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

An error with optional timestamp.

```tsx
type ErrorWithTimestamp = {
  msg?: string
  message?: string
  timestamp?: number | string
}
```

## Foundation types

Foundation types describe the shape of devices, containers, alerts, site configuration, and settings data flowing through `@tetherto/mdk-react-devkit/foundation` components and API responses. They are organized into barrels including `alerts`, `config`, `device`, and `settings.types`.

### Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)

### Import
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

```tsx
  Alert,
  Device,
  DeviceLast,
  DeviceInfo,
  ContainerSnap,
  ContainerStats,
  MinerStats,
  MinerConfig,
  SettingsUser,
  PermLevel,
  GlobalConfig,
  TimelineChartDataPoint,
  TimelineChartDataset,
  TimelineChartData,
  ChartRange,
  AxisTitleText,
  MetricsEfficiencyLogEntry,
} from '@tetherto/mdk-react-devkit/foundation'
```

### Alert types

#### `Alert`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Raw alert record as it appears on a device's `alerts` array.

```tsx
type Alert = {
  id?: string
  severity: string
  createdAt: number | string
  name: string
  description: string
  message?: string
  uuid?: string
  code?: string | number
  [key: string]: unknown
}
```

The `severity` field uses string values like `critical`, `high`, `medium`, `low`. The open `[key: string]: unknown` index signature allows 
vendor-specific fields without breaking the type contract.

#### `LogFormattedAlertData`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Alert reshaped for log display components (e.g., `AlertsLog`).

```tsx
type LogFormattedAlertData = {
  title: string
  subtitle: string
  status: string
  severityLevel: number
  creationDate: number | string
  body: string
  id: string
  uuid?: string
  [key: string]: unknown
}
```

### Device types

The `Device` family models everything that appears on the device explorer: miners, containers, power meters, temperature sensors, and cabinets. 
The shape is intentionally permissive (open index signatures, optional fields) because devices come from a live API that adds vendor-specific fields over time.

#### `Device`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

The root device record. Used pervasively in the [operations centre](/v0-4-0/ui/react/foundation/operations) components.

```tsx
type Device = {
  id: string
  type: string
  tags?: string[]
  rack?: string
  last?: DeviceLast
  username?: string
  info?: DeviceInfo
  containerId?: string
  address?: string | null
  code?: string
  alerts?: Alert[] | null
  powerMeters?: Device[]
  tempSensors?: Device[]
  transformerTempSensor?: Device
  rootTempSensor?: Device
  [key: string]: unknown
}
```

The `type` string discriminates devices by category (such as miner or container) and by vendor. The `last` field carries the latest snapshot from the device.

#### `DeviceLast`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

The latest reading wrapper that lives on `Device.last`.

```tsx
type DeviceLast = {
  err?: string | null
  type?: string
  snap?: ContainerSnap
  alerts?: Alert[] | null
  [key: string]: unknown
}
```

`err` is a connection or upstream error string when the device is unreachable. `snap` carries the actual stats and config payload.

#### `DeviceInfo`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Identification and placement metadata that lives on `Device.info`.

```tsx
type DeviceInfo = {
  container?: string
  pos?: string
  poolConfig?: string
  serialNum?: string
  macAddress?: string | null
  posHistory?: Partial<PosHistoryEntry[]>
  [key: string]: unknown
}
```

#### `PosHistoryEntry`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

A single past placement of a miner.

```tsx
type PosHistoryEntry = {
  container: string
  pos: string
  removedAt: number
}
```

#### `DeviceData`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

A flattened version of `Device` with a guaranteed (non-optional) `snap` field, returned by the `getDeviceData` helper.

```tsx
type DeviceData = {
  id: string
  type: string
  tags?: string[]
  rack?: string
  snap: ContainerSnap
  alerts?: Alert[]
  username?: string
  info?: DeviceInfo
  containerId?: string
  address?: string
  err?: string
  [key: string]: unknown
}
```

### Container types

#### `Container`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

A `Device` specialized for containers, with container-specific `info` and `last` shapes.

```tsx
type Container = {
  info?: Partial<ContainerInfo>
  last?: Partial<ContainerLast>
} & Device
```

#### `ContainerInfo`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Cooling, supply, and pressure metadata for a container.

```tsx
type ContainerInfo = {
  container: string
  cooling_system: Record<string, unknown>
  cdu: Record<string, unknown>
  primary_supply_temp: number
  second_supply_temp1: number
  second_supply_temp2: number
  supply_liquid_temp: number
  supply_liquid_set_temp: number
  supply_liquid_pressure: number
  return_liquid_pressure: number
}
```

#### `ContainerPosInfo`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Position descriptor for a device inside a container (PDU/socket coordinates).

```tsx
type ContainerPosInfo = {
  containerInfo: Partial<{ container: string; type: string }>
  pdu: string | number
  socket: string | number
  pos: string
  [key: string]: unknown
}
```

#### `ContainerLast`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Last-snapshot wrapper for a container.

```tsx
type ContainerLast = {
  snap: {
    stats?: Partial<ContainerStats>
  }
  alerts: unknown[] | null
  err: string | null
}
```

#### `ContainerSnap`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

The `snap` payload found on `DeviceLast.snap` for both miners and containers. `stats` is what most components read.

```tsx
type ContainerSnap = {
  stats?: Partial<ContainerStats>
  config?: Record<string, unknown>
}
```

#### `ContainerStats`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

The big stats blob produced by every container snapshot.

```tsx
type ContainerStats = {
  status: string
  ambient_temp_c: number
  humidity_percent: number
  power_w: number
  container_specific: Partial<ContainerSpecific>
  distribution_box1_power_w: number
  distribution_box2_power_w: number
  stats: Record<string, unknown>
  temperature_c: Partial<StatsTemperatureC>
  frequency_mhz: Partial<StatsFrequencyMhz>
  miner_specific: Partial<MinerSpecificStats>
  [key: string]: unknown
}
```

`status` is one of `running`, `offline`, `stopped` (see `CONTAINER_STATUS` in [Constants](/v0-4-0/reference/app-toolkit/ui-kit/constants#foundation-constants)).

#### `ContainerSpecific`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Container-specific stats (currently the PDU array).

```tsx
type ContainerSpecific = {
  pdu_data: Partial<ContainerPduData>[]
  [key: string]: unknown
}
```

#### `ContainerPduData`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Per-PDU power and status reading.

```tsx
type ContainerPduData = {
  power_w: number
  status: number
}
```

#### `StatsTemperatureC`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Temperature stats with per-chip detail.

```tsx
type StatsTemperatureC = {
  avg: number
  min: number
  max: number
  chips: TempChipData[]
  [key: string]: unknown
}
```

#### `StatsFrequencyMhz`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Frequency stats with per-chip detail.

```tsx
type StatsFrequencyMhz = {
  avg: number
  chips: ChipData[]
  [key: string]: unknown
}
```

#### `ChipData` / `TempChipData`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Per-chip readings.

```tsx
type ChipData = {
  index: number
  current: number
}

type TempChipData = {
  index: number
  max?: number
  min?: number
  avg?: number
}
```

#### `MinerSpecificStats`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Miner-specific stats blob.

```tsx
type MinerSpecificStats = {
  upfreq_speed: number
  [key: string]: unknown
}
```

### Miner types

#### `MinerStats`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Per-miner stats reported on each snapshot.

```tsx
type MinerStats = {
  status?: string
  uptime_ms?: number
  power_w?: number
  hashrate_mhs?: MinerHashrateMhs
  poolHashrate?: string
  temperature_c?: { max?: number }
}
```

#### `MinerHashrateMhs`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Hashrate readings, currently just the rolling 5-minute window.

```tsx
type MinerHashrateMhs = {
  t_5m?: number
}
```

#### `MinerInfo`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Identifying info for a miner (used by miner record cards).

```tsx
type MinerInfo = {
  container?: string
  pos?: string
  macAddress?: string
  serialNum?: string
}
```

#### `MinerConfig`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Mutable miner configuration.

```tsx
type MinerConfig = {
  firmware_ver?: string
  power_mode?: string
  led_status?: boolean
}
```

`power_mode` values come from `MINER_POWER_MODE` (`sleep`, `low`, `normal`, `high`).

#### `MinerDeviceSnapshot`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Lightweight snapshot wrapper holding only `MinerConfig`.

```tsx
type MinerDeviceSnapshot = {
  last?: { snap?: { config?: MinerConfig } }
}
```

#### `MinerRecord`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Combined miner record used by list/table views.

```tsx
type MinerRecord = {
  id?: string
  shortCode?: string
  info?: MinerInfo
  address?: string
  type?: string
  alerts?: unknown[]
  stats?: MinerStats
  config?: MinerConfig
  device?: MinerDeviceSnapshot
  error?: string
  err?: string
  isPoolStatsEnabled?: boolean
}
```

### Power and cabinet types

#### `PowerMeter`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Minimal power meter reading shape.

```tsx
type PowerMeter = {
  last?: {
    snap?: {
      stats?: {
        power_w?: number
      }
    }
  }
}
```

#### `LvCabinetRecord`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

LV cabinet record carrying its associated power meters.

```tsx
type LvCabinetRecord = {
  id: string
  powerMeters?: PowerMeter[]
}
```

### Config types

#### `GlobalConfig`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Site-wide configuration from your API or store, including nominal targets for reporting dashboards. Load this shape in your app from your API or 
store and pass it into foundation reporting UI as needed.

```tsx
type GlobalConfig = {
  nominalSiteHashrate_MHS?: number
  nominalAvailablePowerMWh?: number
  nominalPowerConsumption_MW?: number
  nominalWeightedAvgEfficiency_WThs?: number
  nominalMinerCapacity?: number
  isAutoSleepAllowed?: boolean
  siteEnergyDataThresholdMWh?: number
  [key: string]: unknown
}
```

| Field | Type | Description |
|-------|------|-------------|
| `nominalSiteHashrate_MHS` | `number` | Nominal site hashrate (MH/s) |
| `nominalAvailablePowerMWh` | `number` | Nominal available power (MWh on the wire) |
| `nominalPowerConsumption_MW` | `number` | Nominal power consumption (MW) |
| `nominalWeightedAvgEfficiency_WThs` | `number` | Nominal weighted average efficiency (W/TH) |
| `nominalMinerCapacity` | `number` | Nominal miner capacity |
| `isAutoSleepAllowed` | `boolean` | Whether auto-sleep is permitted for the site |
| `siteEnergyDataThresholdMWh` | `number` | Energy data threshold (MWh) |
| `[key: string]` | `unknown` | Additional API fields without breaking the type |

### Timeline chart types

Types for [`TimelineChart`](/v0-4-0/ui/react/foundation/dashboard/charts#timelinechart) in `@tetherto/mdk-react-devkit/foundation`. Each segment uses `x: [startMs, endMs]` and `y` matching a row in `labels`.

#### `TimelineChartDataPoint`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

One horizontal segment on a timeline row.

```tsx
type TimelineChartDataPoint = {
  x: [number, number]
  y: string | undefined
}
```

| Field | Type | Description |
|-------|------|-------------|
| `x` | `[number, number]` | Segment start and end (epoch ms) |
| `y` | `string \| undefined` | Row label; must match an entry in `TimelineChartData.labels` |

#### `TimelineChartDataset`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

A named group of segments (legend entry).

```tsx
type TimelineChartDataset = {
  label: string
  data: TimelineChartDataPoint[]
  borderColor?: string[]
  backgroundColor?: string[]
  color?: string
}
```

#### `TimelineChartData`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Full payload for `initialData` and `newData`.

```tsx
type TimelineChartData = {
  labels: string[]
  datasets: TimelineChartDataset[]
}
```

| Field | Type | Description |
|-------|------|-------------|
| `labels` | `string[]` | Row names (Y axis) |
| `datasets` | `TimelineChartDataset[]` | Segment groups keyed by `label` |

#### `ChartRange`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Visible time window for the `range` prop.

```tsx
type ChartRange = {
  min: Date | number
  max: Date | number
}
```

#### `AxisTitleText`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Axis title strings for `axisTitleText`.

```tsx
type AxisTitleText = {
  x: string
  y: string
}
```

### Metrics efficiency types

Types for **`/auth/metrics/efficiency`**, used by the site view tab on 
[`OperationsEfficiency`](/v0-4-0/ui/react/foundation/reporting/operations-efficiency).

#### `MetricsEfficiencyLogEntry`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

One point on the site efficiency time series.

```tsx
type MetricsEfficiencyLogEntry = {
  ts: number
  efficiencyWThs: number
}
```

| Field | Type | Description |
|-------|------|-------------|
| `ts` | `number` | Timestamp (epoch ms) |
| `efficiencyWThs` | `number` | Site efficiency (W/TH) at `ts` |

#### `MetricsEfficiencySummary`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Summary block returned with the efficiency metrics response.

```tsx
type MetricsEfficiencySummary = {
  avgEfficiencyWThs: number | null
}
```

#### `MetricsEfficiencyResponse`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Wrapper for the efficiency metrics endpoint (`log` entries plus summary), using the shared `MetricsResponse` shape from `@tetherto/mdk-react-devkit/foundation`.

### Settings types

#### `SettingsUser`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

A user record as it appears in user-management lists.

```tsx
type SettingsUser = {
  id: string
  name?: string
  email: string
  role: string
  last_login?: string
  lastActive?: string
  [key: string]: unknown
}
```

#### `RoleOption`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Role option for select dropdowns. Also exported as the array `USER_ROLES` in [Constants](/v0-4-0/reference/app-toolkit/ui-kit/constants#foundation-constants).

```tsx
type RoleOption = {
  label: string
  value: string
}
```

#### `PermLevel`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Permission level values.

```tsx
type PermLevel = 'rw' | 'r' | false
```

#### `RolesPermissionsData`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

The shape consumed by [`RBACControlSettings`](/v0-4-0/ui/react/foundation/settings/access-control).

```tsx
type RolesPermissionsData = {
  permissions: Record<string, Record<string, PermLevel>>
  labels: Record<string, string>
}
```

#### `SettingsExportData`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

The export envelope produced and consumed by [`ImportExportSettings`](/v0-4-0/ui/react/foundation/settings/import-export). 
Generic `TExtra` lets you attach app-specific extras.

```tsx
type SettingsExportData<TExtra extends Record<string, unknown> = Record<string, unknown>> = {
  headerControls?: Record<string, boolean>
  featureFlags?: Record<string, boolean>
  timestamp?: string
  version?: string
} & TExtra
```

#### `ImportResult`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Result returned from settings import operations.

```tsx
type ImportResult = {
  success: boolean
  applied?: string[]
  errors?: string[]
  message?: string
}
```

# Utilities (/v0-4-0/reference/app-toolkit/ui-kit/utilities)

This page documents helper functions exported by the MDK packages.

- [Core utilities](#core-utilities) ships **15 utility modules** with functions for formatting, dates, validation, conversions, class-name merging, and more
- [Foundation utilities](#foundation-utilities) currently ships a single public utility module (`settings-utils`) for parsing, validating, and exporting settings JSON
- [UI core alert utilities](#ui-core-alert-utilities) ships query-parameter builders and time-range chunking helpers for the Alerts feature

## Core utilities

Helper functions exported by `@tetherto/mdk-react-devkit/core` for formatting, dates, validation, conversions, and class-name merging.

### Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/core#prerequisites)

### Import
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

```tsx
  formatNumber,
  formatHashrate,
  formatDate,
  formatRelativeTime,
  cn,
  isEmpty,
  isValidEmail,
} from '@tetherto/mdk-react-devkit/core'
```

### Formatting utilities

#### `formatNumber`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Format numbers with locale formatting and configurable options.

```tsx

formatNumber(1234.567)                          // "1,234.57"
formatNumber(null)                              // "-"
formatNumber(1234, { minimumFractionDigits: 2 }) // "1,234.00"
formatNumber(undefined, {}, 'N/A')              // "N/A"
```

#### `formatHashrate`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Format hashrate values with rounding.

```tsx

formatHashrate(150.456)  // "150.46"
formatHashrate(null)     // "-"
```

#### `formatCurrency`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Format currency values.

```tsx

formatCurrency(1234.56, 'USD')      // "$1,234.56"
formatCurrency(0.00012345, 'BTC')   // "₿0.00012345"
```

#### `getPercentFormattedNumber`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Format numbers as percentages.

```tsx

getPercentFormattedNumber(0.75)      // "75%"
getPercentFormattedNumber(0.1234, 1) // "12.3%"
```

#### `formatValueUnit`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Format value-unit objects.

```tsx

formatValueUnit(150, 'TH/s')  // "150 TH/s"
```

### Date utilities

#### `formatDate`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Format dates with customizable patterns.

```tsx

formatDate(new Date())                              // "Jan 15, 2025"
formatDate(1705334400000, { format: 'yyyy-MM-dd' }) // "2025-01-15"
```

#### `formatRelativeTime`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Format dates as relative time strings.

```tsx

formatRelativeTime(new Date(Date.now() - 3600000))  // "1h ago"
formatRelativeTime(new Date(Date.now() - 86400000)) // "1d ago"
```

#### `formatChartDate`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Format timestamps for chart display.

```tsx

formatChartDate(1705334400)        // "Jan 15"
formatChartDate(1705334400, true)  // "Jan 15, 2025"
```

#### `isValidTimestamp`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Check if a timestamp is valid.

```tsx

isValidTimestamp(1705334400000)  // true
isValidTimestamp('invalid')      // false
```

#### `parseMonthLabelToDate`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Parse month labels to `Date` objects.

```tsx

parseMonthLabelToDate('01-26')    // Date(2026, 0, 1)
parseMonthLabelToDate('03-2025')  // Date(2025, 2, 1)
```

#### `getPastDateFromDate`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Get a date in the past.

```tsx

getPastDateFromDate({ dateTs: Date.now(), days: 7 })  // 7 days ago
```

### Validation utilities

#### `isEmpty`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Check if a value is empty.

```tsx

isEmpty(null)       // true
isEmpty('')         // true
isEmpty([])         // true
isEmpty({})         // true
isEmpty('hello')    // false
isEmpty([1, 2, 3])  // false
```

#### `isValidEmail`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Validate email addresses.

```tsx

isValidEmail('user@example.com')  // true
isValidEmail('invalid')           // false
```

#### `isValidUrl`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Validate URLs.

```tsx

isValidUrl('https://example.com')  // true
isValidUrl('not-a-url')            // false
```

#### `isNil`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Check if value is `null` or `undefined`.

```tsx

isNil(null)       // true
isNil(undefined)  // true
isNil(0)          // false
isNil('')         // false
```

#### `isPlainObject`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Check if value is a plain object.

```tsx

isPlainObject({})           // true
isPlainObject({ a: 1 })     // true
isPlainObject([])           // false
isPlainObject(new Date())   // false
```

### Class name utilities

#### `cn`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Merge class names using [clsx](https://github.com/lukeed/clsx) and [tailwind-merge](https://github.com/dcastil/tailwind-merge).

```tsx

cn('px-4', 'py-2')                    // "px-4 py-2"
cn('text-red', isError && 'bg-red')   // conditional classes
cn('p-4', { 'hidden': !visible })     // object syntax
```

### Conversion utilities

#### `toMW` / `toMWh`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Convert watts to megawatts.

```tsx

toMW(1000000)   // 1
toMWh(1000000)  // 1
```

#### `toPHS`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Convert raw hashrate to `PH/s`.

```tsx

toPHS(1000000000000000)  // 1
```

#### `convertMpaToBar`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Convert pressure units.

```tsx

convertMpaToBar(0.1)  // 1
```

#### `unitToKilo`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Convert to kilo units.

```tsx

unitToKilo(1000)  // 1
```

### Number utilities

#### `percentage`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Calculate percentage.

```tsx

percentage(25, 100)  // 25
percentage(1, 4)     // 25
```

#### `getPercentChange`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Calculate percentage change.

```tsx

getPercentChange(110, 100)  // 10
getPercentChange(90, 100)   // -10
```

#### `convertUnits`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Convert between SI-prefix units.

```tsx

convertUnits(1, 'k', 'M')           // 0.001
convertUnits(1000, 'decimal', 'k')  // 1
```

#### `safeNumber`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Safely convert to number.

```tsx

safeNumber('123')      // 123
safeNumber('invalid')  // 0
safeNumber(null)       // 0
```

### String utilities

#### `toTitleCase`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Convert string to Title Case.

```tsx

toTitleCase('hello world')  // "Hello World"
```

#### `formatMacAddress`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Format MAC addresses.

```tsx

formatMacAddress('aa:bb:cc:dd:ee:ff')  // "AA:BB:CC:DD:EE:FF"
```

#### `safeString`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Safely convert to string.

```tsx

safeString(123)    // "123"
safeString(null)   // ""
```

### Time utilities

#### `secondsToMs`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Convert seconds to milliseconds.

```tsx

secondsToMs(60)  // 60000
```

#### `breakTimeIntoIntervals`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Split `[start, end]` into consecutive windows of `intervalMs`. Re-exported from `@tetherto/mdk-ui-core`.

```tsx

breakTimeIntoIntervals(start, end, 3_600_000)  // Array of 1-hour { start, end } windows
```

#### `timeRangeWalker`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Generator for iterating through time ranges.

```tsx

for (const interval of timeRangeWalker(start, end, duration)) {
  // Process each interval
}
```

### Color utilities

#### `hexToRgba`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Convert hex color to rgba.

```tsx

hexToRgba('#72F59E', 0.5)  // "rgba(114, 245, 158, 0.5)"
```

### Array utilities

#### `getNestedValue`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Get nested value by dot-path.

```tsx

getNestedValue({ a: { b: 1 } }, 'a.b')  // 1
```

#### `getWeightedAverage`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Calculate weighted average.

```tsx

getWeightedAverage(items, 'value', 'weight')
```

#### `circularArrayAccess`
<PackageBadge>@tetherto/mdk-react-devkit/core</PackageBadge>

Create infinite cycling generator.

```tsx

const colors = circularArrayAccess(['red', 'green', 'blue'])
colors.next().value  // 'red'
colors.next().value  // 'green'
colors.next().value  // 'blue'
colors.next().value  // 'red' (cycles)
```

## Foundation utilities

Helpers exported by `@tetherto/mdk-react-devkit/foundation` for filtering, formatting, validating, parsing, and exporting settings data.

### Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)

### Import
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

```tsx
  filterUsers,
  formatRoleLabel,
  formatLastActive,
  validateSettingsJson,
  parseSettingsFile,
  exportSettingsToFile,
} from '@tetherto/mdk-react-devkit/foundation'
```

### Settings utilities

#### `filterUsers`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Filter a `SettingsUser[]` list by email substring (case-insensitive) and exact role match. Used by the user-management table search.

```tsx

filterUsers({
  users,
  email: 'alice',   // partial, case-insensitive match on user.email
  role: 'admin',    // exact match on user.role; pass null to skip
})
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `users` | `SettingsUser[]` | Source list |
| `email` | `string \| null \| undefined` | Substring filter on `email` (case-insensitive). Skipped when falsy. |
| `role` | `string \| null \| undefined` | Exact role match. Skipped when falsy. |

#### `formatRoleLabel`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Convert a snake case role identifier to a human-readable Title Case label.

```tsx

formatRoleLabel('site_manager')           // "Site Manager"
formatRoleLabel('reporting_tool_manager') // "Reporting Tool Manager"
formatRoleLabel('admin')                  // "Admin"
```

#### `formatLastActive`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Format a timestamp string as `MM/DD/YYYY - HH:MM`. Returns `'-'` when the input is missing or invalid.

```tsx

formatLastActive('2025-01-15T14:30:00Z')  // "01/15/2025 - 14:30"
formatLastActive(undefined)               // "-"
formatLastActive('not-a-date')            // "-"
```

#### `validateSettingsJson`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Type-guard that checks whether an unknown value is a valid `SettingsExportData`. Returns `true` if the value is an object that contains at least one of `headerControls`, `featureFlags`, or `timestamp`.

```tsx

if (validateSettingsJson(parsed)) {
  // parsed is now narrowed to SettingsExportData
}
```

#### `parseSettingsFile`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Read a `File` containing settings JSON, validate it, and resolve to `SettingsExportData`. Rejects on invalid JSON, an invalid format, or a file read error.

```tsx

try {
  const settings = await parseSettingsFile(file)
  applySettings(settings)
} catch (err) {
  notifyError('Could not import settings', err.message)
}
```

| Throws | Reason |
|--------|--------|
| `` `Invalid settings file format. Please ensure the file is a valid ${WEBAPP_NAME} settings export.` `` | The JSON parsed but didn't match the `SettingsExportData` shape. The literal interpolates [`WEBAPP_NAME`](/v0-4-0/reference/app-toolkit/ui-kit/constants#webapp_name). |
| `Failed to parse JSON file. Please ensure the file is valid JSON.` | The file contents weren't valid JSON. |
| `Failed to read file.` | The browser couldn't read the file. |

#### `exportSettingsToFile`
<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Serialize `SettingsExportData` to JSON, package it as a downloadable Blob, and trigger a browser download. Returns the generated filename (e.g., `mdk-settings-2025-01-15T14-30-00-000Z.json`). The filename prefix is fixed in the foundation kit and does not interpolate [`WEBAPP_NAME`](/v0-4-0/reference/app-toolkit/ui-kit/constants#webapp_name).

```tsx

const filename = exportSettingsToFile({
  headerControls: { poolMiners: true, consumption: false },
  featureFlags: { betaCharts: true },
  timestamp: new Date().toISOString(),
  version: '1.0.0',
})
```

## UI core alert utilities

`@tetherto/mdk-ui-core` ships two utility modules for the Alerts feature: query-parameter builders (`alert-queries`) and time-range chunking helpers (`historical-log-chunks`). These are the source consumed directly by the `useCurrentAlertDevices` and `useHistoricalAlerts` adapter hooks.

### Prerequisites

- Complete the [@tetherto/mdk-ui-core installation](/v0-4-0/reference/app-toolkit/ui-core)

### Import

<PackageBadge>@tetherto/mdk-ui-core</PackageBadge>

```tsx
  ONE_DAY_MS,
  DEFAULT_HISTORICAL_WINDOW_MS,
  getDefaultHistoricalAlertsRange,
  buildCurrentAlertDevicesParams,
  buildHistoricalAlertsParams,
  breakTimeIntoIntervals,
  mergeAlertsByUuid,
  fetchHistoricalAlertsInChunks,
} from '@tetherto/mdk-ui-core'
```

### Alert query builders

#### `ONE_DAY_MS`
<PackageBadge>@tetherto/mdk-ui-core</PackageBadge>

One day in milliseconds (`86_400_000`). The fetch-window size used by the historical-alerts data path.

```tsx

ONE_DAY_MS  // 86400000
```

#### `DEFAULT_HISTORICAL_WINDOW_MS`
<PackageBadge>@tetherto/mdk-ui-core</PackageBadge>

Default historical-alerts look-back window — 14 days (`14 * ONE_DAY_MS`). Matches the devkit [`<Alerts>`](/v0-4-0/ui/react/foundation/alerts) feature default; wider ranges fan out into more 24-hour requests.

```tsx

DEFAULT_HISTORICAL_WINDOW_MS  // 1209600000 (14 days in ms)
```

#### `getDefaultHistoricalAlertsRange`
<PackageBadge>@tetherto/mdk-ui-core</PackageBadge>

Returns the default historical-alerts range: the last `DEFAULT_HISTORICAL_WINDOW_MS` ending now. Used by the devkit [`<Alerts>`](/v0-4-0/ui/react/foundation/alerts) feature and the shell Alerts page to seed their range state. Pass `now` to fix the upper bound in tests.

```tsx

const range = getDefaultHistoricalAlertsRange()
// { start: <14 days ago>, end: <now> }

// Injectable for tests
getDefaultHistoricalAlertsRange(1_700_000_000_000)
// { start: 1_700_000_000_000 - DEFAULT_HISTORICAL_WINDOW_MS, end: 1_700_000_000_000 }
```

| Parameter | Status | Type | Default | Description |
|-----------|--------|------|---------|-------------|
| `now` | Optional | `number` | `Date.now()` | Upper bound of the window (ms epoch). |

#### `buildCurrentAlertDevicesParams`
<PackageBadge>@tetherto/mdk-ui-core</PackageBadge>

Builds the `list-things` query params for the current-alerts table: every device that currently carries one or more alerts, with a 1 000 device limit. Consumed by `useCurrentAlertDevices`.

`filterTags` widen the server-side selector so the fetch narrows with the active search chips (`ip-`, `sn-`, `mac-`, `firmware-`) instead of relying on client-side filtering alone.

```tsx

// All devices with active alerts
const params = buildCurrentAlertDevicesParams()

// Narrowed to devices matching the active search chips
const filtered = buildCurrentAlertDevicesParams(['ip-192.168.1.1', 'sn-ABC123'])
```

| Parameter | Status | Type | Default | Description |
|-----------|--------|------|---------|-------------|
| `filterTags` | Optional | `string[]` | `[]` | Active alert search chips to narrow the selector server-side. |

#### `buildHistoricalAlertsParams`
<PackageBadge>@tetherto/mdk-ui-core</PackageBadge>

Builds the `history-log` query params for a single alerts window (`logType: 'alerts'`). Called once per 24-hour sub-window by the chunked fetch.

```tsx

const params = buildHistoricalAlertsParams({ start: range.start, end: range.end })
// { logType: 'alerts', start: ..., end: ... }
```

| Parameter | Status | Type | Default | Description |
|-----------|--------|------|---------|-------------|
| `range` | Required | `HistoricalAlertsRange` | none | `{ start: number; end: number }` — ms epoch bounds for the window. |

### Historical alert chunking

#### `breakTimeIntoIntervals`
<PackageBadge>@tetherto/mdk-ui-core</PackageBadge>

Splits `[start, end]` into consecutive windows of `intervalMs` (default `ONE_DAY_MS`); the final window is clamped to `end`. Returns an empty array for an empty or inverted range.

```tsx

// 24-hour windows (default)
const windows = breakTimeIntoIntervals(start, end)

// Custom window size
const hourly = breakTimeIntoIntervals(start, end, 3_600_000)
```

| Parameter | Status | Type | Default | Description |
|-----------|--------|------|---------|-------------|
| `start` | Required | `number` | none | Range start (ms epoch). |
| `end` | Required | `number` | none | Range end (ms epoch). |
| `intervalMs` | Optional | `number` | `ONE_DAY_MS` | Window size in ms. |

Returns `TimeInterval[]` where each element is `{ start: number; end: number }`.

#### `mergeAlertsByUuid`
<PackageBadge>@tetherto/mdk-ui-core</PackageBadge>

Concatenates `next` onto `prev`, replacing any row that shares a `uuid` (later windows win) and appending the rest. Rows without a `uuid` are always appended.

```tsx

const merged = mergeAlertsByUuid(previousAlerts, newAlerts)
```

| Parameter | Status | Type | Default | Description |
|-----------|--------|------|---------|-------------|
| `prev` | Required | `T[]` | none | Existing alert rows. |
| `next` | Required | `T[]` | none | Rows from the latest window fetch. |

Returns `T[]` where `T extends { uuid?: string }`.

#### `fetchHistoricalAlertsInChunks`
<PackageBadge>@tetherto/mdk-ui-core</PackageBadge>

Fetches a historical-alerts range as successive 24-hour windows (oldest to newest), merging results by `uuid`. Individual window failures are swallowed so one bad request does not drop the whole range. The loop bails out early when `options.signal` is aborted.

```tsx
  fetchHistoricalAlertsInChunks,
  buildHistoricalAlertsParams,
} from '@tetherto/mdk-ui-core'

const alerts = await fetchHistoricalAlertsInChunks(
  { start: range.start, end: range.end },
  async (window) => {
    const result = await myApi.historyLog(buildHistoricalAlertsParams(window))
    return result.data
  },
  { signal: abortController.signal },
)
```

| Parameter | Status | Type | Default | Description |
|-----------|--------|------|---------|-------------|
| `range` | Required | `{ start: number; end: number }` | none | Full date range to paginate over (ms epoch). |
| `fetchWindow` | Required | `function` | none | Called once per 24-hour window, oldest first. Receives a `TimeInterval` and returns `Promise<T[]>`. |
| `options.intervalMs` | Optional | `number` | `ONE_DAY_MS` | Window size in ms. |
| `options.signal` | Optional | `AbortSignal` | none | Checked between windows — aborts the loop early on range changes. |

# ORK reference (/v0-4-0/reference/ork)

`@tetherto/mdk-ork` is the orchestration kernel of the MDK stack. This subsection holds the canonical specs for its internal
modules. For the architectural narrative explaining how these modules fit together, see
[ORK](/v0-4-0/concepts/stack/ork).

## What's documented

- **[Modules](/v0-4-0/reference/ork/modules)**: per-module responsibility, interfaces, state machines, transition rules,
  crash-recovery procedures, and scaling characteristics.

# ORK modules (/v0-4-0/reference/ork/modules)

`@tetherto/mdk-ork`'s coordination splits across single-purpose modules. Each owns its own state machine, persistence boundary,
and scaling characteristics. Six modules ship in v0.0.1; two more (Fault Supervisor, Concurrency Manager) are deferred
to a later release.

For the architectural overview that explains how these modules connect, see
[ORK](/v0-4-0/concepts/stack/ork). This page is the per-module spec.

## How to read this page

Each accordion below covers one module:

- **Responsibility**: what the module owns and the example commands or events it handles.
- **Interfaces**: input and output channels, plus the public function names callers depend on.
- **State machine**: the canonical diagram for the module's internal lifecycle.
- **Crash recovery**: behavior on a fresh start, including how state is reconstructed.
- **Scalability**: where the module can be extracted, sharded, or replicated.

## Modules

<Accordions>

<Accordion title="Command Dispatcher" id="command-dispatcher">

**Responsibility**: validates incoming commands against the generic MDK schema, checks permissions, and resolves the correct
worker based on the `deviceId`.

*Example*: when an App Node sends a `reboot` command for `wm001`, the Dispatcher verifies that `wm001` exists, that `reboot` is a
valid capability for it, then passes the request to the State Machine.

**Interfaces**:

- *Input*: receives generic commands via Holepunch RPC (HRPC) and the MDK Protocol from the App Node or MCP Handler.
- *Output*: hands off validated commands to the Command State Machine.
- *Functions*: `dispatchCommand(deviceId, action, payload)`

**State machine**:

```mermaid
stateDiagram-v2
    [*] --> Validating
    Validating --> RoutingAction : Valid
    Validating --> Rejected : Invalid
    RoutingAction --> Enqueued
    Enqueued --> [*]
```

**Crash recovery**: none needed. In-flight requests fail and must be retried by the client.

**Scalability**: extracted easily; can run independently to offload validation.

</Accordion>

<Accordion title="Command State Machine" id="command-state-machine">

**Responsibility**: tracks the execution lifecycle of every single command in the system. Receives execution results directly via
the synchronous HRPC response. If the connection drops or a response is delayed, it relies on the Scheduler to fetch the latest
status via `state.pull` so commands cannot hang.

*Example*: once the `reboot` command is dispatched it transitions to `EXECUTING`. If the HRPC response returns OK it transitions
to `SUCCESS`. If the response hangs, the next `state.pull` fetches the true status from the worker.

**Interfaces**:

- *Input*: receives validated commands from the Dispatcher; status updates from HRPC responses or Scheduler ticks.
- *Output*: invokes the worker HRPC execution layer; emits terminal state results to the caller.
- *Functions*: `enqueue(command)`, `syncState(commandId)`, `cancel(commandId)`

**State machine**:

```mermaid
stateDiagram-v2
    [*] --> QUEUED
    QUEUED --> DISPATCHED
    DISPATCHED --> EXECUTING : HRPC sent
    EXECUTING --> SUCCESS : state.pull (response)
    EXECUTING --> FAILED : state.pull (error)
    EXECUTING --> TIMEOUT
    TIMEOUT --> QUEUED : Retry allowed
    TIMEOUT --> FAILED : Max retries
    SUCCESS --> [*]
    FAILED --> [*]
```

**Crash recovery**: on startup, performs a recovery sweep of pending commands. `DISPATCHED` and `EXECUTING` commands are forced
to `TIMEOUT` (and re-queued if retries are available); `QUEUED` commands are left untouched.

**Scalability**: requires state sharding (for example, by device or rack).

</Accordion>

<Accordion title="Worker Registry" id="worker-registry">

**Responsibility**: the phonebook for the entire `@tetherto/mdk-ork` ecosystem. Maps which physical device IDs belong to which connected
worker channels and stores their declared capabilities.

*Example*: a `whatsminer-worker` connects and declares it manages `wm001` and `wm002`. The Registry saves this topology so the
Dispatcher knows exactly where to route a command for `wm001`.

**Interfaces**:

- *Input*: `identity.register` requests from workers.
- *Output*: internal events triggering full state lifecycle binding.
- *Functions*: `resolveWorkerState(target)`

**State machine**:

```mermaid
stateDiagram-v2
    [*] --> Unregistered
    Unregistered --> Discovered : DHT peer detected
    Discovered --> IdentitySaved : identity pulled
    IdentitySaved --> Ready : capabilities pulled
    Ready --> Terminated : eviction
    Terminated --> [*]
```

**Crash recovery**: rebuilt from state, which serves as a baseline to detect workers that were registered but failed to reconnect.

**Scalability**: read-heavy by nature; can be extracted into a read-replica architecture or partitioned by region or rack.

</Accordion>

<Accordion title="Telemetry Collector" id="telemetry-collector">

**Responsibility**: a lightweight proxy and routing layer between the upper system (UI / AI) and the downstream workers. Rather
than `@tetherto/mdk-ork` performing heavy time-series aggregations, the *worker* is responsible for storing and aggregating data for the
specific devices it controls. The Collector simply provides an interface to query this data and proxies the response up to the
UI (via App Node) or the AI Agent.

**Worker data handling (telemetry context)**:

- *Compaction*: the worker handles compaction of metrics over large time frames.
- *Local storage*: workers should save telemetry data in a local Hyper DB.
- *As-requested serving*: the worker serves data strictly when `@tetherto/mdk-ork` asks for it, precisely as dictated by the telemetry
schemas in `mdk-contract.json`.
- *Internal scheduling*: to achieve this without blocking `@tetherto/mdk-ork`, the worker may run its own internal scheduler for device
polling.

**Interfaces**:

- *Input*: client or AI telemetry queries (for example, "fetch metrics for device wm001").
- *Output*: normalized telemetry payloads passed straight through from the Worker to the requesting layer.
- *Functions*: `proxyTelemetryFetch(deviceId, queryArgs)`

**State machine**:

```mermaid
stateDiagram-v2
    [*] --> Idle
    Idle --> Proxying : Request received
    Proxying --> RoutingToClient : Worker returns data
    Proxying --> Timeout : Worker unresponsive
    RoutingToClient --> Idle
```

**Scalability**: because the heavy lifting of data storage and aggregation is pushed down into the isolated worker processes,
the Collector remains stateless and highly scalable as a pure asynchronous router.

</Accordion>

<Accordion title="Scheduler" id="scheduler">

**Responsibility**: the system metronome. Triggers repetitive tasks without holding any domain-specific logic itself.

*Example*: emits an internal `tick` event every 5 seconds that the Health Monitor listens to, prompting it to ping all workers.

**Interfaces**:

- *Input*: system clock and configured task intervals.
- *Output*: injects intents (for example, `telemetry.pull`, `health.ping`) into the Dispatcher or Collector.
- *Functions*: `addJob(interval, intent)`, `removeJob(jobId)`

**State machine**:

```mermaid
stateDiagram-v2
    [*] --> Waiting
    Waiting --> Triggered : Interval elapsed
    Triggered --> Waiting
```

**Crash recovery**: timers re-initialize from zero on startup. Tasks are strictly idempotent.

**Scalability**: scales trivially. Requires basic distributed locking to avoid duplicate ticks in multi-process `@tetherto/mdk-ork`
deployments.

</Accordion>

<Accordion title="Health Monitor" id="health-monitor">

**Responsibility**: continuously evaluates the liveness and readiness of every registered worker to prevent routing messages to
dead nodes.

*Example*: if `health.ping` to a worker fails three times in a row, the Health Monitor marks the worker's status as `SICK` and
tells the Registry to halt routing new commands there.

**Interfaces**:

- *Input*: executes `health.ping` sequentially based on Scheduler ticks.
- *Output*: pushes status updates to the Registry.
- *Functions*: `pingWorker(workerId)`, `getHealth(workerId)`

**State machine**:

```mermaid
stateDiagram-v2
    [*] --> UNKNOWN
    UNKNOWN --> HEALTHY : Ping success
    HEALTHY --> SICK : Ping failed (1)
    SICK --> DEAD : Ping failed (threshold)
    SICK --> HEALTHY : Ping success
    DEAD --> HEALTHY : Reconnected
```

**Crash recovery**: blank slate on startup; re-evaluates all known workers immediately via ping.

**Scalability**: operates locally per `@tetherto/mdk-ork` kernel, or via independent lightweight ping agents.

</Accordion>

<Accordion title="Fault Supervisor (> v0.0.1)" id="fault-supervisor">

**Idea**: implements circuit-breaker patterns to protect the overall system from cascading failures caused by bad hardware or
software bugs (for example, rejecting commands during a cooling period after repeated errors).

**Status**: deferred for the first cut to keep the core orchestrator simple. If the use case arises (such as complex retry
backoffs or cluster destabilization), it will be reintroduced.

</Accordion>

<Accordion title="Concurrency Manager (> v0.0.1)" id="concurrency-manager">

**Idea**: provides guaranteed lock management and queue limits to ensure mutually exclusive commands do not overlap on physical
devices.

**Status**: deferred for the first cut. The system relies solely on the basic command queue. If explicit global locks or
backpressure limits become necessary, this module will be built out.

</Accordion>

</Accordions>

# Protocol reference (/v0-4-0/reference/protocol)

The MDK Protocol is the contract that crosses every layer of the stack: Workers, `@tetherto/mdk-ork`, and the App Node all
exchange the same envelope. This subsection holds the canonical specs. For the architectural narrative explaining
how the protocol fits together, see [Architecture](/v0-4-0/concepts/architecture#the-mdk-protocol).

## What's documented

- **[Messages](/v0-4-0/reference/protocol/messages)**: envelope schema, request/response examples, the full action
  catalogue, and the base command set.

# Protocol messages (/v0-4-0/reference/protocol/messages)

Every MDK Protocol message uses the same envelope regardless of which layers are talking. This page is the canonical
spec for that envelope, the full set of protocol actions, and the base command set every worker must support. For the
architectural narrative explaining when each action fires, see [Architecture](/v0-4-0/concepts/architecture#the-mdk-protocol).

## Envelope

```json
{
  "id":            "uuid-v4",
  "version":       "0.1.0",
  "type":          "request | response | event",
  "action":        "<protocol action>",
  "sender":        "<component:type:instance>",
  "target":        "<component:type:instance> | null",
  "deviceId":      "string | null",
  "timestamp":     1711640000000,
  "payload":       {}
}
```

External consumers (UI or AI agents) only provide `deviceId`; the `target` worker identity is internally resolved by `@tetherto/mdk-ork`.

A concrete request and response pair, end to end:

```json
// request: App Node asks @tetherto/mdk-ork to reboot device wm001
{
  "id": "8d1c-e3a4",
  "version": "0.1.0",
  "type": "request",
  "action": "command.request",
  "sender": "appNode:fleet-api:1",
  "target": null,
  "deviceId": "wm001",
  "timestamp": 1711640000000,
  "payload": { "command": "reboot" }
}

// response: @tetherto/mdk-ork relays the worker's terminal result
{
  "id": "1f9b-77c2",
  "version": "0.1.0",
  "type": "response",
  "action": "command.result",
  "sender": "ork:kernel:tx-1",
  "target": "appNode:fleet-api:1",
  "deviceId": "wm001",
  "timestamp": 1711640002145,
  "payload": { "status": "SUCCESS", "elapsedMs": 2145 }
}
```

## Actions

| Action | Type | Direction | Purpose |
|---|---|---|---|
| *(DHT presence)* | passive | Worker to DHT topic | Worker joins a known Hyperswarm topic; `@tetherto/mdk-ork` detects its peer connection automatically |
| `identity.request` | request | `@tetherto/mdk-ork` to Worker | Requests the worker's identity and managed devices |
| `capability.request` | request | `@tetherto/mdk-ork` to Worker | Asks the worker to declare its full capability schema |
| `state.pull` | request | `@tetherto/mdk-ork` to Worker | Worker returns a snapshot of state-machine status (low cadence tick, e.g., 60s) |
| `telemetry.pull` | request | `@tetherto/mdk-ork` to Worker | Worker returns device metrics plus history (medium cadence tick, e.g., 10s) |
| `command.request` | request | `@tetherto/mdk-ork` to Worker | Resolves the worker by `deviceId` and dispatches the command for execution |
| `health.ping` | request | `@tetherto/mdk-ork` to Worker | Liveness probe (high cadence tick, e.g., 5s) |

## Base command set

The protocol standardizes a Base Command Set supported by every worker:

- `getConfig`: retrieve current device configuration
- `setConfig`: update device configuration parameters
- `health`: fetch detailed diagnostic health status from the device

# Supported hardware (/v0-4-0/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, MicroBT
- **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](/v0-4-0/concepts/terminology) (ORK, worker, manager, thing, mock)
- Decide how to run the worker service — [Deployment topologies](/v0-4-0/concepts/deployment-topologies)
- Run a miner worker — [Run a miner worker](/v0-4-0/how-to/miners)

# MDK Repositories (/v0-4-0/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](/v0-4-0/community/contributing#licensing).*

# Roadmap (/v0-4-0/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<br/><b>Foundation</b><br/>Public, end-to-end but rudimentary])
        it1[[Jun–Jul 2026<br/>New release every 2 weeks]]
    end

    subgraph lab [Lab testing]
        p2([Jul 2026<br/><b>Lab testing</b><br/>Complete end-to-end, lab only])
        it2[[Aug–Oct 2026<br/>New release every 2 weeks]]
    end

    subgraph onsite [On site testing]
        p3([Oct 2026<br/><b>On site testing</b><br/>Testing at real sites])
        it3[[Nov–Dec 2026<br/>New release every 2 weeks]]
    end

    subgraph prod [Production Ready]
        p4([Dec 2026<br/><b>Production Ready</b><br/>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.

# Backend stack tutorials (/v0-4-0/tutorials/backend-stack)

<Callout type="info">
If ORK, worker, manager, or thing are unfamiliar, read [`terminology.md`](/v0-4-0/concepts/terminology) first.
</Callout>

## Overview

Get started with MDK in three short tutorials that build on each other. Each rung adds one layer of capability: first you watch the stack run, then you drive it, then you run a browser demo on top.

| Rung | You'll | You'll end with | Mock hardware | Time |
|---|---|---|---|---|
| [1. Run the stack](/v0-4-0/tutorials/backend-stack/run) | **Observe** — run one command and watch a stack come up | ORK plus one registered device, IDs printed | Antminer S19XP | `< 3 min` |
| [2. Control devices from the CLI](/v0-4-0/tutorials/backend-stack/cli) | **Interact** — drive a running stack from a REPL | Live telemetry and commands over an IPC socket | Whatsminer M56S | `< 3 min` |
| [3. Run the dashboard demo](/v0-4-0/tutorials/full-stack/dashboard) | **Run** — launch a browser dashboard on the stack | A React dashboard with live charts at `:3030` | Whatsminer M56S | `< 15 min` after a one-time UI build |

**New to MDK? Start with [1. Run the stack](/v0-4-0/tutorials/backend-stack/run).** Each rung links to the next, so you can climb straight through.

<Callout type="info">
Rungs uses different mock hardware on purpose — Antminer on rung 1, Whatsminer on rungs 2 and 3. Notice, the MDK API stays identical: every rung calls the same `getOrk()`, `startWorker()`, and `registerThing()` shape. Only the worker class and the mock device change. That sameness is the point — one interface, any hardware.
</Callout>

Each rung is self-contained and repeats the clone-and-install step, so you can start at whichever one you need.

# 2. Control from CLI (/v0-4-0/tutorials/backend-stack/cli)

*Get started · 2 of 3 · Control devices from the CLI*

<Callout type="info">
If ORK, worker, manager, or thing are unfamiliar, read [`terminology.md`](/v0-4-0/concepts/terminology) first.
</Callout>

## Overview

This is rung 2 of the [Get started](/v0-4-0/tutorials/backend-stack) ladder: **interact**. It walks the shortest path from a fresh clone to a fully wired MDK stack
you can drive interactively from a CLI. Everything runs in one Node process, no real hardware required.

What you'll have at the end:

- A mock Whatsminer M56S serving telemetry on `127.0.0.1:14028`
- An ORK with one worker registered and one device discovered
- An interactive `client.js` REPL talking to ORK over its IPC socket — pull metrics, list workers, send commands like `reboot` and `setpower`
- (Optional) An App Node HTTP API on `:3000` so non-Node consumers (browsers, AI agents over MCP) can hit the same stack over REST

<Callout type="info">
Same shape as [rung 1](/v0-4-0/tutorials/backend-stack/run): the stack still boots with `getOrk()`, `startWorker()`, and `registerThing()`. Only the worker
class (`WM_M56S`) and the mock hardware (Whatsminer instead of Antminer) change. What's new here is a second process — `client.js` — that connects
over IPC and drives the running stack.
</Callout>

The example lives in [`examples/backend/mdk-e2e/`](https://github.com/tetherto/mdk/blob/main/examples/backend/README.md#end-to-end-mdk-e2e) and contains six runnable scripts. This tutorial uses
three of them: `run.js` for a smoke test, `server.js` for the long-running stack, and `client.js` for interactive control.

## Prerequisites

- Node.js >=24 (LTS)
- npm >=11

<Callout type="warn">
The stack starts an ORK whose control plane is peer-to-peer over a Hyperswarm DHT, so it needs outbound network access. Without it,
the stack stalls at startup while the ORK tries to reach DHT bootstrap nodes. See [how workers connect](/v0-4-0/concepts/stack/workers) for the ORK/DHT mechanics.
</Callout>

<Steps>

<Step>

### Clone and install

#### 1.1 Clone the repo

```bash
git clone git@github.com:tetherto/mdk.git
cd mdk
```

#### 1.2 Install dependencies

The monorepo has two workspaces with their own dependency trees. Install both:

```bash
backend/core/install-packages.sh ci
backend/workers/install-packages.sh ci
```

<Callout type="info">
Each script walks every `package.json` under its workspace and runs `npm ci`. The `examples/mdk-e2e/` package is included automatically — no extra
install step needed.
</Callout>

</Step>

<Step>

<details>
<summary>(1.3 Optional) Smoke test the stack</summary>

Before going interactive, prove the wiring works. `run.js` starts a mock Whatsminer + worker + ORK in one process, exercises a few queries, prints the
results, and exits cleanly:

```bash
node examples/backend/mdk-e2e/run.js
```

Expected output (the UUID and metric values vary):

```
Devices: [ '8f3e9a2b-7c1d-4e5a-9f8b-6c2d1e3f4a5b [miner-wm-m56s]' ]
Telemetry: ONLINE hashrate=170000 power=3500W
Commands: reboot, setPowerMode, setLED, setPowerPct, setupPools, saveComment
```

If you see those three lines, every layer is working: the mock is responding, the worker registered the device, ORK discovered the worker over the local
DHT topic, and IPC routing is delivering envelopes both ways. The script tears itself down and exits with code 0.

<Callout type="warn">
If the smoke test fails with `EADDRINUSE` on port 14028, a previous run left a Node process alive. Kill stragglers with `pkill -f mdk-e2e` and retry.
</Callout>

</details>

</Step>

<Step>

### Run the interactive demo

#### 3.1 Start the stack

In your terminal:

```bash
node examples/backend/mdk-e2e/server.js
```

`server.js` starts the same mock + worker + ORK as `run.js`, but stays running and prints the IDs you'll need:

```
  ORK key:  7a4c8b...e3f0
  Device:   8f3e9a2b-7c1d-4e5a-9f8b-6c2d1e3f4a5b

  hp-rpc-cli -s 7a4c8b...e3f0 -m mdk -d '{...}'
  hp-rpc-cli -s 7a4c8b...e3f0 -m mdk -d '{...}'

  Ctrl+C to stop.
```

The `ORK key` is a 64-char hex public key. `Device` is a UUIDv4 generated at registration time. Both vary per run — note the device UUID for the next step.

The two `hp-rpc-cli` lines are paste-ready commands for inspecting ORK over HRPC from another machine. You don't need them for this tutorial — they're
there if you have [`hp-rpc-cli`](https://github.com/holepunchto/hp-rpc-cli) installed and want to go off-script.

#### 3.2 Connect the interactive client

Open a second terminal in the same `mdk` directory:

```bash
node examples/backend/mdk-e2e/client.js
```

`client.js` connects to ORK's default IPC socket and gives you an MDK REPL:

```
  MDK Client — connected to IPC: /tmp/mdk/ork.sock
  Type "help" for commands, "quit" to exit.

mdk>
```

#### 3.3 Drive the stack

3.3.1 Discover the telemetry of your (mock) device by entering commands at the `mdk>` prompt, substituting `<deviceId>` with the UUID printed in Step 3.1:

```
mdk> metrics <deviceId>
```

Each command builds an MDK Protocol envelope, writes it to ORK's IPC socket, and prints the JSON response.

3.3.2 Try changing the power mode and observing the effect:

```
mdk> setpower <deviceId> low
mdk> metrics <deviceId>
```

After `setpower ... low` the second `metrics` call should reflect the power mode change.

{/* sync with examples/backend/mdk-e2e/client.js help block */}
<details>
<summary>Full command reference</summary>

**Reads**

```
workers                      — list workers
list [deviceId]              — list devices
count [deviceId]             — device count
metrics <deviceId>           — live telemetry from hardware
logs <deviceId>              — recent logs
settings [deviceId]          — worker settings
stats [deviceId]             — fleet stats
config <deviceId>            — device config (pools, etc.)
capabilities <deviceId>      — mdk-contract capabilities
state [deviceId]             — worker state snapshot
```

**Commands**

```
reboot <deviceId>            — reboot miner
setpower <deviceId> <mode>   — set power mode (normal/low/high)
setled <deviceId> [on|off]   — toggle LED
```

```
quit / exit                  — exit client
```

</details>

#### 3.4 Tear down

When you're done, exit the client and stop the stack:

```
mdk> quit
```

Then `Ctrl+C` in Terminal 1.

</Step>

<Step>

### 4 (Optional) enable App Node for HTTP access

In **Terminal 1**, `Ctrl+C` the running stack, then restart with `--app-node`:

```bash
node examples/backend/mdk-e2e/server.js --app-node
```

You'll see an extra line in the startup banner:

```
  App-node: http://localhost:3000 (noAuth mode)
```

Confirm it's alive — App Node has no index page, so hit `/auth/site` directly:

```bash
curl http://localhost:3000/auth/site
```

You should see something like `{"site":"Site_Name"}`.

<Callout type="info">
In `noAuth` mode most data endpoints (e.g. `/auth/list-things`, `/auth/miners`) are unavailable — they require the cache and auth config that the
full App Node service sets up. `--app-node` in `server.js` is a development shortcut; for full REST access to device telemetry, run App Node as a
full service. See [`backend/core/app-node/`](https://github.com/tetherto/mdk/blob/main/backend/core/app-node/README.md) for setup.
</Callout>

<Callout type="warning">
`--app-node` runs in `noAuth` mode for development convenience. Do not expose port 3000 outside localhost.
</Callout>

</Step>

</Steps>

## What just happened

Your stack wired up, in order:

1. **Mock device**:`server.js` calls `wmMock.createServer({ port: 14028, ... })` — an HTTP server speaking the Whatsminer protocol with canned telemetry.
2. **ORK**: `getOrk()` boots the kernel, generates a random DHT topic, and opens an IPC socket at the default path (`os.tmpdir()/mdk/ork.sock`).
3. **Worker**: `startWorker(WM_M56S, { ork })` instantiates the Whatsminer manager, mounts its protocol adapter, and registers with ORK directly — no
DHT round-trip because they share the process.
4. **Thing registration**: `manager.registerThing({ info, opts })` tells the worker about the device at `127.0.0.1:14028`. The worker stores the
registration and starts polling.
5. **Client**: `client.js` opens the IPC socket from a second process and sends MDK Protocol envelopes (`worker.list`, `telemetry.pull`, `command.request`, ...).
ORK routes them to the worker, the worker hits the mock, and the response flows back over IPC.

<Callout type="info">
No App Node here? Right. App Node is the translator that lets non-Node consumers — browser UIs, AI agents over MCP — speak MDK Protocol to ORK.
`client.js` already speaks MDK Protocol over IPC, so it talks to ORK directly. App Node becomes mandatory only when the consumer can't open a UNIX socket.
See [`architecture.md#app-node`](/v0-4-0/concepts/architecture#app-node).
</Callout>

## Cleanup

`Ctrl+C` in Terminal 1 stops the worker, ORK, and mock cleanly. `run.js` deletes its own state directory on exit. `server.js` leaves data
under `os.tmpdir()/mdk/` — safe to ignore, or remove with:

```bash
rm -rf "$TMPDIR/mdk" /tmp/mdk
```

## Continue

Next: [3. Run the dashboard demo](/v0-4-0/tutorials/full-stack/dashboard): put a browser dashboard on a running stack with live charts.

## Go deeper

- Walk the simpler single-script Antminer path: [1. Run the stack](/v0-4-0/tutorials/backend-stack/run)
- Run a [full site (multiple workers and devices):](https://github.com/tetherto/mdk/blob/main/examples/backend/mdk-site/site.js)
- Understand the [install pattern for any worker](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/install-pattern.md)
- Read all [runnable examples in one place](https://github.com/tetherto/mdk/blob/main/examples/backend/README.md)

# 1. Run the stack (/v0-4-0/tutorials/backend-stack/run)

*Get started · 1 of 3 · Run the stack*

<Callout type="info">
If ORK, worker, manager, or thing are unfamiliar, read [`terminology.md`](/v0-4-0/concepts/terminology) first.
</Callout>

## Overview

This is rung 1 of the [Get started](/v0-4-0/tutorials/backend-stack) ladder: **observe**. It walks the shortest path from a fresh clone to a running ORK with one mock Antminer registered, no hardware required. Everything runs in one Node process.

What you'll have at the end:

- A mock Antminer S19XP serving Bitmain CGI endpoints on `127.0.0.1:14021`
- An ORK started and aware of one registered device
- The ORK HRPC key and device ID printed to the terminal — ready for further inspection

## Prerequisites

- Node.js >=24 (LTS)
- npm >=11

<Callout type="warn">
The stack starts an ORK whose control plane is peer-to-peer over a Hyperswarm DHT, so it needs outbound network access. Without it the stack stalls at startup while the ORK tries to reach DHT bootstrap nodes. See [how workers connect](/v0-4-0/concepts/stack/workers) for the ORK/DHT mechanics.
</Callout>

<Steps>

<Step>

### Clone and install

#### 1.1 Clone the repo

```bash
git clone git@github.com:tetherto/mdk.git
cd mdk
```

#### 1.2 Install dependencies

The monorepo has two workspaces with their own dependency trees. Install both:

```bash
backend/core/install-packages.sh ci
backend/workers/install-packages.sh ci
```

<Callout type="info">
Each script walks every `package.json` under its workspace and runs `npm ci`.
</Callout>

</Step>

<Step>

### Run the example

```bash
node backend/workers/miners/antminer/examples/run-s19xp.js
```

Expected output (the hex key varies):

```
  ORK HRPC key: 7a4c8b...e3f0
  Device: t-miner-am-s19xp-127-0-0-1

  Ctrl+C to stop.
```

If you see those two lines, the whole stack is up: mock device responding, worker registered, ORK started and aware of the device.

<Callout type="warn">
If the example fails with `EADDRINUSE`, a previous run left port 14021 bound. Kill stale Node processes with `pkill -f run-s19xp` and retry.
</Callout>

</Step>

</Steps>

## What just happened

Here is what [`run-s19xp.js`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/examples/run-s19xp.js) does, line by line — the minimum runnable shape for an MDK stack:

```js
const { getOrk, startWorker } = require('@tetherto/mdk')
const { AM_S19XP } = require('@tetherto/miner-antminer')
const amMock = require('@tetherto/miner-antminer/mock/server')

// Start a mock Antminer S19XP on port 14021 to emit telemetry
amMock.createServer({ port: 14021, host: '127.0.0.1', type: 's19xp', serial: 'AM-001', password: 'root' })

// Start ORK — the orchestration kernel that manages all workers
const ork = await getOrk()

// Start the Antminer worker — it joins the same DHT topic as ORK, allowing ORK to discover and pull the worker's identity and capabilities
const { manager } = await startWorker(AM_S19XP, { ork })

// Register the mock Antminer device with the worker so it starts polling
await manager.registerThing({
  info: { container: 'site-1', serialNum: 'AM-001' },   // logical identity
  opts: { address: '127.0.0.1', port: 14021, username: 'root', password: 'root' } // connection details
})
```

Five steps, in order:

1. **Starts a mock miner.** `amMock.createServer({ port: 14021, type: 's19xp', serial: 'AM-001', password: 'root' })` binds port 14021 with a Bitmain-compatible HTTP API serving canned telemetry. It exposes Bitmain CGI paths only — there is no root route (so `curl http://127.0.0.1:14021/` would return 404). To verify the mock directly, use a CGI path with Digest auth: `curl --digest -u root:root http://127.0.0.1:14021/cgi-bin/miner_type.cgi`
2. **Starts ORK.** `getOrk()` brings up the kernel and joins a freshly generated DHT topic.
3. **Starts a worker.** `startWorker(AM_S19XP, { ork })` instantiates the `AM_S19XP` manager class, mounts the protocol adapter, and joins the same DHT topic — ORK detects the new peer and pulls the worker's identity and capabilities.
4. **Registers a thing.** `manager.registerThing({ info, opts })` tells the worker about one device at `127.0.0.1:14021`. The worker stores the registration and begins polling the mock.
5. **Prints the IDs and waits.** The script logs ORK's public HRPC key and the device ID, then sits idle. Ctrl+C tears everything down.

<Callout type="info">
No App Node here? Right. App Node is the translator that lets non-Node consumers — browser UIs, AI agents over MCP — speak MDK Protocol to ORK. This script already speaks MDK Protocol over IPC, so it talks to ORK directly. See [`architecture.md#app-node`](/v0-4-0/concepts/architecture#app-node) for when App Node is mandatory.
</Callout>

## Cleanup

`Ctrl+C` stops the mock, worker, and ORK cleanly. The script uses the default ORK root at `os.tmpdir()/mdk/` — safe to ignore, or remove with:

```bash
rm -rf "$TMPDIR/mdk" /tmp/mdk
```

## Continue

Next: [2. Control devices from the CLI](/v0-4-0/tutorials/backend-stack/cli) — keep a stack running and drive it interactively (Whatsminer plus an MDK REPL).

## Next steps

- For the four supported Antminer models (`AM_S19XP`, `AM_S19XPH`, `AM_S21`, `AM_S21PRO`) and how to swap them, see [`backend/workers/miners/antminer/USAGE.md`](https://github.com/tetherto/mdk/blob/main/backend/workers/miners/antminer/USAGE.md)
- Run a full site (5 workers, 26 devices) — [`examples/backend/mdk-site/site.js`](https://github.com/tetherto/mdk/blob/main/examples/backend/mdk-site/site.js)
- See ORK and a worker as separate OS processes — [`dht-worker.js`](https://github.com/tetherto/mdk/blob/main/examples/backend/mdk-e2e/dht-worker.js) + [`dht-ork.js`](https://github.com/tetherto/mdk/blob/main/examples/backend/mdk-e2e/dht-ork.js) + [`client.js`](https://github.com/tetherto/mdk/blob/main/examples/backend/mdk-e2e/client.js)
- Understand the install pattern for any worker — [`backend/workers/v0-4-0/install-pattern.md`](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/install-pattern.md)
- Use ORK directly without `getOrk()` — [`backend/workers/v0-4-0/orchestrator.md`](https://github.com/tetherto/mdk/blob/main/backend/workers/docs/orchestrator.md)
- Read all runnable examples in one place — [`examples/backend/README.md`](https://github.com/tetherto/mdk/blob/main/examples/backend/README.md)

# Run a demo from stack to dashboard (/v0-4-0/tutorials/full-stack/dashboard)

*Get started · 3 of 3 · Run the dashboard demo*

<Callout type="info">
If ORK, worker, manager, or thing are unfamiliar, read [`terminology.md`](/v0-4-0/concepts/terminology) first.
</Callout>

## Overview

This is rung 3 of the [Get started](/v0-4-0/tutorials/backend-stack) ladder: **run**. It puts a real browser dashboard on top of the stack you already know, using
the [`mdk-site-monitor`](https://github.com/tetherto/mdk/tree/main/examples/e2e) example.

What you'll have at the end:

- The same mock Whatsminer M56S stack from [rung 2](/v0-4-0/tutorials/backend-stack/cli), fronted by an App Node REST API on `:3000`
- A React dashboard on `:3030` — total hashrate, total power, and active device count as metric cards, a live hashrate chart, and a per-device breakdown
- The dashboard polling App Node every 5 seconds, built entirely from MDK UI components (`MetricCard`, `HashRateLineChart`, `Typography`, ...)

<Callout type="info">
Same shape as rungs 1 and 2: underneath, the stack still boots with `getOrk()`, `startWorker()`, and `registerThing()`, and the device is the same
Whatsminer M56S mock as [rung 2](/v0-4-0/tutorials/backend-stack/cli). What's new is two layers on top — App Node translating MDK Protocol to REST, and a Vite React UI
consuming that REST.
</Callout>

## Prerequisites

- Node.js >=24 (LTS)
- npm >=11

<Callout type="warn">
The stack starts an ORK whose control plane is peer-to-peer over a Hyperswarm DHT, so it needs outbound network access. Without it the stack stalls
at startup while the ORK tries to reach DHT bootstrap nodes. See [how workers connect](/v0-4-0/concepts/stack/workers)
for the ORK/DHT mechanics.
</Callout>

<Steps>

<Step>

### Clone and install the stack

#### 1.1 Clone the repo

```bash
git clone git@github.com:tetherto/mdk.git
cd mdk
```

#### 1.2 Install dependencies

```bash
backend/core/install-packages.sh ci
backend/workers/install-packages.sh ci
```

</Step>

<Step>

### Build the UI packages (one time)

The dashboard imports pre-built MDK UI packages from `ui`. Build them once:

```bash
cd ui
npm install
npm run build
cd -
```

<Callout type="info">
This is the slow step — it compiles the UI workspace. You only do it once; re-running the dashboard later skips straight to the next step.
</Callout>

</Step>

<Step>

### Install the dashboard's dependencies

```bash
cd examples/e2e/ui
npm install
cd -
```

</Step>

<Step>

### Start everything

`start.js` is an interactive launcher. From the repo root:

```bash
node examples/e2e/start.js
```

It prints the available services and a prompt. Type `start all` to launch the backend and UI together:

```
mdk> start all
```

This starts:

1. **App Node** (`server.js --app-node`) — ORK + mock Whatsminer + a REST API on `http://localhost:3000`, in `noAuth` mode
2. **UI** — a Vite dev server on `http://localhost:3030`, launched with `VITE_NO_AUTH=true` so it skips the login screen

Wait a few seconds for both to come up, then open:

```
http://localhost:3030
```

You'll see the dashboard populate within ~5 seconds (its first poll), then update live.

Other launcher commands:

```
status                       — show running services and their URLs
stop  [ork|app-node|ui|all]  — stop a service (default: all)
start [ork|app-node|ui|all]  — start a service (default: all)
help                         — show usage
exit                         — stop everything and quit
```

<Callout type="warning">
`start all` runs App Node in `noAuth` mode for development convenience. Do not expose port 3000 outside localhost.
</Callout>

</Step>

</Steps>

## What you'll see

```mermaid
flowchart TD
  mock["Mock Whatsminer M56S (TCP :14028)"] --> worker["MDK worker"]
  worker --> ork["ORK (IPC socket)"]
  ork --> appnode["App Node (HTTP :3000)"]
  appnode -->|"GET /site-monitor/hashrate every 5s"| ui["Dashboard (Vite :3030)"]
```

The page renders three metric cards (Total Hashrate in TH/s, Total Power in W, Active Devices), a live hashrate line chart that grows as new data points
arrive, and a per-device table. All of it comes from `@tetherto/mdk-react-devkit` components driven by the `@tetherto/mdk-react-adapter` data hooks —
no hand-rolled UI. See [`SiteHashratePage.tsx`](https://github.com/tetherto/mdk/blob/main/examples/e2e/ui/src/SiteHashratePage.tsx) for the page source.

## Cleanup

At the launcher prompt:

```
mdk> exit
```

`exit` stops App Node, the mock, ORK, and the UI dev server. `server.js` leaves data under `os.tmpdir()/mdk/` — safe to ignore, or remove with:

```bash
rm -rf "$TMPDIR/mdk" /tmp/mdk
```

<details>
<summary>Add real auth (Google OAuth)</summary>

The `start all` shortcut above runs in `noAuth` mode so you can see the dashboard immediately. To exercise the real authentication flow — a Google
sign-in that issues a bearer token the dashboard sends on every request — configure OAuth and start the services without the `noAuth` shortcut.

**1. Create a Google OAuth 2.0 client**

1. Go to [Google Cloud Console API credentials](https://console.cloud.google.com/apis/credentials)
2. Create an **OAuth 2.0 Client ID** (type: *Web application*)
3. Add to **Authorized redirect URIs**: `http://localhost:3000/oauth/google/callback`
4. Add to **Authorized JavaScript origins**: `http://localhost:3030`
5. Copy the **Client ID** and **Client Secret**

**2. Configure App Node**

Edit `backend/core/app-node/config/facs/httpd-oauth2.config.json` (copy from `.example` if it doesn't exist) and fill in your client ID and secret:

```json
{
  "h0": {
    "method": "google",
    "credentials": { "client": { "id": "<YOUR_CLIENT_ID>", "secret": "<YOUR_CLIENT_SECRET>" } },
    "startRedirectPath": "/oauth/google",
    "callbackUri": "http://localhost:3000/oauth/google/callback",
    "callbackUriUI": "http://localhost:3000",
    "users": []
  }
}
```

**3. Set yourself as super-admin**

In `backend/core/app-node/config/facs/auth.config.json`, set `superAdmin` to your Google account email:

```json
{ "a0": { "superAdmin": "you@example.com" } }
```

**4. Run with auth**

Start App Node and the UI without the `VITE_NO_AUTH` shortcut (run them directly rather than via `start all`), open `http://localhost:3030`,
click **Sign in with Google**, and authorise with the super-admin email. The dashboard then shows live data behind the token.

For the full setup, see the example's [`README.md`](https://github.com/tetherto/mdk/tree/main/examples/e2e).

</details>

## Continue

Previous: [2. Control devices from the CLI](/v0-4-0/tutorials/backend-stack/cli)

You've climbed the whole ladder: observed a stack, driven it from a CLI, and built a live UI on it.

## Go deeper

- The [full example, including the OAuth setup and how to add a new data panel](https://github.com/tetherto/mdk/tree/main/examples/e2e)
- Learn more about [MDK UI toolkit (components, hooks, theming)](https://github.com/tetherto/mdk/blob/main/ui/README.md)
- Run a full site (multiple workers and devices)[site-example](https://github.com/tetherto/mdk/blob/main/examples/backend/mdk-site/site.js)
- Read [all runnable examples in one place](https://github.com/tetherto/mdk/blob/main/examples/backend/README.md)

# Build a dashboard with an agent (/v0-4-0/tutorials/ui/react/build-any-dashboard-with-an-agent)

This tutorial walks a **realistic agent session** end to end: the same two-step flow as
[Build dashboards with your AI agent](/v0-4-0/agents).

## Overview

MDK's React UI Kit's library of presentational and composable building blocks can be used wherever suits you.

Mining is one of many disciplines that benefits from charts, tables, tabs, and stat cards. Your telemetry; your dash.

You bring *your* labels, *your* mock data, and *your* layout. 

The first thing we built was [Mining Dashboards](/v0-4-0/ui/react/get-started). 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](/v0-4-0/reference/app-toolkit/ui-cli) command the agent would run
- Resulting page code
- Run instructions

<Callout type="info">
We use mock JSON in the browser. No ORK worker, pool API, or fleet backend is required. Presentational imports from
`@tetherto/mdk-react-devkit/core` are enough for a highly visual, shippable UI.
</Callout>

## 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+

{/* npm packages are not yet published to the registry; clone the MDK UI monorepo and use workspace dependencies until publish. */}

- A project folder inside the MDK UI monorepo (this walkthrough uses `apps/stats-lab`, same pattern as
  [Wire a React app](/v0-4-0/tutorials/ui/react/tutorial#create-your-app))
- [Build dashboards with your AI agent](/v0-4-0/agents) wired once (`mdk-ui init`) or willingness to run `init` in step 1

## Agent session walkthrough

<Steps>

<Step>

### 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](/v0-4-0/reference/app-toolkit/ui-cli#init).

</Step>

<Step>

### State a non-mining intent

Paste a prompt that names the domain explicitly so the agent does not reach for hashrate widgets:

> Build a **statistics tutorial dashboard** for students in `apps/stats-lab`. Include:
>
> - A **histogram** of final exam scores (bar chart buckets 50–59 through 90–100)
> - A **line chart** of weekly class average over six weeks
> - A **sortable table** of students with midterm, final, and section
> - Three **summary stat cards**: mean final, median final, enrollment count
>
> Use mock data in the repo. Import only from `@tetherto/mdk-react-devkit/core` and `@tetherto/mdk-react-devkit/foundation` where
> needed. No mining APIs.

The agent's job is unchanged from [Build dashboards with your AI agent](/v0-4-0/agents#prompt-your-agent): discover exports,
scaffold, and verify compile.

</Step>

<Step>

### Discovery commands the agent runs

Behind the prompt, a well-behaved session issues deterministic CLI lookups (no model calls). A representative transcript:

<Accordions>

<Accordion title="Rank intent → shortlist">

```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`.

</Accordion>

<Accordion title="Filter by capability">

```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.

</Accordion>

<Accordion title="Read contracts before coding">

```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.

</Accordion>

<Accordion title="Scaffold and verify">

```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.

</Accordion>

</Accordions>

Full command reference: [UI CLI](/v0-4-0/reference/app-toolkit/ui-cli).

</Step>

<Step>

### Review the scaffolded page

After the agent edits `App.tsx`, a Stats Lab dashboard might look like this:

<Accordions>

<Accordion title="Full Stats Lab page source">

```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<DataTableSortingState>([])

  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<Student>[] = [
    { accessorKey: 'name', header: 'Student' },
    { accessorKey: 'section', header: 'Section' },
    { accessorKey: 'midterm', header: 'Midterm' },
    { accessorKey: 'final', header: 'Final' },
  ]

  return (
    <div style={{ padding: '2rem', display: 'grid', gap: '1.5rem' }}>
      <header>
        <h1 style={{ margin: 0 }}>Stats Lab</h1>
        <p style={{ margin: '0.5rem 0 0', opacity: 0.8 }}>
          Intro statistics — interpret distributions and trends
        </p>
      </header>

      <div
        style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))',
          gap: '1rem',
        }}
      >
        <SingleStatCard
          name="Mean final"
          value={Math.round(meanFinal)}
          unit="pts"
          variant="primary"
        />
        <SingleStatCard
          name="Median final"
          value={medianFinal}
          unit="pts"
          variant="secondary"
        />
        <SingleStatCard
          name="Enrollment"
          value={students.length}
          unit="students"
          variant="tertiary"
        />
      </div>

      <Tabs defaultValue="charts">
        <TabsList>
          <TabsTrigger value="charts">Charts</TabsTrigger>
          <TabsTrigger value="roster">Roster</TabsTrigger>
        </TabsList>
        <TabsContent value="charts">
          <div
            style={{
              display: 'grid',
              gap: '1.5rem',
              gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
            }}
          >
            <section>
              <h2 style={{ fontSize: '1rem' }}>Final exam distribution</h2>
              <BarChart data={histogram} height={280} />
            </section>
            <section>
              <h2 style={{ fontSize: '1rem' }}>Weekly class average</h2>
              <LineChart
                data={weeklyAverage}
                height={280}
                unit="pts"
                beginAtZero
              />
            </section>
          </div>
        </TabsContent>
        <TabsContent value="roster">
          <DataTable
            columns={columns}
            data={students}
            sorting={sorting}
            onSortingChange={setSorting}
          />
        </TabsContent>
      </Tabs>
    </div>
  )
}

```

</Accordion>

</Accordions>

Nothing in this file references pools, workers, or TH/s. The same components appear on mining pages because the **data model is
generic**.

</Step>

</Steps>

## Run Stats Lab locally

Follow the same monorepo workflow as [Wire a React app](/v0-4-0/tutorials/ui/react/tutorial). If you already completed that tutorial,
skip to **Run the app** with `stats-lab` as the workspace name.

<Steps>

<Step>

### Clone and build the UI monorepo

<Tabs>
  <Tab value="https" label="HTTPS" default>

```bash
git clone https://github.com/tetherto/mdk.git
cd mdk
```

  </Tab>
  <Tab value="ssh" label="SSH">

```bash
git clone git@github.com:tetherto/mdk.git
cd mdk
```

  </Tab>
</Tabs>

```bash
npm install
npm run build
```

</Step>

<Step>

### 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-core": "*",
```

Install from the monorepo root:

```bash
cd ../..
npm install
```

</Step>

<Step>

### Wrap with `MdkProvider`

In `apps/stats-lab/src/main.tsx`, mirror
[Wire a React app — Wrap your app in MdkProvider](/v0-4-0/tutorials/ui/react/tutorial#wrap-your-app-in-mdkprovider):

```tsx

// …
<MdkProvider apiBaseUrl="https://example.invalid">
  <App />
</MdkProvider>
```

Mock data does not call the API; the provider satisfies components that expect React context.

</Step>

<Step>

### 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.

</Step>

<Step>

### Optional: compare with the MDK demo app

Same commands as [Wire a React app — Run the demo app](/v0-4-0/tutorials/ui/react/tutorial#run-the-demo-app):

```bash
npm run dev:catalog
```

Open [http://localhost:5173/mdk](http://localhost:5173/mdk) to browse mining-oriented examples, then contrast with Stats Lab:
**same primitives, different story**.

</Step>

</Steps>

## Why the agent stays accurate

Manifests list real exports, `check` catches many invented props, and `docs` / `example` ground the session in shipped contracts.
The app build remains the final TypeScript and Vite verification.

## Next steps

- [Build dashboards with your AI agent](/v0-4-0/agents): the two-step entry point
- [UI CLI reference](/v0-4-0/reference/app-toolkit/ui-cli): every command in the discovery transcript
- [Chart components](/v0-4-0/ui/react/core/components/charts): `BarChart`, `LineChart`, and related data shapes
- [Data display](/v0-4-0/ui/react/core/components/data-display): `DataTable` sorting and pagination
- [Wire a React app](/v0-4-0/tutorials/ui/react/tutorial): hands-on monorepo setup without an agent

# Your first component (/v0-4-0/tutorials/ui/react/first-component)

This tutorial assists first-time users to understand how to render a single MDK component in a React app as fast as possible. 

<Callout type="warn">
This path skips `<MdkProvider>` which works for presentational `/core` and `/foundation` imports. For adapter hooks or connected foundation components, 
continue with [Wire a React app](/v0-4-0/tutorials/ui/react/tutorial).
</Callout>

## Prerequisites

- **Node.js** >=24
- **npm** >=11
- **React** 19+ and **react-dom** 19+

{/* npm packages are not yet published to the registry; clone the MDK UI monorepo and use workspace dependencies until publish. */}

<Steps>

<Step>

### Clone and build

<Callout type="info">
The MDK UI (`mdk/ui`) is an npm workspace. Clone the repository, install at the workspace root, and 
build all packages before adding an app or running the demo.
</Callout>

#### 1.1 Clone the repository

<Tabs>
  <Tab value="https" label="HTTPS" default>

```bash
git clone https://github.com/tetherto/mdk.git
cd mdk/ui
```

  </Tab>
  <Tab value="ssh" label="SSH">

```bash
# Requires an SSH key configured for your GitHub account
git clone git@github.com:tetherto/mdk.git
cd mdk/ui
```

  </Tab>
</Tabs>

#### 1.2 Install and build

```bash
npm install
npm run build
```

This builds `@tetherto/mdk-react-devkit` and the other packages in the `ui/` workspace.

</Step>

<Step>

### Create your app

#### 2.1 Scaffold a Vite React app

Create a new React app in the `apps/` folder:

```bash
cd apps
npm create vite@latest my-app -- --template react-ts
cd my-app
```

<Callout type="info">
Ignore the CLI "Now run" instructions — follow the MDK-specific steps below.
</Callout>

#### 2.2 Add MDK to `package.json`

Open `package.json` and add the workspace dependency:

<Tabs>
  <Tab value="add" label="Add dependency" default>

```jsonc
    "@tetherto/mdk-react-devkit": "*",
```

  </Tab>
  <Tab value="context" label="In context">

```jsonc
{
  "name": "my-app",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "@tetherto/mdk-react-devkit": "*", // [!code ++]
    "react": "^19.2.6", // [!code highlight]
    "react-dom": "^19.2.6" // [!code highlight]
  },
}
```
<Callout type="info">
Vite also adds a `devDependencies` block (`eslint`, `typescript`, `vite`, `@types/*`); leave that block as is.
</Callout>

  </Tab>
</Tabs>

#### 2.3 Install from the workspace root

```bash
cd ../..    # from apps/my-app back up to the workspace root mdk/ui
npm install
```

</Step>

<Step>

### Import styles

#### 3.1 Import the MDK stylesheet

Import the MDK stylesheet once in your app's entry point (typically `src/main.tsx`):

<Tabs>
  <Tab value="add" label="Add styles import" default>

```tsx
```

  </Tab>
  <Tab value="context" label="In context">

```tsx

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)
```

  </Tab>
</Tabs>

</Step>

<Step>

### Render your first component

> This example replaces `App.tsx` with a single mining metric tile that toggles between a normal and an alarmed state.

#### 4.1 Replace `App.tsx` with the demo

This pulls a button from `@tetherto/mdk-react-devkit/core` and a domain component (`SingleStatCard`) from `@tetherto/mdk-react-devkit/foundation`:

```tsx

function App() {
  const [overheating, setOverheating] = useState(false)

  return (
    <div style={{ padding: '2rem', display: 'grid', gap: '1rem', maxWidth: 320 }}>
      <h1>Miner status</h1>
      <SingleStatCard
        name="Inlet temperature"
        value={overheating ? 78 : 28}
        unit="°C"
        variant={overheating ? 'tertiary' : 'primary'}
        color="red"
        flash={overheating}
      />
      <Button onClick={() => setOverheating((on) => !on)}>
        {overheating ? 'Cool down' : 'Simulate overheat'}
      </Button>
    </div>
  )
}

```

#### 4.2 Run your app

From the workspace root mdk/ui, start the dev server for your app. The `-w` name must match the `name` 
field in `apps/my-app/package.json` (`my-app` from step 2.1):

```bash
npm -w my-app run dev
```

Alternatively, from the app folder:

```bash
cd apps/my-app
npm run dev
```

You should see a card showing `Inlet temperature 28 °C` with a **Simulate overheat** button below it. 
Click the button and the card flips into an alarm state: red border, red text, value jumps to `78 °C`, 
and the whole card pulses. Click **Cool down** to reset.

That is `@tetherto/mdk-react-devkit/core` (the `Button`) and `@tetherto/mdk-react-devkit/foundation` (the `SingleStatCard`) working 
together: generic primitives plus mining-domain components, in one app.

</Step>

</Steps>

## Next steps

- To browse the full kit in one app first, see [Explore the demo](/v0-4-0/ui/react/explore-the-demo)
- [Wire a React app](/v0-4-0/tutorials/ui/react/tutorial): add `<MdkProvider>` and adapter hooks for connected foundation components
- [Quickstart](/v0-4-0/ui/react/quickstart): minimum integration reference (install, provider, stores, theming)
- [Core](/v0-4-0/ui/react/core): primitives reference (`@tetherto/mdk-react-devkit/core`)
- [Foundation](/v0-4-0/ui/react/foundation): mining-domain components (`@tetherto/mdk-react-devkit/foundation`)

# Wire a React app (/v0-4-0/tutorials/ui/react/tutorial)

This tutorial walks you through the full React stack: three workspace packages, `<MdkProvider>`, and adapter hooks.

<Callout type="info">
For a faster path that renders one presentational component without the provider, see [Your first component](/v0-4-0/tutorials/ui/react/first-component). 
</Callout>

## Prerequisites

- **Node.js** >=24
- **npm** >=11
- **React** 19+ and **react-dom** 19+

{/* npm packages are not yet published to the registry; clone the MDK UI monorepo and use workspace dependencies until publish. */}

<Steps>

<Step>

### Clone and build

<Callout type="info">
The `mdk/ui` directory is the npm workspace root. Clone the repository, install at the workspace root, 
and build all packages before adding an app or running the demo.
</Callout>

#### 1.1 Clone the repository

<Tabs>
  <Tab value="https" label="HTTPS" default>

```bash
git clone https://github.com/tetherto/mdk.git
cd mdk/ui
```

  </Tab>
  <Tab value="ssh" label="SSH">

```bash
# Requires an SSH key configured for your GitHub account
git clone git@github.com:tetherto/mdk.git
cd mdk/ui
```

  </Tab>
</Tabs>

#### 1.2 Install and build

```bash
npm install
npm run build
```

This builds `@tetherto/mdk-react-devkit` and the other packages in the `ui/` workspace.

</Step>

<Step>

### Create your app

#### 2.1 Scaffold a Vite React app

Create a new React app in the `apps/` folder:

```bash
cd apps
npm create vite@latest my-app -- --template react-ts
cd my-app
```

<Callout type="info">
Ignore the CLI "Now run" instructions — follow the MDK-specific steps below.
</Callout>

#### 2.2 Add MDK to `package.json`

Open `package.json` and add all three workspace packages:

<Tabs>
  <Tab value="add" label="Add dependencies" default>

```jsonc
    "@tetherto/mdk-react-devkit": "*",
    "@tetherto/mdk-react-adapter": "*",
    "@tetherto/mdk-ui-core": "*",
```

  </Tab>
  <Tab value="context" label="In context">

```jsonc
{
  "name": "my-app",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "@tetherto/mdk-react-devkit": "*", // [!code ++]
    "@tetherto/mdk-react-adapter": "*", // [!code ++]
    "@tetherto/mdk-ui-core": "*", // [!code ++]
    "react": "^19.2.6", // [!code highlight]
    "react-dom": "^19.2.6" // [!code highlight]
  },
}
```
<Callout type="info">
Vite also adds a `devDependencies` block (`eslint`, `typescript`, `vite`, `@types/*`); leave that block as is.
</Callout>

  </Tab>
</Tabs>

#### 2.3 Install from the workspace root

```bash
cd ../..    # from apps/my-app back up to the workspace root mdk/ui
npm install
```

</Step>

<Step>

### Wrap your app in MdkProvider

`<MdkProvider>` wires the headless stores from [`@tetherto/mdk-ui-core`](/v0-4-0/reference/app-toolkit/ui-core) into React and sets up the TanStack `QueryClient`. It is required for adapter hooks and connected foundation components. See also [Quickstart — Wrap your app in MdkProvider](/v0-4-0/ui/react/quickstart#wrap-your-app-in-mdkprovider).

#### 3.1 Update `main.tsx`

<Tabs>
  <Tab value="add" label="Add provider" default>

```tsx
```

Wrap `<App />`:

```tsx
<MdkProvider apiBaseUrl="https://app-node.example.com">
  <App />
</MdkProvider>
```

  </Tab>
  <Tab value="context" label="In context">

```tsx

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <MdkProvider apiBaseUrl="https://app-node.example.com"> {/* [!code ++] */}
      <App />
    </MdkProvider> {/* [!code ++] */}
  </StrictMode>,
)
```

  </Tab>
</Tabs>

</Step>

<Step>

### Use adapter hooks and render a component

Adapter hooks subscribe to Zustand stores and re-render when the selected slice changes. This example uses `useAuth` and `useDevices` from the adapter, then renders a presentational `SingleStatCard` from foundation.

#### 4.1 Replace `App.tsx`

```tsx

function App() {
  const { permissions } = useAuth()
  const { selectedDevices } = useDevices()

  return (
    <div style={{ padding: '2rem', display: 'grid', gap: '1rem', maxWidth: 360 }}>
      <h1>Operator dashboard</h1>
      <p>Selected devices: {selectedDevices?.length ?? 0}</p>
      <p>Permissions loaded: {permissions ? 'yes' : 'no'}</p>
      <SingleStatCard
        name="Inlet temperature"
        value={28}
        unit="°C"
        variant="primary"
      />
      <Button onClick={() => console.log('Ready for connected components')}>
        Log readiness
      </Button>
    </div>
  )
}

```

<Callout type="info">
Connected foundation components (device explorers, pool manager, settings dashboards) expect `<MdkProvider>` and often read multiple stores. Start with presentational imports while you wire your API; swap in connected components as your backend comes online.
</Callout>

</Step>

<Step>

### Run your app

From the workspace root `mdk/ui`:

```bash
npm -w my-app run dev
```

Or from the app folder:

```bash
cd apps/my-app
npm run dev
```

You should see the operator dashboard with device and permission readouts plus a temperature card. The adapter hooks confirm `<MdkProvider>` is wired — without it, they would throw.

</Step>

</Steps>

## Next steps

- [Quickstart](/v0-4-0/ui/react/quickstart): install reference, store hooks, and theming
- [Core](/v0-4-0/ui/react/core): primitives reference (`@tetherto/mdk-react-devkit/core`)
- [Foundation](/v0-4-0/ui/react/foundation): mining-domain components (`@tetherto/mdk-react-devkit/foundation`)

### Run the demo app

If you are already at the workspace root `mdk/ui`:

```bash
npm run dev:catalog
```

If your shell is still in the app folder:

```bash
cd ../..    # apps/my-app → apps → workspace root mdk/ui
npm run dev:catalog
```

Open [http://localhost:5173/mdk](http://localhost:5173/mdk) to browse examples.

# UI Kit (/v0-4-0/ui)

## TL;Dr

<Callout type="info">
Today the published React path is complete; other frameworks are on the [roadmap](/v0-4-0/resources/roadmap) and will reuse the same headless core.
</Callout>

The MDK React UI Kit is a [three-layer stack](#how-the-layers-fit-together):

- [`@tetherto/mdk-ui-core`](/v0-4-0/reference/app-toolkit/ui-core): headless Zustand stores and TanStack Query client factory (framework-agnostic)
- [`@tetherto/mdk-react-adapter`](/v0-4-0/ui/react/quickstart#wrap-your-app-in-mdkprovider): `<MdkProvider>` and store hooks; binds the core into your UI runtime (React today)
- [`@tetherto/mdk-react-devkit`](/v0-4-0/ui/react/get-started): [`/core`](/v0-4-0/ui/react/core) primitives and [`/foundation`](/v0-4-0/ui/react/foundation) mining-domain components

## Framework quickstarts

<Cards>
  <Card
    icon={<Atom />}
    title={<span style={cardTitle}>React</span>}
    href="/v0-4-0/ui/react/get-started"
    description={
      <span style={cardProse}>
        Get started with the React MDK UI Kit
      </span>
    }
  />
  <Card
    icon="🚧"
    title={<span style={cardTitle}>Vue</span>}
    href="/v0-4-0/resources/roadmap"
    description={
      <>
        <span style={cardProse}>
          Reactive hooks for Vue via <code>@tetherto/mdk-vue</code>
        </span>
        <span style={cardCta}>Learn about the release schedule →</span>
      </>
    }
  />
  <Card
    icon="🚧"
    title={<span style={cardTitle}>Svelte</span>}
    href="/v0-4-0/resources/roadmap"
    description={
      <>
        <span style={cardProse}>
          Reactive hooks for Svelte via <code>@tetherto/mdk-svelte</code>
        </span>
        <span style={cardCta}>Learn about the release schedule →</span>
      </>
    }
  />
  <Card
    icon="🚧"
    title={<span style={cardTitle}>Web Components</span>}
    href="/v0-4-0/resources/roadmap"
    description={
      <>
        <span style={cardProse}>
          Framework-agnostic Web Components via <code>@tetherto/wc</code>
        </span>
        <span style={cardCta}>Learn about the release schedule →</span>
      </>
    }
  />
</Cards>

## How the layers fit together

The MDK React UI Kit and subsequent frameworks will share a similar architecture:

```mermaid
graph TB
    subgraph headless [Layer 1: Headless]
        uiCore([<b>mdk-ui-core</b><br/>Zustand stores · Query client factory<br/>Framework-agnostic])
    end

    subgraph adapter [Layer 2: Framework adapter]
        reactAdapter([<b>mdk-react-adapter</b><br/>MdkProvider · store hooks<br/>React today])
    end

    subgraph uiLib [Layer 3: Framework UI library]
        reactDevkit([<b>mdk-react-devkit</b><br/>/core + /foundation<br/>Primitives and mining-domain UI])
    end

    uiCore --> reactAdapter
    reactAdapter --> reactDevkit

    style headless fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A
    style adapter fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A
    style uiLib fill:#F7931A,stroke:#1A1A1A,color:#1A1A1A
```

## Next steps

- [Quickstart](/v0-4-0/ui/react/quickstart): integrate the three packages into your React app
- [Explore the demo](/v0-4-0/ui/react/explore-the-demo): run the demo app in your browser

# @tetherto/mdk-react-devkit (/v0-4-0/ui/react/core)

[The core entrypoint](/v0-4-0/resources/repositories) (`@tetherto/mdk-react-devkit/core`) provides the foundational **React** UI components, 
utilities, and theme system for the MDK React UI Kit. This is the `/core` subpath of the devkit package, not the 
framework-agnostic [`@tetherto/mdk-ui-core`](/v0-4-0/reference/app-toolkit/ui-core) headless package.

<Callout type="info">
Mining-domain components, hooks, and settings live on the [`/foundation`](/v0-4-0/ui/react/foundation) entrypoint. 
Both subpaths are exported from `@tetherto/mdk-react-devkit`.
</Callout>

## Prerequisites

- Complete the installation

<Accordions>
  <Accordion title="Show steps">
    ```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
```

  </Accordion>
</Accordions>

- Add the dependency to your app's `package.json`

<Tabs>
  <Tab value="workspace" label="Monorepo workspace" default>

```json
{
  "dependencies": {
    "@tetherto/mdk-react-devkit": "*",
    "@tetherto/mdk-react-adapter": "*",
    "@tetherto/mdk-ui-core": "*"
  }
}
```

  </Tab>
  <Tab value="npm" label="🚧 Registry install">

> **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-core
```

  </Tab>
</Tabs>

Run `npm install` from the `mdk/ui` workspace root after your app is under `apps/` so npm links workspace packages.

## What's included

### Components

Production-ready React components organized by category:

| Category | Description |
|----------|-------------|
| [Forms](/v0-4-0/ui/react/core/components/forms) | Input and form control components |
| [Overlays](/v0-4-0/ui/react/core/components/overlays) | Dialogs, popovers, tooltips, and toasts |
| [Data display](/v0-4-0/ui/react/core/components/data-display) | Cards, tables, tags, and data presentation |
| [Charts](/v0-4-0/ui/react/core/components/charts) | Data visualization components |
| [Navigation](/v0-4-0/ui/react/core/components/navigation) | Sidebar, tabs, and breadcrumbs |
| [Loading](/v0-4-0/ui/react/core/components/loading) | Spinners, loaders, and progress indicators |
| [Errors](/v0-4-0/ui/react/core/components/errors) | Error boundaries, error cards, and alerts |
| [Logs](/v0-4-0/ui/react/core/components/logs) | Log display components |

See the [Components reference](/v0-4-0/ui/react/core/components) for the full list with demo links.

### Icons

70+ purpose-built icons for Bitcoin mining applications:

- Navigation icons (Dashboard, Farms, Inventory, etc.)
- Status icons (Mining, Offline, Error, etc.)
- Weather icons (Sunny, Cloudy, Rainy, etc.)
- Alarm icons (Temperature, Pressure, Fluid, etc.)

### Utilities

Helper functions for common operations:

- [`formatNumber`](/v0-4-0/reference/app-toolkit/ui-kit/utilities#formatnumber), [`formatHashrate`](/v0-4-0/reference/app-toolkit/ui-kit/utilities#formathashrate), [`formatCurrency`](/v0-4-0/reference/app-toolkit/ui-kit/utilities#formatcurrency): Number formatting
- [`formatDate`](/v0-4-0/reference/app-toolkit/ui-kit/utilities#formatdate), [`formatRelativeTime`](/v0-4-0/reference/app-toolkit/ui-kit/utilities#formatrelativetime): Date formatting
- [`cn`](/v0-4-0/reference/app-toolkit/ui-kit/utilities#cn): Class name merging ([clsx](https://github.com/lukeed/clsx) wrapper)
- Conversion utilities for energy, hashrate, and units
- Validation utilities for email, URL, and empty checks

### Theme system

CSS custom properties and utilities for consistent styling:

- Color tokens (primary, gray scales)
- Spacing scale
- Border radius scale
- Font size scale
- Light/dark mode support

### Types

TypeScript types for type-safe development:

- [`ComponentSize`](/v0-4-0/reference/app-toolkit/ui-kit/types#componentsize), [`ButtonVariant`](/v0-4-0/reference/app-toolkit/ui-kit/types#buttonvariant), [`ColorVariant`](/v0-4-0/reference/app-toolkit/ui-kit/types#colorvariant)
- [`Status`](/v0-4-0/reference/app-toolkit/ui-kit/types#status), [`PaginationParams`](/v0-4-0/reference/app-toolkit/ui-kit/types#paginationparams), [`ApiResponse`](/v0-4-0/reference/app-toolkit/ui-kit/types#apiresponse)
- Chart types, table types, and utility types

## Reference

Detailed reference material lives in the unified [Reference section](/v0-4-0/reference). Core (`/core`) slices are:

- [Constants](/v0-4-0/reference/app-toolkit/ui-kit/constants#core-constants): colors, units, currency, chart configs
- [Types](/v0-4-0/reference/app-toolkit/ui-kit/types#core-types): UI primitives, API shapes, value-unit types
- [Utilities](/v0-4-0/reference/app-toolkit/ui-kit/utilities#core-utilities): formatting, validation, conversions, `cn` and friends

## Import examples

```tsx
// Import components

// Import utilities

// Import types

// Import icons

// Import styles (required for component styling)
```

# Components (/v0-4-0/ui/react/core/components)

The `@tetherto/mdk-react-devkit/core` package provides production-ready React components. All components are built with accessibility in mind and support both light and dark themes.

## Import

### Prerequisites

- Complete the [installation](/v0-4-0/ui/react/quickstart)

<Accordions>
  <Accordion title="Show steps">
    ```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
```

  </Accordion>
</Accordions>

- Import the core styles in your app's entry point:

```tsx
```
### Import named components

Declare the components you require:

```tsx
```

## Supported components

<ComponentTablesByCategory />

## Styling

Components use BEM-style CSS classes (e.g., `.mdk-button`, `.mdk-card__header`) for styling consistency and easy customization.

## Class name forwarding

Every component forwards `className` to its root element, so you can apply layout or override styles without ejecting. This is assumed throughout and not repeated in every props table. Components with multiple styleable parts also expose **named** hooks (for example `contentClassName`, `indicatorClassName`, `dropdownClassName`) — those *are* listed in the relevant component's props table.

## Component design principles

- **Composable**: Components are designed to work together
- **Accessible**: Built with ARIA attributes and keyboard navigation
- **Themeable**: Support light/dark modes via CSS custom properties
- **Type-safe**: Full TypeScript support with exported prop types

# Chart components (/v0-4-0/ui/react/core/components/charts)

Chart components for visualizing time-series data, metrics, and statistics. Built on [Chart.js](https://www.chartjs.org/) and 
[Lightweight Charts](https://tradingview.github.io/lightweight-charts/), supporting:

- Chart types: components that render datasets (`AreaChart`, `BarChart`, `DoughnutChart`, `GaugeChart`, `LineChart`)
- Domain chart wrappers: opinionated chart components with preset layouts and series config (`AverageDowntimeChart`, 
`ThresholdLineChart`, `OperationsEnergyCostChart`)
- [Composition elements](/v0-4-0/ui/react/core/components/charts/composition): layout chrome, legends, stats rows, and chart 
utilities (`ChartContainer`, `ChartStatsFooter`, `DetailLegend`, `MinMaxAvg`, helpers)

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/core#prerequisites)

## All chart type components

<ComponentTable category="Charts" subcategory={null} pkg="core" />

## `AreaChart`

<ComponentDescription name="AreaChart" />

Presentational Chart.js area chart (line series with fill). Data and options follow the same Chart.js model as 
[`LineChart`](#linechart), without a separate MDK data wrapper type.

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Required | `ChartData` | none | [Chart.js](https://www.chartjs.org/) line chart `data` (datasets typically use `fill: true` for an area look) |
| `options` | Optional | `ChartOptions` | none | [Chart.js](https://www.chartjs.org/) options (merged with defaults) |
| `tooltip` | Optional | `ChartTooltipConfig` | none | Custom HTML tooltip config; when set, replaces the default Chart.js tooltip |
| `height` | Optional | `number` | `300` | Chart height in pixels |
| `className` | Optional | `string` | none | Root class name from the host app |

### Basic usage

```tsx
const data = {
  labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
  datasets: [
    {
      label: 'Revenue',
      data: [100, 120, 115, 134, 168],
      fill: true,
      backgroundColor: 'rgba(114, 245, 158, 0.2)',
      borderColor: '#72F59E',
    },
  ],
}

<AreaChart data={data} />
```

## `BarChart`

<ComponentDescription name="BarChart" />

The `BarChart` component renders Chart.js bar or column series. It manages:

1. **Data** (`data`): labels and datasets passed to Chart.js.
2. **Chart options** (`options`): merged with MDK defaults.
3. **Formatting** (`formatYLabel`, `formatDataLabel`, `tooltip`): axis labels, data labels, and HTML tooltips.
4. **Presentation** (`isStacked`, `isHorizontal`, `showLegend`, `legendPosition`, `legendAlign`, `showDataLabels`): layout and legend.
5. **Size** (`height`): chart height in pixels.

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Required | `ChartData` | none | [Chart.js](https://www.chartjs.org/) data object |
| `options` | Optional | `ChartOptions` | none | [Chart.js](https://www.chartjs.org/) options (merged with defaults) |
| `formatYLabel` | Optional | `function` | none | Format Y-axis tick labels |
| `formatDataLabel` | Optional | `function` | none | Format data label values |
| `tooltip` | Optional | `ChartTooltipConfig` | none | Custom HTML tooltip config |
| `isStacked` | Optional | `boolean` | `false` | Stack bars on top of each other |
| `isHorizontal` | Optional | `boolean` | `false` | Render bars horizontally |
| `showLegend` | Optional | `boolean` | `true` | Show [Chart.js](https://www.chartjs.org/) legend |
| `legendPosition` | Optional | `'top' \| 'right' \| 'bottom' \| 'left'` | `'top'` | Legend position |
| `legendAlign` | Optional | `'start' \| 'center' \| 'end'` | `'start'` | Legend alignment |
| `showDataLabels` | Optional | `boolean` | `false` | Show values above bars |
| `height` | Optional | `number` | `300` | Chart height in pixels |

### Basic usage

```tsx
const data = {
  labels: ['Jan', 'Feb', 'Mar', 'Apr'],
  datasets: [
    {
      label: 'Hashrate (TH/s)',
      data: [120, 150, 180, 200],
      backgroundColor: '#72F59E',
    },
  ],
}

<BarChart data={data} height={300} />
```

### Data from hooks

Hand-built Chart.js `data` (above) is valid. When app hooks return `{ labels, series }` declarative input, convert with 
[`buildBarChartData`](/v0-4-0/ui/react/core/components/charts/composition#hook-shaped-bar-data) in 
[chart utilities](/v0-4-0/ui/react/core/components/charts/composition#chart-utilities), 
optionally merge per-series `datalabels`, then pass the result to `data`.

### Stacked bar chart

```tsx
const data = {
  labels: ['Site A', 'Site B', 'Site C'],
  datasets: [
    { label: 'Online', data: [100, 80, 120], backgroundColor: '#72F59E' },
    { label: 'Offline', data: [10, 20, 5], backgroundColor: '#FF6B6B' },
  ],
}

<BarChart data={data} isStacked />
```

### Horizontal bar chart

```tsx
<BarChart
  data={data}
  isHorizontal
  formatYLabel={(value) => `${value} TH/s`}
/>
```

### With data labels

```tsx
<BarChart
  data={data}
  showDataLabels
  formatDataLabel={(value) => `${value.toFixed(1)}%`}
/>
```

## `DoughnutChart`

<ComponentDescription name="DoughnutChart" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Required | `DoughnutChartDataset[]` | none | Array of `{ label, value, color? }` slices (see [data shape](#data-shape-for-doughnut-charts)) |
| `unit` | Optional | `string` | `''` | Unit suffix shown in the default tooltip |
| `options` | Optional | `ChartOptions` | none | [Chart.js](https://www.chartjs.org/) doughnut options (merged with defaults) |
| `cutout` | Optional | `string` | `'75%'` | Inner radius cutout (doughnut ring thickness) |
| `borderWidth` | Optional | `number` | `4` | Border width between segments |
| `height` | Optional | `number` | `260` | Chart height in pixels |
| `legendPosition` | Optional | `string` | `'top'` | Where to place the custom legend relative to the chart |
| `tooltip` | Optional | `ChartTooltipConfig` | none | Custom HTML tooltip; when set, replaces the default doughnut tooltip |
| `formatValue` | Optional | `(value: number) => string` | none | Formats slice values in the built-in legend and default tooltip (default: raw number) |
| `className` | Optional | `string` | none | Root class name from the host app |

#### Data shape for doughnut charts

Pass `data` as an array of `{ label, value, color? }`. The chart maps this into Chart.js internally 
(`labels`, single dataset, segment colors). Omit `color` to use the default palette rotation.

### Basic usage

```tsx
const data = [
  { label: 'Online', value: 85, color: '#72F59E' },
  { label: 'Offline', value: 10, color: '#FF6B6B' },
  { label: 'Maintenance', value: 5, color: '#FFD700' },
]

<DoughnutChart data={data} />
```

## `GaugeChart`

Speedometer-style presentational arc gauge for displaying a single normalized value, providing:

1. **Percent** (`percent`): fill level from `0` to `1` (values outside that range are clamped).
2. **Arc appearance** (`colors`, `arcWidth`, `nrOfLevels`): segment colors, relative thickness, and segment count.
3. **Center label** (`hideText`): percentage text in the center of the gauge (hidden when `hideText` is `true`).
4. **Accessibility** (`id`): stable id wired into SVG labels (defaults to `mdk-gauge-chart`).
5. **Layout** (`height`, `maxWidth`, `className`): container height, max width, and host root class.

<ComponentDescription name="GaugeChart" />

<Callout type="info">
  The `GaugeChart` is SVG-based; strokes and labels may paint past the host element’s layout bounds. For a hard edge, wrap the 
  gauge in an element with **`overflow: hidden`**, or add vertical spacing. 
  [`ChartContainer`](/v0-4-0/ui/react/core/components/charts/composition#chartcontainer) does **not** set overflow clipping on the 
  chart slot—it is for title, loading/empty, and footer chrome like other charts (see 
  [Composition](/v0-4-0/ui/react/core/components/charts/composition)); it may add spacing that makes 
  overlap less visible, but it is not a substitute for clipping when you need it.
</Callout>

### Import

```tsx
```

The gauge is driven by a **normalized** `percent` in the range **0–1** (for example `0.75` is 75%). It is not 
a `value` / `max` API, and it does not accept `label` or `unit` props (see 
[`ChartContainer`](/v0-4-0/ui/react/core/components/charts/composition#chartcontainer) 
for layout notes).

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `percent` | Required | `number` | none | Fill level from `0` to `1` (clamped) |
| `colors` | Optional | `string[]` | green → soft green → red (theme) | Arc segment colors as HEX strings |
| `arcWidth` | Optional | `number` | `0.2` | Arc thickness as a fraction of the gauge radius (`0`–`1`) |
| `nrOfLevels` | Optional | `number` | `3` | Number of colored segments on the arc |
| `hideText` | Optional | `boolean` | `false` | Hide the percentage text in the center |
| `id` | Optional | `string` | `'mdk-gauge-chart'` | Stable id for SVG accessibility labels |
| `height` | Optional | `number` \| `string` | `200` | Container height (pixels or CSS length, e.g. `'50%'`) |
| `maxWidth` | Optional | `number` | `500` | Maximum width in pixels |
| `className` | Optional | `string` | none | Root class name from the host app |

### Basic usage

```tsx
<GaugeChart percent={0.75} />
```

### In a chart card (same shell as other charts)

[`ChartContainer`](/v0-4-0/ui/react/core/components/charts/composition#chartcontainer) adds title, loading, 
and empty chrome; it does **not** apply `overflow: hidden` to the chart body. 
When you still see bleed next to siblings or footers, wrap `GaugeChart` in an extra element with **`overflow: hidden`**.

```tsx

<ChartContainer title="Utilization" loading={false} empty={false}>
  <div style={{ overflow: 'hidden' }}>
    <GaugeChart percent={0.75} />
  </div>
</ChartContainer>
```

### Custom colors and arc

```tsx
<GaugeChart
  percent={0.6}
  colors={['#72F59E', '#FFD700', '#FF6B6B']}
  arcWidth={0.25}
  nrOfLevels={3}
  hideText={false}
  height={220}
/>
```

## `LineChart`

<ComponentDescription name="LineChart" />

The `LineChart` component renders time-series lines. It manages:

1. **Series data** (`data`): `LineChartData` with millisecond `x` values (see [data shape](#data-shape-for-line-charts)).
2. **Context** (`timeline`, `unit`): timeline identifier and unit label for tooltips.
3. **Formatting** (`yTicksFormatter`, `priceFormatter`, `customLabel`): tick and tooltip text.
4. **Presentation** (`backgroundColor`, `beginAtZero`, `showPointMarkers`, `showDateInTooltip`, `uniformDistribution`): axis and marker behavior.
5. **Size** (`height`): chart height in pixels.

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Required | `LineChartData` | none | Object with a `datasets` array (see [data shape](#data-shape-for-line-charts)) |
| `timeline` | Optional | `string` | none | Timeline identifier |
| `yTicksFormatter` | Optional | `function` | none | Format Y-axis ticks |
| `priceFormatter` | Optional | `function` | none | Format tooltip values |
| `customLabel` | Optional | `string` | none | Custom tooltip label |
| `unit` | Optional | `string` | `''` | Unit label for values |
| `backgroundColor` | Optional | `string` | none | Chart background color |
| `beginAtZero` | Optional | `boolean` | `false` | Start Y-axis at zero |
| `showPointMarkers` | Optional | `boolean` | `false` | Show data point markers |
| `showDateInTooltip` | Optional | `boolean` | `false` | Show date in tooltip |
| `uniformDistribution` | Optional | `boolean` | `false` | Uniform time distribution |
| `height` | Optional | `number` | `240` | Chart height in pixels |

#### Data shape for line charts

`LineChartData` is `{ datasets: LineDataset[] }`. Each `LineDataset` has `label`, `borderColor`, optional 
`visible` / `borderWidth` / `extraTooltipData`, and `data: { x, y }[]` where `x` is a time value in **milliseconds** 
(for example from `Date.prototype.valueOf()`) and `y` is the series value (number, or `null` / `undefined` for gaps).

### Basic usage

```tsx
const data = {
  datasets: [
    {
      label: 'Hashrate',
      borderColor: '#72F59E',
      data: [
        { x: 1704067200000, y: 150 },
        { x: 1704153600000, y: 155 },
        { x: 1704240000000, y: 160 },
      ],
    },
  ],
}

<LineChart data={data} height={300} unit="TH/s" />
```

### Multiple lines

```tsx
const hashrateData = [
  { x: 1704067200000, y: 150 },
  { x: 1704153600000, y: 155 },
]
const targetData = [
  { x: 1704067200000, y: 140 },
  { x: 1704153600000, y: 145 },
]

const data = {
  datasets: [
    {
      label: 'Hashrate',
      borderColor: '#72F59E',
      data: hashrateData,
    },
    {
      label: 'Target',
      borderColor: '#FFD700',
      data: targetData,
    },
  ],
}

<LineChart data={data} showPointMarkers beginAtZero />
```

## `AverageDowntimeChart`

<ComponentDescription name="AverageDowntimeChart" />

Stacked bar chart showing monthly average downtime broken into **Curtailment** and **Op. Issues** series. Wraps 
[`ChartContainer`](/v0-4-0/ui/react/core/components/charts/composition#chartcontainer) and [`BarChart`](#barchart). It manages:

1. **Data** (`data`): period labels and per-series rate arrays (values are fractions 0–1).
2. **Formatting** (`yTicksFormatter`): formats Y-axis ticks, tooltips, and optional bar data labels.
3. **Presentation** (`showDataLabels`, `title`, `unit`): bar data labels and header text.
4. **Layout** (`height`, `barWidth`): chart height and bar width in pixels.
5. **State** (`isLoading`, `emptyMessage`): loading overlay and empty-state copy.

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Optional | `AverageDowntimeChartData` | none | Period labels and downtime rate arrays (see [data shape](#data-shape-for-averagedowntimechart)) |
| `yTicksFormatter` | Optional | `function` | `defaultAverageDowntimeRateFormatter` | Formats Y-axis ticks, tooltips, and data labels; input values are 0–1 rates |
| `showDataLabels` | Optional | `boolean` | `false` | Show formatted rate values above each bar segment |
| `title` | Optional | `string` | `'Monthly Average Downtime'` | Header title |
| `unit` | Optional | `string` | `'%'` | Unit label shown beneath the title |
| `height` | Optional | `number` | `280` | Chart height in pixels |
| `barWidth` | Optional | `number` | `38` | Bar width in pixels |
| `isLoading` | Optional | `boolean` | `false` | When `true`, shows a loading overlay |
| `emptyMessage` | Optional | `string` | `'No data available'` | Copy shown in the empty state |
| `className` | Optional | `string` | none | Root class name from the host app |

#### Data shape for `AverageDowntimeChart`

Pass `data` as an `AverageDowntimeChartData` object:

| Field | Type | Description |
|-------|------|-------------|
| `labels` | `string[]` | Period labels (e.g. month names) |
| `curtailment` | `number[]` | Curtailment downtime rates (0–1) per period |
| `operationalIssues` | `number[]` | Operational issue downtime rates (0–1) per period |

Both series arrays are optional; absent series are omitted from the chart.

### Basic usage

```tsx

<AverageDowntimeChart
  data={{
    labels: ['Jan', 'Feb', 'Mar'],
    curtailment: [0.02, 0.01, 0.03],
    operationalIssues: [0.05, 0.04, 0.06],
  }}
/>
```

### Custom formatter

`yTicksFormatter` receives raw 0–1 rate values and must return a display string. The default, `defaultAverageDowntimeRateFormatter`, 
multiplies by 100 and appends `%`:

```tsx
  AverageDowntimeChart,
  defaultAverageDowntimeRateFormatter,
} from '@tetherto/mdk-react-devkit/core'

// Override: show one decimal place
<AverageDowntimeChart
  data={data}
  yTicksFormatter={(rate) => `${(rate * 100).toFixed(1)}%`}
  showDataLabels
/>
```

### Exported helpers

| Export | Role |
|--------|------|
| `buildAverageDowntimeBarChartData` | Converts `AverageDowntimeChartData` into Chart.js `data` |
| `buildAverageDowntimeTooltip` | Builds the HTML tooltip config for the stacked bars |
| `defaultAverageDowntimeRateFormatter` | Default Y-axis / label formatter (rate × 100, `%` suffix) |
| `hasAverageDowntimeData` | Returns `true` when `data` has at least one non-empty series |

## `ThresholdLineChart`

<ComponentDescription name="ThresholdLineChart" />

Time-series line chart with optional horizontal threshold bands. Wraps 
[`ChartContainer`](/v0-4-0/ui/react/core/components/charts/composition#chartcontainer) and [`LineChart`](#linechart). It manages:

1. **Data** (`data`): one or more line series plus optional threshold lines.
2. **Formatting** (`yTicksFormatter`): formats Y-axis ticks.
3. **Presentation** (`title`, `unit`, `isTall`, `isLegendVisible`): header, height variant, and legend toggle.
4. **Layout** (`height`): explicit chart height in pixels (overrides `isTall`).
5. **State** (`emptyMessage`): empty-state copy.

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Optional | `ThresholdLineChartData` | none | Series and optional thresholds (see [data shape](#data-shape-for-thresholdlinechart)) |
| `yTicksFormatter` | Optional | `function` | unit-aware default | Formats Y-axis tick labels; defaults to `"${value} ${unit}"` when `unit` is set, otherwise `String(value)` |
| `title` | Optional | `string` | none | Header title; rendered as `"${title} (${unit})"` when both are set |
| `unit` | Optional | `string` | none | Unit appended to the title and used by the default tick formatter |
| `isTall` | Optional | `boolean` | `false` | When `true`, uses a taller default height of `360px` instead of `280px` |
| `isLegendVisible` | Optional | `boolean` | `true` | Show the interactive legend with dataset visibility toggles |
| `height` | Optional | `number` | `280` (`360` when `isTall`) | Chart height in pixels; explicit value overrides `isTall` |
| `emptyMessage` | Optional | `string` | `'No data available'` | Copy shown in the empty state |
| `className` | Optional | `string` | none | Root class name from the host app |

#### Data shape for `ThresholdLineChart`

`ThresholdLineChartData` holds series and optional threshold lines:

| Field | Type | Description |
|-------|------|-------------|
| `series` | `ThresholdLineChartSeries[]` | Line series to render |
| `thresholds` | `ThresholdLineChartThreshold[]` | Optional horizontal reference lines |

Each `ThresholdLineChartSeries`:

| Field | Type | Description |
|-------|------|-------------|
| `label` | `string` | Series label shown in the legend |
| `points` | `ThresholdLineChartPoint[]` | Data points (`{ timestamp: string \| number, value: number }`) |
| `color` | `string` | Optional line color |
| `fill` | `boolean` | Optional area fill below the line |

Each `ThresholdLineChartThreshold`:

| Field | Type | Description |
|-------|------|-------------|
| `label` | `string` | Label shown in the legend |
| `value` | `number` | Y-axis value at which the horizontal line is drawn |
| `color` | `string` | Optional line color |

### Basic usage

```tsx

<ThresholdLineChart
  title="Power Consumption"
  unit="MW"
  data={{
    series: [
      {
        label: 'Actual',
        color: '#72F59E',
        points: [
          { timestamp: '2025-01-01', value: 32 },
          { timestamp: '2025-01-02', value: 35 },
          { timestamp: '2025-01-03', value: 29 },
        ],
      },
    ],
    thresholds: [
      { label: 'Capacity limit', value: 38, color: '#FF6B6B' },
    ],
  }}
/>
```

### Tall variant

```tsx
<ThresholdLineChart
  title="Hashrate"
  unit="TH/s"
  isTall
  data={data}
/>
```

### Exported helpers

| Export | Role |
|--------|------|
| `toThresholdLineChartData` | Converts `ThresholdLineChartData` into `LineChartData` for the underlying `LineChart` |
| `hasNonZeroData` | Returns `true` when `data` contains at least one series with a non-zero value |

## `OperationsEnergyCostChart`

<ComponentDescription name="OperationsEnergyCostChart" />

Doughnut chart comparing operational costs against energy costs in USD per MWh. Wraps 
[`ChartContainer`](/v0-4-0/ui/react/core/components/charts/composition#chartcontainer) and [`DoughnutChart`](#doughnutchart). It manages:

1. **Data** (`data`): operational and energy cost values in USD.
2. **Presentation** (`title`, `unit`): header text and unit label.
3. **Layout** (`height`): chart height in pixels.
4. **State** (`isLoading`, `emptyMessage`): loading overlay and empty-state copy.

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Optional | `OperationsEnergyCostChartData` | none | Cost values in USD (see [data shape](#data-shape-for-operationsenergycostchart)) |
| `title` | Optional | `string` | `'Operations vs Energy Cost'` | Header title |
| `unit` | Optional | `string` | `'$/MWh'` | Unit label shown beneath the title |
| `height` | Optional | `number` | `200` | Chart height in pixels |
| `isLoading` | Optional | `boolean` | `false` | When `true`, shows a loading overlay |
| `emptyMessage` | Optional | `string` | `'No data available'` | Copy shown in the empty state |
| `className` | Optional | `string` | none | Root class name from the host app |

#### Data shape for `OperationsEnergyCostChart`

`OperationsEnergyCostChartData` has two optional numeric fields:

| Field | Type | Description |
|-------|------|-------------|
| `operationalCostsUSD` | `number` | Operational costs in USD |
| `energyCostsUSD` | `number` | Energy costs in USD |

Both fields are optional. Slices with a zero or absent value are omitted from the doughnut.

### Basic usage

```tsx

<OperationsEnergyCostChart
  data={{
    operationalCostsUSD: 1200,
    energyCostsUSD: 4800,
  }}
/>
```

### Exported helpers

| Export | Role |
|--------|------|
| `buildOperationsEnergyCostSlices` | Converts `OperationsEnergyCostChartData` into `DoughnutChartDataset[]` slices, omitting zero-value entries |
| `buildOperationsEnergyCostTooltip` | Builds the HTML tooltip config with two-decimal formatting |
| `hasOperationsEnergyCostData` | Returns `true` when at least one cost field is a positive number |

# Chart composition (/v0-4-0/ui/react/core/components/charts/composition)

Compose chart types from [Chart components](/v0-4-0/ui/react/core/components) with wrappers and helpers. `ChartContainer` adds title, loading, and empty states; `ChartStatsFooter` and `DetailLegend` add summary rows and rich legends (for example [`LineChartCard`](/v0-4-0/ui/react/foundation/dashboard/charts#linechartcard)).

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/core#prerequisites)

## Components

<ComponentTable category="Charts" subcategory="Composition" pkg="core" />

## `ChartContainer`

<ComponentDescription name="ChartContainer" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `children` | Required | `ReactNode` | none | Chart body (for example a `BarChart` or `LineChart`) |
| `title` | Optional | `string` | none | Title text |
| `titleExtra` | Optional | `ReactNode` | none | Node rendered immediately after the title text (e.g. an info tooltip); only shown when `title` is set and `header` is not |
| `header` | Optional | `ReactNode` | none | Custom header node |
| `headerAction` | Optional | `ReactNode` | none | Action rendered on the right side of the header row (e.g. an expand toggle); sits alongside the range selector when both are present |
| `legendData` | Optional | `LegendItem[]` | none | Legend entries rendered in the container chrome |
| `highlightedValue` | Optional | `object` | none | Highlighted metric (`value`, `unit`, `className`, `style`); highlight chrome renders only when this prop is set and the chart body is visible (not `loading` / not `empty`) |
| `rangeSelector` | Optional | `object` | none | Range selector props (`options`, `value`, `onChange`, `className`, `style`, `buttonClassName`) |
| `loading` | Optional | `boolean` | none | When `true`, shows a centered `Loader` overlay over the chart area |
| `empty` | Optional | `boolean` | none | When `true`, shows empty state |
| `emptyMessage` | Optional | `string` | `'No data available'` | Copy shown in the empty overlay when `empty` is `true` and `loading` is not `true` |
| `minMaxAvg` | Optional | `object` | none | Min / max / avg strings for the summary row |
| `timeRange` | Optional | `string` | none | Time range label |
| `footer` | Optional | `ReactNode` | none | Footer slot |
| `footerClassName` | Optional | `string` | none | Class name on the footer wrapper |
| `onToggleDataset` | Optional | `function` | none | Called with dataset index when legend toggles visibility |
| `className` | Optional | `string` | none | Root class name from the host app |

### Basic usage

```tsx
<ChartContainer loading={isLoading} empty={data.length === 0}>
  <BarChart data={data} />
</ChartContainer>
```

## `ChartStatsFooter`

<ComponentDescription name="ChartStatsFooter" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `minMaxAvg` | Optional | `object` | none | Min, max, and average strings shown in the primary row when provided |
| `stats` | Optional | `ChartStatsFooterItem[]` | none | Additional stat rows (`label`, `value`) in a columnar grid |
| `statsPerColumn` | Optional | `number` | `1` | Number of stat items per column when `stats` is set |
| `secondaryLabel` | Optional | `object` | none | Secondary block with `title` and `value` when provided |
| `className` | Optional | `string` | none | Root class name from the host app |

The component renders **nothing** when `minMaxAvg`, `stats`, and `secondaryLabel` are all absent or empty.

### Basic usage

```tsx
<ChartStatsFooter
  stats={[
    { label: 'Min', value: '120 TH/s' },
    { label: 'Max', value: '180 TH/s' },
    { label: 'Avg', value: '150 TH/s' },
  ]}
/>
```

## `DetailLegend`

<ComponentDescription name="DetailLegend" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `items` | Required | `DetailLegendItem[]` | none | Legend rows (`label`, `color`, optional `icon`, `currentValue`, `percentChange`, `hidden`) |
| `onToggle` | Optional | `function` | none | Called with `label` and index when a row is toggled |
| `className` | Optional | `string` | none | Root class name from the host app |

### Basic usage

```tsx
<DetailLegend
  items={[
    { label: 'Online', color: '#72F59E', currentValue: { value: 85, unit: '%' } },
    { label: 'Offline', color: '#FF6B6B', currentValue: { value: 15, unit: '%' } },
  ]}
/>
```

## `MinMaxAvg`

<ComponentDescription name="MinMaxAvg" />

Compact summary row displaying min, max, and average values with consistent MDK label and value styling. Rendered automatically by [`ChartContainer`](#chartcontainer) when its `minMaxAvg` prop is set, and by [`ChartStatsFooter`](#chartstatsfooter) in the same way. Use it directly when you need the summary row outside of a `ChartContainer` or `ChartStatsFooter`.

The component renders **nothing** when all three values are absent or empty strings.

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `min` | Optional | `string` | none | Minimum value label (for example `'65 TH/s'`) |
| `max` | Optional | `string` | none | Maximum value label |
| `avg` | Optional | `string` | none | Average value label |
| `className` | Optional | `string` | none | Root class name from the host app |

### `MinMaxAvgValues` type

```tsx
type MinMaxAvgValues = Partial<{
  min: string
  max: string
  avg: string
}>
```

`MinMaxAvgValues` is the shape accepted by the `minMaxAvg` prop on both `ChartContainer` and `ChartStatsFooter`.

### Basic usage

```tsx

<MinMaxAvg min="65 TH/s" max="80 TH/s" avg="72.6 TH/s" />
```

### Usage with `ChartContainer`

```tsx

const stats = computeStats(values)

<ChartContainer
  title="Hashrate"
  minMaxAvg={{
    min: `${stats.min} TH/s`,
    max: `${stats.max} TH/s`,
    avg: `${stats.avg} TH/s`,
  }}
>
  <LineChart data={chartData} />
</ChartContainer>
```

## Chart utilities

Pure functions for building Chart.js data and options. Import from the package root alongside components.

```tsx
  defaultChartOptions,
  defaultChartColors,
  buildBarChartData,
  buildBarChartOptions,
  buildChartTooltip,
  computeStats,
} from '@tetherto/mdk-react-devkit/core'
```

| Export | Role |
|--------|------|
| `defaultChartOptions` | Shared Chart.js defaults used by MDK chart components |
| `defaultChartColors` | Default dataset color palette |
| `buildBarChartData` | Map MDK bar input into Chart.js `data` |
| `buildBarChartOptions` | Build bar chart options (stacking, axes, formatters) |
| `buildChartTooltip` | HTML tooltip config for Chart.js |
| `computeStats` | Min, max, and average for a numeric array |

Types `BarChartInput`, `BarChartSeries`, `BarChartLine`, and `BarChartConstant` are exported from the same package for hook-shaped bar data.

### Hook-shaped bar data

App and reporting hooks often return declarative bar input instead of Chart.js `data`. `buildBarChartData` converts that shape 
into `{ labels, datasets }` for [`BarChart`](/v0-4-0/ui/react/core/components/charts#barchart). The pipeline:

1. **Input** (`BarChartInput`): optional `labels`, required `series`, optional `lines` and `constants` for mixed bar/line overlays.
2. **Build** (`buildBarChartData`): returns Chart.js `data` with MDK gradient styling and layout defaults (`barWidth`, `categoryPercentage`, `barPercentage` are 
optional on the input).
3. **Data labels** (optional): per-series overrides on the input (`formatter`, `anchor`, `align`, `offset`, `font`, `padding`, `display`, `clamp`, `clip`) map 
to each built dataset’s Chart.js `datalabels` by series index.
4. **Render** (`<BarChart data={...} />`): pass the built object to the component; pair with `buildBarChartOptions` when you need stacking, axes, or formatters.

`BarChartInput` shape

| Field | Status | Type | Description |
|-------|--------|------|-------------|
| `series` | Required | `BarChartSeries[]` | Bar datasets (`label`, `values`, optional `color`, `stack`, `gradient`, bar layout props) |
| `labels` | Optional | `string[]` | Category labels; omitted labels are derived from series `values` keys or indices |
| `lines` | Optional | `BarChartLine[]` | Line overlays on the same chart |
| `constants` | Optional | `BarChartConstant[]` | Horizontal reference lines |

Each `BarChartSeries` uses `values` as either `number[]` (positional) or `Record<string, number>` (keyed by category label).

#### Example

Hook output to `BarChart` example:

```tsx
  BarChart,
  buildBarChartData,
  ChartContainer,
} from '@tetherto/mdk-react-devkit/core'

// Typical shape returned by app/reporting data hooks
const hookOutput = {
  labels: ['Q1', 'Q2', 'Q3'],
  series: [
    {
      label: 'Revenue',
      values: [4.2, 3.8, 5.1],
      color: '#72F59E',
      dataLabels: {
        formatter: (v: number) => `${v.toFixed(1)}M`,
        anchor: 'end',
        align: 'top',
      },
    },
    {
      label: 'OpEx',
      values: [1.8, 2.0, 1.6],
      color: '#FFD700',
    },
  ],
}

const cleanSeries = hookOutput.series.map(({ dataLabels: _dl, ...s }) => s)
const base = buildBarChartData({ labels: hookOutput.labels, series: cleanSeries })

const chartData = {
  labels: base.labels,
  datasets: base.datasets.map((dataset, i) => {
    const overrides = hookOutput.series[i]?.dataLabels
    return overrides ? { ...dataset, datalabels: overrides } : dataset
  }),
}

const isEmpty =
  !hookOutput.series.length ||
  hookOutput.series.every((s) => {
    const vals = Array.isArray(s.values) ? s.values : Object.values(s.values)
    return !vals.length || vals.every((v) => v === 0)
  })

<ChartContainer title="Quarterly" empty={isEmpty}>
  <BarChart data={chartData} />
</ChartContainer>
```

#### Empty and all-zero series

Treat bar data as empty when any of the following is true:

- `series` is missing or has length `0`
- Every series has empty `values` (array or record)
- Every numeric value across all series is `0`

Prefer `ChartContainer` `empty` or a placeholder instead of rendering a flat chart. After building Chart.js `data`, you can also 
use [`useChartDataCheck`](/v0-4-0/reference/app-toolkit/hooks/components#usechartdatacheck) from `@tetherto/mdk-react-devkit/foundation` 
with `{ data: chartData }`.

# Data display (/v0-4-0/ui/react/core/components/data-display)

Components for displaying data in cards, tables, badges, tags, and other visual formats.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/core#prerequisites)

## `DataTable`

<ComponentDescription name="DataTable" />

### Import

```tsx

// Types for column definitions
  DataTableColumnDef,
  DataTableSortingState,
  DataTablePaginationState,
  DataTableRowSelectionState,
} from '@tetherto/mdk-react-devkit/core'
```

### Basic usage

```tsx

type Miner = {
  id: string
  name: string
  hashrate: number
  status: string
}

const columns: DataTableColumnDef<Miner>[] = [
  {
    accessorKey: 'name',
    header: 'Name',
  },
  {
    accessorKey: 'hashrate',
    header: 'Hashrate',
    cell: ({ row }) => `${row.original.hashrate} TH/s`,
  },
  {
    accessorKey: 'status',
    header: 'Status',
  },
]

function MinersTable() {
  return <DataTable columns={columns} data={miners} />
}
```

### With sorting and pagination

```tsx
const [sorting, setSorting] = useState<DataTableSortingState>([])
const [pagination, setPagination] = useState<DataTablePaginationState>({
  pageIndex: 0,
  pageSize: 10,
})

<DataTable
  columns={columns}
  data={miners}
  sorting={sorting}
  onSortingChange={setSorting}
  pagination={pagination}
  onPaginationChange={setPagination}
/>
```

### With row selection

```tsx
const [rowSelection, setRowSelection] = useState<DataTableRowSelectionState>({})

<DataTable
  columns={columns}
  data={miners}
  rowSelection={rowSelection}
  onRowSelectionChange={setRowSelection}
  enableRowSelection
/>
```

### Styling

- `.mdk-data-table`: Root container
- `.mdk-data-table__header`: Header row
- `.mdk-data-table__body`: Table body
- `.mdk-data-table__row`: Data row
- `.mdk-data-table__cell`: Table cell

## `Card`

<ComponentDescription name="Card" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `className` | Optional | `string` | none | Additional CSS class |
| `onClick` | Optional | `function` | none | Click handler (adds clickable styling) |
| `children` | Optional | `ReactNode` | none | Card content |

### Basic usage

```tsx
<Card>
  <Card.Header>Title</Card.Header>
  <Card.Body>Content goes here</Card.Body>
  <Card.Footer>Actions</Card.Footer>
</Card>
```

Default children are automatically wrapped in the body slot:

```tsx
<Card>
  <Card.Header>Title</Card.Header>
  This content goes to the body automatically
</Card>
```

### Clickable card

```tsx
<Card onClick={() => navigate('/details')}>
  <Card.Header>Click me</Card.Header>
  <Card.Body>Navigates on click</Card.Body>
</Card>
```

### Styling

- `.mdk-card`: Root container
- `.mdk-card--clickable`: Applied when `onClick` is provided
- `.mdk-card__header`: Header slot
- `.mdk-card__body`: Body slot
- `.mdk-card__footer`: Footer slot

## `LabeledCard`

<ComponentDescription name="LabeledCard" />

A generic card container with a header label, an optional navigation link on the label, and a set of layout modifiers. Useful for compact stat or metadata blocks.

### Import

```tsx
```

### Props

All props are optional.

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `label` | Optional | `ReactNode` | none | Header content shown above the card body |
| `children` | Optional | `ReactNode` | none | Card body content |
| `getNavigateOptions` | Optional | `(label: string) => { href?: string; target?: string }` | none | Returns a link `href`/`target` for the label; only activates when `label` is a plain string |
| `isDark` | Optional | `boolean` | `false` | Applies a dark background modifier |
| `isFullWidth` | Optional | `boolean` | `false` | Stretches the card to full container width |
| `isFullHeight` | Optional | `boolean` | `false` | Stretches the card to full container height |
| `isRelative` | Optional | `boolean` | `false` | Sets `position: relative` on the container |
| `isScrollable` | Optional | `boolean` | `false` | Enables vertical scroll on the card body |
| `hasNoWrap` | Optional | `boolean` | `false` | Prevents content from wrapping |
| `hasNoMargin` | Optional | `boolean` | `false` | Removes default margin |
| `hasNoBorder` | Optional | `boolean` | `false` | Removes the card border |
| `className` | Optional | `string` | none | Additional class for the root element |

### Basic usage

```tsx
<LabeledCard label="Device Overview" isFullWidth>
  <p>Content goes here.</p>
</LabeledCard>
```

### With a navigation link on the label

```tsx
<LabeledCard
  label="Miners with error"
  getNavigateOptions={(label) => ({ href: '/miners?filter=error' })}
>
  <MinerList />
</LabeledCard>
```

When `label` is the string `'Miners with error'`, an informational tooltip is added automatically, explaining that minor errors not affecting hashrate are excluded.

## `Accordion`

<ComponentDescription name="Accordion" />

### Import

```tsx
  Accordion,
  AccordionItem,
  AccordionTrigger,
  AccordionContent,
} from '@tetherto/mdk-react-devkit/core'
```

### `Accordion` props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `title` | Optional | `string` | `''` | Accordion header title |
| `isOpened` | Optional | `boolean` | `false` | Initially expanded |
| `isRow` | Optional | `boolean` | `false` | Row layout for content |
| `unpadded` | Optional | `boolean` | `false` | Remove content padding |
| `noBorder` | Optional | `boolean` | `false` | Remove trigger border |
| `solidBackground` | Optional | `boolean` | `false` | Solid background color |
| `showToggleIcon` | Optional | `boolean` | `true` | Show expand/collapse icon |
| `toggleIconPosition` | Optional | `'left' \| 'right'` | `'left'` | Icon position |
| `customLabel` | Optional | `ReactNode` | none | Custom label next to title |
| `onValueChange` | Optional | `function` | none | Callback when state changes |

### Basic usage

```tsx
<Accordion title="FAQ Section">
  <p>This content can be expanded or collapsed.</p>
</Accordion>
```

### With custom label

```tsx
<Accordion
  title="System Status"
  customLabel={<Badge variant="success">Active</Badge>}
  showToggleIcon={false}
>
  <p>All systems operational.</p>
</Accordion>
```

### Multiple items

```tsx
<AccordionRoot type="multiple">
  <AccordionItem value="item-1">
    <AccordionTrigger>Section 1</AccordionTrigger>
    <AccordionContent>Content 1</AccordionContent>
  </AccordionItem>
  <AccordionItem value="item-2">
    <AccordionTrigger>Section 2</AccordionTrigger>
    <AccordionContent>Content 2</AccordionContent>
  </AccordionItem>
</AccordionRoot>
```

### Styling

- `.mdk-accordion`: Root container
- `.mdk-accordion--solid-background`: Solid background variant
- `.mdk-accordion__item`: Individual item
- `.mdk-accordion__trigger`: Clickable header
- `.mdk-accordion__content`: Collapsible content area

## `Badge`

<ComponentDescription name="Badge" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `count` | Optional | `number` | `0` | Number to display |
| `overflowCount` | Optional | `number` | `99` | Max count before showing "99+" |
| `showZero` | Optional | `boolean` | `false` | Show badge when count is 0 |
| `dot` | Optional | `boolean` | `false` | Show as dot instead of number |
| `text` | Optional | `string` | none | Custom text content |
| `color` | Optional | [`ColorVariant`](/v0-4-0/reference/app-toolkit/ui-kit/types#colorvariant) | `'primary'` | Badge color |
| `size` | Optional | `'sm' \| 'md' \| 'lg'` | `'md'` | Badge size |
| `status` | Optional | `'success' \| 'processing' \| 'error' \| 'warning' \| 'default'` | none | Status variant |
| `square` | Optional | `boolean` | `false` | Square badge (no border-radius) |
| `offset` | Optional | `[number, number]` | `[0, 0]` | Position offset [x, y] |

### Basic usage

```tsx
// Number badge on content
<Badge count={5}>
  <Button>Messages</Button>
</Badge>

// Overflow
<Badge count={100} overflowCount={99}>
  <BellIcon />
</Badge>
// Shows "99+"
```

### Dot badge

```tsx
<Badge dot>
  <NotificationIcon />
</Badge>
```

### Status badge

```tsx
<Badge status="success" text="Online" />
<Badge status="error" text="Offline" />
<Badge status="processing" text="Syncing" />
<Badge status="warning" text="Warning" />
```

### Standalone badge

```tsx
<Badge count={25} />
<Badge text="NEW" color="primary" square />
```

### Styling

- `.mdk-badge`: Badge element
- `.mdk-badge--{color}`: Color variant
- `.mdk-badge--{size}`: Size variant
- `.mdk-badge--dot`: Dot variant
- `.mdk-badge--status`: Status variant
- `.mdk-badge-wrapper`: Wrapper when wrapping content

## `Pagination`

<ComponentDescription name="Pagination" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `current` | Optional | `number` | `1` | Current page number |
| `total` | Optional | `number` | `0` | Total number of items |
| `pageSize` | Optional | `number` | `20` | Items per page |
| `pageSizeOptions` | Optional | `number[]` | `[10, 20, 50, 100]` | Page size options |
| `showSizeChanger` | Optional | `boolean` | `true` | Show page size dropdown |
| `showTotal` | Optional | `boolean` | `false` | Show total count text |
| `disabled` | Optional | `boolean` | `false` | Disable pagination |
| `size` | Optional | `'sm' \| 'md' \| 'lg'` | `'sm'` | Size variant |
| `onChange` | Optional | `(page: number, pageSize: number) => void` | none | Callback when the page number or page size changes |
| `onSizeChange` | Optional | `(current: number, size: number) => void` | none | Callback when the page size changes |

### Basic usage

```tsx
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)

<Pagination
  current={page}
  total={500}
  pageSize={pageSize}
  onChange={(newPage, newSize) => {
    setPage(newPage)
    setPageSize(newSize)
  }}
/>
```

### With total display

```tsx
<Pagination
  current={1}
  total={100}
  pageSize={20}
  showTotal
/>
// Shows "1-20 of 100"
```

### Styling

- `.mdk-pagination`: Root container
- `.mdk-pagination__pages`: Page buttons container
- `.mdk-pagination__button`: Navigation button
- `.mdk-pagination__button--active`: Active page
- `.mdk-pagination__total`: Total count text
- `.mdk-pagination__size-changer`: Page size select

## `Tag`

<ComponentDescription name="Tag" />

### Import

```tsx
```

### Basic usage

```tsx
<Tag>Default Tag</Tag>
<Tag color="primary">Primary</Tag>
<Tag color="success">Success</Tag>
<Tag onClose={() => handleRemove()}>Removable</Tag>
```

## `Avatar`

<ComponentDescription name="Avatar" />

### Import

```tsx
```

### Basic usage

```tsx
<Avatar src="/user.jpg" alt="User" />
<Avatar>JD</Avatar>
<Avatar src="/user.jpg" size="lg" />
```

## `SkeletonBlock`

<ComponentDescription name="SkeletonBlock" />

### Import

```tsx
```

### Basic usage

```tsx
// Text skeleton
<SkeletonBlock className="h-4 w-48" />

// Circle skeleton
<SkeletonBlock className="h-12 w-12 rounded-full" />

// Card skeleton
<Card>
  <Card.Body>
    <SkeletonBlock className="h-4 w-full mb-2" />
    <SkeletonBlock className="h-4 w-3/4" />
  </Card.Body>
</Card>
```

## `EmptyState`

<ComponentDescription name="EmptyState" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `description` | Optional | `ReactNode` | none | **Required.** Description text |
| `image` | Optional | `'default' \| 'simple' \| ReactNode` | `'default'` | Image/icon to display |
| `size` | Optional | `'sm' \| 'md' \| 'lg'` | `'md'` | Size variant |

### Basic usage

```tsx
<EmptyState description="No data available" />

<EmptyState
  description="No miners found matching your search"
  image="simple"
  size="sm"
/>
```

### Custom image

```tsx
<EmptyState
  description="No alerts at this time"
  image={<BellOffIcon size={48} />}
/>
```

### Styling

- `.mdk-empty-state`: Root container
- `.mdk-empty-state--{size}`: Size variant
- `.mdk-empty-state__image`: Image container
- `.mdk-empty-state__description`: Description text

## `BtcAveragePrice`

<ComponentDescription name="BtcAveragePrice" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `price` | Optional | `number \| null` | none | BTC price in USD; formatted with grouping and no decimal places. Shows `-` when `null`, `undefined`, non-finite, or negative |
| `label` | Optional | `string` | `'BTC Average Price'` | Label shown before the price |

### Basic usage

```tsx
<BtcAveragePrice price={97_500} />
<BtcAveragePrice price={null} />
```

## `Typography`

<ComponentDescription name="Typography" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `variant` | Optional | `'heading1' \| 'heading2' \| 'heading3' \| 'body' \| 'secondary' \| 'caption'` | none | Typography variant |
| `size` | Optional | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl' \| '3xl' \| '4xl'` | none | Text size |
| `weight` | Optional | `'light' \| 'normal' \| 'medium' \| 'semibold' \| 'bold'` | none | Font weight |
| `align` | Optional | `TextAlign` | none | Text alignment |
| `color` | Optional | `'default' \| 'muted' \| 'primary' \| 'success' \| 'warning' \| 'error'` | none | Text color variant |
| `truncate` | Optional | `boolean` | `false` | Truncate text with ellipsis |
| `className` | Optional | `string` | none | Custom class name |

### Basic usage

```tsx
<Typography variant="heading1">Operations dashboard</Typography>
<Typography variant="heading2">Sites</Typography>
<Typography variant="body">
  Primary site operations are running within expected parameters.
</Typography>
<Typography variant="secondary">Last refreshed 12 seconds ago.</Typography>
<Typography variant="caption" color="muted">Updated 2 minutes ago</Typography>
<Typography weight="semibold" align="center" truncate>Site #1</Typography>
```

## `Indicator`

<ComponentDescription name="Indicator" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `color` | Optional | `'gray' \| 'slate' \| 'red' \| 'amber' \| 'yellow' \| 'green' \| 'blue' \| 'purple'` | none | Color variant of the indicator dot |
| `size` | Optional | `'sm' \| 'md' \| 'lg'` | none | Size variant of the indicator |
| `vertical` | Optional | `boolean` | `false` | When `true`, adds extra spacing between child elements and stacks them vertically |
| `onClick` | Optional | `function` | none | Click handler |
| `children` | Optional | `ReactNode` | none | Label content (text, icons, or multiple elements) |
| `className` | Optional | `string` | none | Custom class name |

### Basic usage

```tsx
<Indicator color="green">Online</Indicator>
<Indicator color="red">Offline</Indicator>
<Indicator color="amber">Maintenance</Indicator>

<Indicator color="green" size="sm">Running</Indicator>

<Indicator color="green" vertical>
  <span>Running</span>
  <span>142</span>
</Indicator>
```

## `CurrencyToggler`

<ComponentDescription name="CurrencyToggler" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `currencies` | Required | `(string \| CurrencyItem)[]` | none | Currencies to toggle between, as codes or `CurrencyItem` objects |
| `value` | Required | `string` | none | Currently selected currency code |
| `onChange` | Required | `(currency: string) => void` | none | Called with the newly selected currency code |
| `className` | Optional | `string` | none | Custom class name for the root element |

### Basic usage

```tsx
<CurrencyToggler
  value={currency}
  onChange={setCurrency}
  currencies={['USD', 'BTC', 'SAT']}
/>
```

## `ListViewFilter`

<ComponentDescription name="ListViewFilter" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `options` | Required | `CascaderOption[]` | none | Cascader options for filtering |
| `onChange` | Required | `(selections: CascaderValue[]) => void` | none | Callback when filters change |
| `localFilters` | Optional | `LocalFilters` | none | Current filter values as key-value pairs (e.g. `{ status: ['active'] }`) |
| `filterKey` | Optional | `string` | none | Key to force re-mounting the Cascader when filters change (resets its internal state) |
| `className` | Optional | `string` | none | Custom class name for the filter button |

### Basic usage

```tsx
const FILTER_OPTIONS = [
  {
    value: 'type',
    label: 'Type',
    children: [
      { value: 'Antminer S19XP', label: 'Antminer S19XP' },
      { value: 'Avalon A1346', label: 'Avalon A1346' },
    ],
  },
  {
    value: 'status',
    label: 'Status',
    children: [
      { value: 'active', label: 'Active' },
      { value: 'offline', label: 'Offline' },
    ],
  },
]

const [filters, setFilters] = useState<LocalFilters>({})

<ListViewFilter
  options={FILTER_OPTIONS}
  localFilters={filters}
  onChange={(selections) => setFilters(selectionToFilters(selections))}
/>
```

## `Mosaic`

<ComponentDescription name="Mosaic" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `template` | Required | `string[] \| string[][]` | none | Grid layout template — area names per row that map child `MosaicItem`s to grid regions |
| `children` | Required | `ReactNode` | none | `MosaicItem` elements placed into the named grid areas |
| `columns` | Optional | `string \| string[]` | none | Column track sizes (e.g. `'1fr 2fr 1fr'`); defaults to equal columns derived from the template |
| `gap` | Optional | `string` | `'12px'` | Gap between grid cells (any CSS length) |
| `rowHeight` | Optional | `string` | `'auto'` | Height applied to each grid row (any CSS length) |
| `className` | Optional | `string` | none | Custom class name for the root element |

### Basic usage

```tsx
<Mosaic columns={3} gap={16}>
  <MosaicItem>
    <Card>Hashrate</Card>
  </MosaicItem>
  <MosaicItem span={2}>
    <Card>Active miners</Card>
  </MosaicItem>
  <MosaicItem>
    <Card>Power usage</Card>
  </MosaicItem>
</Mosaic>
```

# Errors (/v0-4-0/ui/react/core/components/errors)

Components for displaying errors, alerts, and handling error states.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/core#prerequisites)

## `CoreAlert`

<ComponentDescription name="CoreAlert" />

### Import

```tsx
```

The component is authored as `Alert` in `@tetherto/mdk-react-devkit/core` but re-exported as `CoreAlert` at the package barrel to avoid collisions with other `Alert` primitives (for example Radix `AlertDialog`).

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `type` | Optional | `'success' \| 'info' \| 'warning' \| 'error'` | `'info'` | Alert type |
| `title` | Optional | `ReactNode` | none | Main message |
| `description` | Optional | `ReactNode` | none | Additional description |
| `showIcon` | Optional | `boolean` | `false` | Show type icon |
| `icon` | Optional | `ReactNode` | none | Custom icon |
| `closable` | Optional | `boolean` | `false` | Show close button |
| `onClose` | Optional | `function` | none | Close callback |
| `banner` | Optional | `boolean` | `false` | Full-width banner style |
| `action` | Optional | `ReactNode` | none | Action element |

### Basic usage

```tsx
<CoreAlert type="info" title="System maintenance scheduled" showIcon />

<CoreAlert
  type="success"
  title="Configuration saved"
  description="Your settings have been updated successfully."
  showIcon
  closable
/>
```

### With action

```tsx
<CoreAlert
  type="warning"
  title="Low hashrate detected"
  description="Some miners are performing below expected levels."
  showIcon
  action={<Button size="sm">View Details</Button>}
/>
```

### Banner style

```tsx
<CoreAlert
  type="error"
  title="Connection lost"
  description="Unable to connect to the pool server."
  banner
  showIcon
/>
```

### Styling

- `.mdk-alert`: Root container
- `.mdk-alert--{type}`: Type variant
- `.mdk-alert--banner`: Banner variant
- `.mdk-alert__icon`: Icon container
- `.mdk-alert__content`: Content wrapper
- `.mdk-alert__title`: Title text
- `.mdk-alert__description`: Description text
- `.mdk-alert__action`: Action container
- `.mdk-alert__close`: Close button

## `ErrorBoundary`

<ComponentDescription name="ErrorBoundary" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `fallback` | Optional | `ReactNode` | none | Custom fallback UI |
| `componentName` | Optional | `string` | none | Component name for error display |
| `onError` | Optional | `function` | none | Error callback |
| `children` | Required | `ReactNode` | none | Children to wrap |

### Basic usage

```tsx
<ErrorBoundary fallback={<div>Something went wrong</div>}>
  <MyComponent />
</ErrorBoundary>
```

### With component name

```tsx
<ErrorBoundary
  componentName="HashrateChart"
  onError={(error, info) => console.error(error)}
>
  <HashrateChart data={data} />
</ErrorBoundary>
```

### Higher-order component

```tsx

const SafeChart = withErrorBoundary(
  Chart,
  'Chart',
  (error) => logError(error)
)

<SafeChart data={data} />
```

#### Styling

- `.mdk-error-boundary`: Root container
- `.mdk-error-boundary__title`: Error title
- `.mdk-error-boundary__message`: Error message
- `.mdk-error-boundary__details`: Expandable details
- `.mdk-error-boundary__stack`: Stack trace

## `ErrorCard`

<ComponentDescription name="ErrorCard" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `error` | Required | `string` | none | Error message (supports `\n` for line breaks) |
| `title` | Optional | `string` | `'Errors'` | Title displayed above the error message |
| `variant` | Optional | `'card' \| 'inline'` | `'card'` | Display variant — `card` shows a bordered container, `inline` shows flat text |
| `className` | Optional | `string` | none | Additional CSS class |

### Basic usage

```tsx
<ErrorCard error="Connection timed out. Please try again." />

<ErrorCard
  title="Authentication error"
  error="Invalid credentials. Please check your username and password."
/>

<ErrorCard
  variant="inline"
  title="Warning"
  error="Some miners are reporting high temperatures."
/>
```

### Styling

- `.mdk-error-card`: Root container
- `.mdk-error-card--{variant}`: Variant modifier (`card` / `inline`)
- `.mdk-error-card__title`: Title text
- `.mdk-error-card__message`: Message text

## `NotFoundPage`

<ComponentDescription name="NotFoundPage" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `onGoHome` | Optional | `VoidFunction` | none | Callback fired when the "Go Home" button is clicked (button only renders when provided) |
| `title` | Optional | `string` | none | Page title |
| `message` | Optional | `string` | none | Message displayed below the title |
| `className` | Optional | `string` | none | Additional CSS class |

### Basic usage

```tsx
<NotFoundPage onGoHome={() => navigate('/')} />

<NotFoundPage
  title="Miner not found"
  message="The miner you're looking for doesn't exist or has been removed."
  onGoHome={() => navigate('/')}
/>
```
### Styling

- `.mdk-not-found-page`: Root container
- `.mdk-not-found-page__title`: Title text
- `.mdk-not-found-page__message`: Message text
- `.mdk-not-found-page__button`: Go Home button

# Form components (/v0-4-0/ui/react/core/components/forms)

Form components handle user input and form submission. All components are built with accessibility in mind and support keyboard navigation.

The category splits into three groups:

- [Controls](#controls): raw inputs you can drop in standalone or compose inside `<Form>`
- [Composition](/v0-4-0/ui/react/core/components/forms/composition): the `<Form>` provider and the low-level building blocks 
(`FormField`, `FormItem`, `FormLabel`, `FormControl`, `FormDescription`, `FormMessage`)
- [Prebuilt fields](/v0-4-0/ui/react/core/components/forms/fields): one-tag wrappers that bind a control to React hook form

## Prerequisites

Before using these components, complete the [@tetherto/mdk-react-devkit installation](/v0-4-0/ui/react/core#prerequisites).

## Controls

Standalone form controls. Manage `value` and `onChange`, or compose them inside [`<Form>`](/v0-4-0/ui/react/core/components/forms/composition#form) with 
[`FormField`](/v0-4-0/ui/react/core/components/forms/composition#formfield).

<ComponentTable category="Forms" subcategory={null} pkg="core" />

### `ActionButton`

<ComponentDescription name="ActionButton" />

Triggers an action after **confirmation** in either an inline **popover** (default) or a modal **dialog**. Set **variant** so the trigger 
[`Button`](#button) matches severity (for example `danger` for destructive flows).

After the user confirms, run your side effects there. Consider showing feedback with [`Toast`](/v0-4-0/ui/react/core/components/overlays#toast) and **`Toaster`** 
from the same Toast setup as in [`overlays`](/v0-4-0/ui/react/core/components/overlays). This is the pattern the [demo app(../quickstart) applies.

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `label` | Optional | `string` | none | Trigger button label |
| `variant` | Optional | `'primary' \| 'secondary' \| 'danger'` | `'secondary'` | Visual variant of the trigger [`Button`](#button) |
| `loading` | Optional | `boolean` | `false` | Loading state on the trigger |
| `disabled` | Optional | `boolean` | `false` | Disable the trigger |
| `className` | Optional | `string` | none | Class on the trigger |
| `mode` | Optional | `'popover' \| 'dialog'` | `'popover'` | **`popover`**: confirmation UI in an anchored popover. **`dialog`**: confirmation UI in a modal [`Dialog`](/v0-4-0/ui/react/core/components/overlays#dialog). |
| `confirmation` | Required | `ActionButtonConfirmation` | none | title, body copy, and confirm/cancel handlers. See [Confirmation object](#confirmation-object). |

#### Confirmation object

Nested inside `confirmation`:

| Field | Type | Description |
|-------|------|-------------|
| `title` | `string` | Heading shown in the confirmation UI |
| `description` | `ReactNode` | Optional body (text or JSX) |
| `onConfirm` | `() => void` | Called when the user confirms |
| `onCancel` | `() => void` | Called when the user cancels |
| `confirmLabel` | `string` | Optional confirm button label (defaults vary by `mode`) |
| `cancelLabel` | `string` | Optional cancel button label (defaults to **Cancel**) |
| `icon` | `ReactNode` | Optional icon in **popover** header (popover mode only; dialog layout uses title and body) |

#### Basic usage (popover)

Default `mode` is **`popover`**: compact confirmation next to the trigger.

```tsx
<ActionButton
  label="Restart service"
  variant="danger"
  confirmation={{
    title: 'Restart service',
    description: 'The service will be unavailable briefly.',
    onConfirm: () => {
      void restartService()
    },
  }}
/>
```

#### Dialog mode

Set **`mode="dialog"`** when you want a full modal confirmation (for example irreversible or high-impact actions).

```tsx
<ActionButton
  label="Factory reset"
  variant="danger"
  mode="dialog"
  confirmation={{
    title: 'Confirm factory reset',
    description: 'This cannot be undone.',
    onConfirm: () => {
      void factoryReset()
    },
  }}
/>
```

### `Button`

<ComponentDescription name="Button" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `variant` | Optional | `'primary' \| 'secondary' \| 'danger' \| 'tertiary' \| 'link' \| 'icon' \| 'outline' \| 'ghost'` | `'secondary'` | Visual variant |
| `size` | Optional | `'sm' \| 'md' \| 'lg'` | none | Button size |
| `loading` | Optional | `boolean` | `false` | Show loading spinner |
| `fullWidth` | Optional | `boolean` | `false` | Expand to container width |
| `icon` | Optional | `ReactNode` | none | Icon element |
| `iconPosition` | Optional | `'left' \| 'right'` | `'left'` | Icon placement |
| `contentClassName` | Optional | `string` | none | Class names applied to the inner content wrapper |
| `disabled` | Optional | `boolean` | `false` | Disable interaction |

#### Basic usage

```tsx
<Button>Default Button</Button>
<Button variant="primary">Primary</Button>
<Button variant="danger">Delete</Button>
<Button variant="outline">Outline</Button>
```

#### With icon

```tsx
<Button icon={<PlusIcon />}>Add Item</Button>
<Button icon={<ArrowRightIcon />} iconPosition="right">
  Continue
</Button>
```

#### Loading state

```tsx
<Button loading>Saving...</Button>
<Button variant="primary" loading disabled>
  Processing
</Button>
```

#### Styling

- `.mdk-button`: Root element
- `.mdk-button--variant-{variant}`: Variant modifier
- `.mdk-button--size-{size}`: Size modifier
- `.mdk-button--full-width`: Full width modifier
- `.mdk-button--loading`: Loading state

### `Cascader`

<ComponentDescription name="Cascader" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `options` | Required | `CascaderOption[]` | none | Hierarchical options; parent options with children render in the left panel |
| `value` | Optional | `CascaderValue \| CascaderValue[]` | none | Current selected value(s) |
| `onChange` | Optional | `(value: CascaderValue \| CascaderValue[] \| null) => void` | none | Callback when the selection changes |
| `multiple` | Optional | `boolean` | `false` | Enable multiple selection (shows checkboxes; selected items render as tags) |
| `placeholder` | Optional | `string` | none | Placeholder text shown when no selections are made |
| `disabled` | Optional | `boolean` | `false` | Disable the entire cascader |
| `dropdownClassName` | Optional | `string` | none | Custom class name for the dropdown panels container |
| `className` | Optional | `string` | none | Custom class name for the root element |

#### Basic usage

```tsx
<Cascader
  options={[
    {
      value: 'na',
      label: 'North America',
      children: [
        { value: 'us-east', label: 'US East' },
        { value: 'us-west', label: 'US West' },
      ],
    },
  ]}
  value={value}
  onChange={setValue}
/>
```

### `Checkbox`

<ComponentDescription name="Checkbox" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `size` | Optional | `'xs' \| 'sm' \| 'md' \| 'lg'` | `'md'` | Checkbox size |
| `color` | Optional | `'default' \| 'primary' \| 'success' \| 'warning' \| 'error'` | `'primary'` | Color when checked |
| `radius` | Optional | `'none' \| 'small' \| 'medium' \| 'large' \| 'full'` | `'none'` | Border radius |
| `checked` | Optional | `boolean \| 'indeterminate'` | none | Checked state (controlled) |
| `onCheckedChange` | Optional | `function` | none | Change callback |

#### Basic usage

```tsx
<Checkbox checked={checked} onCheckedChange={setChecked} />

<label className="flex items-center gap-2">
  <Checkbox checked={terms} onCheckedChange={setTerms} />
  I agree to the terms
</label>
```

#### Sizes and colors

```tsx
<Checkbox size="xs" />
<Checkbox size="sm" />
<Checkbox size="md" />
<Checkbox size="lg" color="success" />
```

#### Styling

- `.mdk-checkbox`: Root element
- `.mdk-checkbox--{size}`: Size modifier
- `.mdk-checkbox--{color}`: Color modifier
- `.mdk-checkbox__indicator`: Check mark container

### `DatePicker`

<ComponentDescription name="DatePicker" />

#### Import

```tsx
```

#### Basic usage

```tsx
<DatePicker
  value={date}
  onChange={setDate}
  placeholder="Select date"
/>
```

### `DateRangePicker`

<ComponentDescription name="DateRangePicker" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `selected` | Optional | `DateRange` | none | Selected `{ from, to }` range (controlled) |
| `onSelect` | Optional | `(range: DateRange \| undefined) => void` | none | Called when the user applies a range |
| `placeholder` | Optional | `string` | `'Pick a date range'` | Trigger text when no range is selected |
| `dateFormat` | Optional | `string` | `'MM/dd/yyyy'` | `date-fns` format used in the trigger label |
| `disabled` | Optional | `boolean` | `false` | Disable interaction |
| `showPresets` | Optional | `boolean` | `true` | Show the default preset buttons for the last 7, 14, 30, and 90 days |
| `presets` | Optional | `PresetItem[]` | none | Override the default preset list |
| `allowFutureDates` | Optional | `boolean` | `false` | When `false`, dates after today are disabled |
| `triggerClassName` | Optional | `string` | none | Extra classes for the trigger button |
| `calendarClassName` | Optional | `string` | none | Extra classes for the calendar |
| `modalClassName` | Optional | `string` | none | Extra classes for the popover modal |

Any other [`react-day-picker` props](https://daypicker.dev/) (excluding `mode` and `selected`) are forwarded to the underlying calendar.

#### Basic usage

```tsx
const [range, setRange] = useState<DateRange>()

<DateRangePicker
  selected={range}
  onSelect={setRange}
/>
```

#### Custom presets

```tsx
const presets: PresetItem[] = [
  { label: 'This week', value: { from: startOfWeek(new Date()), to: new Date() } },
  { label: 'This month', value: { from: startOfMonth(new Date()), to: new Date() } },
]

<DateRangePicker
  selected={range}
  onSelect={setRange}
  presets={presets}
/>
```

#### Allowing future dates

```tsx
<DateRangePicker
  selected={range}
  onSelect={setRange}
  allowFutureDates
  showPresets={false}
/>
```

#### Styling

- `.mdk-date-picker__trigger`: Trigger button
- `.mdk-date-picker__modal`: Popover modal container
- `.mdk-date-picker__calendar`: Calendar grid
- `.mdk-date-picker__summary`: Selected-range summary block
- `.mdk-date-picker__presets`: Preset button row

### `Input`

<ComponentDescription name="Input" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `label` | Optional | `string` | none | Visible label for the field |
| `variant` | Optional | `'default' \| 'search'` | `'default'` | Input variant |
| `size` | Optional | `'default' \| 'medium'` | `'default'` | Input size |
| `error` | Optional | `string` | none | Error message (shows red border) |
| `prefix` | Optional | `ReactNode` | none | Element before input |
| `suffix` | Optional | `ReactNode` | none | Element after input |
| `wrapperClassName` | Optional | `string` | none | Wrapper element class |

#### Basic usage

```tsx
<Input label="Email" placeholder="Enter email" id="email" />
<Input variant="search" placeholder="Search miners..." />
```

#### With prefix/suffix

```tsx
<Input prefix="$" suffix="USD" placeholder="0.00" />
<Input suffix="°C" placeholder="Temperature" />
<Input prefix={<UserIcon />} placeholder="Username" />
```

#### Error state

```tsx
<Input
  label="MAC Address"
  error="Invalid MAC address format"
  value={mac}
  onChange={(e) => setMac(e.target.value)}
/>
```

#### Styling

- `.mdk-input`: Input element
- `.mdk-input__wrapper`: Wrapper container
- `.mdk-input__wrapper--error`: Error state
- `.mdk-input__label`: Label element
- `.mdk-input__prefix`: Prefix element
- `.mdk-input__suffix`: Suffix element
- `.mdk-input__error`: Error message

### `Label`

<ComponentDescription name="Label" />

#### Import

```tsx
```

#### Basic usage

```tsx
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" />

<Label htmlFor="password" required>Password</Label>
<Input id="password" type="password" />
```

### `MultiSelect`

<ComponentDescription name="MultiSelect" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `options` | Required | `MultiSelectOption[]` | none | Selectable options (`{ value, label }`) |
| `value` | Optional | `string[]` | none | Controlled selected values; omit to use `defaultValue` for uncontrolled mode |
| `defaultValue` | Optional | `string[]` | none | Initial values for uncontrolled mode; ignored when `value` is provided |
| `onValueChange` | Optional | `(next: string[]) => void` | none | Called with the updated selection |
| `placeholder` | Optional | `ReactNode` | none | Trigger placeholder shown when nothing is selected |
| `emptyMessage` | Optional | `ReactNode` | none | Rendered inside the popover when `options` is empty |
| `maxSelectedDisplay` | Optional | `number` | none | Max selected chips shown in the trigger before collapsing the rest into a "+N more" badge |
| `disabled` | Optional | `boolean` | `false` | Disable the control |
| `size` | Optional | `MultiSelectSize` | none | Size variant |
| `variant` | Optional | `MultiSelectVariant` | none | Visual variant |
| `aria-label` | Optional | `string` | none | Accessible label applied to the trigger button |
| `className` | Optional | `string` | none | Custom class name for the root element |
| `contentClassName` | Optional | `string` | none | Custom class name for the popover content panel |

#### Basic usage

```tsx

const MINER_TYPES: MultiSelectOption[] = [
  { value: 'miner-am-s19xp', label: 'Antminer S19XP' },
  { value: 'miner-wm-m56s', label: 'WhatsMiner M56S' },
  { value: 'miner-av-a1346', label: 'Avalon A1346' },
]

function MinerTypeFilter() {
  const [selected, setSelected] = useState<string[]>([])

  return (
    <MultiSelect
      options={MINER_TYPES}
      value={selected}
      onValueChange={setSelected}
      placeholder="Filter by miner type"
      maxSelectedDisplay={2}
    />
  )
}
```

### `Radio`

<ComponentDescription name="Radio" />

#### Radio composition parts

| Part | Role |
|------|------|
| `RadioGroup` | Root that holds the selected value and calls `onValueChange` when it changes. |
| `Radio` | Default circular radio item; use `label` or children for visible text. |
| `RadioCard` | Card-styled option for horizontal or compact layouts; uses the same item primitive as `Radio`. |

#### Import

```tsx
```

#### `Radio` props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `label` | Optional | `string` | none | Label text (or use children for custom content) |
| `size` | Optional | `'sm' \| 'md' \| 'lg'` | none | Size variant of the radio |
| `color` | Optional | `'default' \| 'primary' \| 'success' \| 'warning' \| 'error'` | none | Color variant when checked |
| `radius` | Optional | `BorderRadius` | none | Border radius variant (`full` makes it circular) |
| `children` | Optional | `ReactNode` | none | Custom content (takes precedence over `label`) |
| `indicatorClassName` | Optional | `string` | none | Custom class name for the indicator element |
| `className` | Optional | `string` | none | Custom class name for the root element |

#### Basic usage

```tsx
<RadioGroup value={value} onValueChange={setValue}>
  <Radio value="small">Small</Radio>
  <Radio value="medium">Medium</Radio>
  <Radio value="large">Large</Radio>
</RadioGroup>
```

**RadioCard** suits card-style, horizontal groups:

```tsx
<RadioGroup value={value} orientation="horizontal" onValueChange={setValue}>
  <RadioCard value="a" label="Option A" />
  <RadioCard value="b" label="Option B" />
</RadioGroup>
```

### `Select`

<ComponentDescription name="Select" />

#### Import

```tsx
  Select,
  SelectTrigger,
  SelectValue,
  SelectContent,
  SelectItem,
  SelectGroup,
  SelectLabel,
} from '@tetherto/mdk-react-devkit/core'
```

#### `Select` props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `allowClear` | Optional | `boolean` | `false` | Show a clear button when a value is selected |

#### `SelectTrigger` props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `size` | Optional | `'sm' \| 'md' \| 'lg'` | `'lg'` | Trigger size |
| `variant` | Optional | `'default' \| 'colored'` | `'default'` | Visual variant |
| `color` | Optional | `string` | none | Custom color for colored variant (hex) |

#### Basic usage

```tsx
<Select value={value} onValueChange={setValue}>
  <SelectTrigger>
    <SelectValue placeholder="Select option" />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="option1">Option 1</SelectItem>
    <SelectItem value="option2">Option 2</SelectItem>
    <SelectItem value="option3">Option 3</SelectItem>
  </SelectContent>
</Select>
```

#### With groups

```tsx
<Select>
  <SelectTrigger size="md">
    <SelectValue placeholder="Select pool" />
  </SelectTrigger>
  <SelectContent>
    <SelectGroup>
      <SelectLabel>North America</SelectLabel>
      <SelectItem value="us-east">US East</SelectItem>
      <SelectItem value="us-west">US West</SelectItem>
    </SelectGroup>
    <SelectGroup>
      <SelectLabel>Europe</SelectLabel>
      <SelectItem value="eu-west">EU West</SelectItem>
    </SelectGroup>
  </SelectContent>
</Select>
```

#### Colored variant

```tsx
<Select>
  <SelectTrigger variant="colored" color="#72F59E">
    <SelectValue placeholder="Status" />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="active">Active</SelectItem>
    <SelectItem value="inactive">Inactive</SelectItem>
  </SelectContent>
</Select>
```

#### Styling

- `.mdk-select__trigger`: Trigger button
- `.mdk-select__trigger--{size}`: Size modifier
- `.mdk-select__trigger--colored`: Colored variant
- `.mdk-select__content`: Dropdown content
- `.mdk-select__item`: Option item

### `Switch`

<ComponentDescription name="Switch" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `size` | Optional | `'sm' \| 'md' \| 'lg'` | `'md'` | Switch size |
| `color` | Optional | `'default' \| 'primary' \| 'success' \| 'warning' \| 'error'` | `'default'` | Color when checked |
| `radius` | Optional | `'none' \| 'small' \| 'medium' \| 'large' \| 'full'` | `'none'` | Border radius |
| `checked` | Required | `boolean` | none | Checked state (controlled) |
| `onCheckedChange` | Required | `function` | none | Change callback |

#### Basic usage

```tsx
<Switch checked={enabled} onCheckedChange={setEnabled} />

<label className="flex items-center gap-2">
  <Switch checked={darkMode} onCheckedChange={setDarkMode} />
  Dark Mode
</label>
```

#### Sizes and colors

```tsx
<Switch size="sm" />
<Switch size="md" color="primary" />
<Switch size="lg" color="success" />
```

#### Styling

- `.mdk-switch`: Root element
- `.mdk-switch--{size}`: Size modifier
- `.mdk-switch--{color}`: Color modifier
- `.mdk-switch__thumb`: Toggle thumb

### `TagInput`

<ComponentDescription name="TagInput" />

#### Import

```tsx
```

#### Basic usage

```tsx
<TagInput
  value={tags}
  onChange={setTags}
  placeholder="Add tags..."
/>
```

### `TextArea`

<ComponentDescription name="TextArea" />

#### Import

```tsx
```

#### Basic usage

```tsx
<TextArea
  label="Description"
  placeholder="Enter description"
  rows={4}
  value={value}
  onChange={(e) => setValue(e.target.value)}
/>
```

## Composition

The `<Form>` provider plus the low-level compound components for assembling custom form fields. Use these when no [prebuilt field](/v0-4-0/ui/react/core/components/forms/fields) fits, or when you need full control over the field's layout.

<ComponentTable category="Forms" subcategory="Composition" pkg="core" />

See the [Composition page](/v0-4-0/ui/react/core/components/forms/composition) for `<Form>`, the seven compound parts, and built-in validators.

## Prebuilt fields

Form-bound fields pre-wired to React hook form. Each combines `FormField`, `FormItem`, `FormLabel`, `FormControl`, `FormDescription`, and `FormMessage` so you can render a labelled, validated field from a single component.

<ComponentTable category="Forms" subcategory="Fields" pkg="core" />

See the [Prebuilt fields page](/v0-4-0/ui/react/core/components/forms/fields) for the full prop reference and component-specific examples.

# Form composition (/v0-4-0/ui/react/core/components/forms/composition)

The `<Form>` provider plus the seven compound parts (`FormField`, `FormItem`, `FormLabel`, `FormControl`, `FormDescription`, `FormMessage`) are the building blocks for any form. Use them when no [prebuilt field](/v0-4-0/ui/react/core/components/forms/fields) fits, or when you need full control over a field's layout.

For standalone inputs that you can drop in without `<Form>`, see [Controls](/v0-4-0/ui/react/core/components/forms#controls).

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/core#prerequisites)
- Familiarity with [React hook form](https://react-hook-form.com/)

## Components

<ComponentTable category="Forms" subcategory="Composition" pkg="core" />

## `Form`

<ComponentDescription name="Form" />

The package includes a complete form system built on [React hook form](https://react-hook-form.com/). `<Form>` is the context provider; the other six components on this page are the building blocks you compose inside it. See [Prebuilt fields](/v0-4-0/ui/react/core/components/forms/fields) for ready-made wrappers that render a labelled, validated field in a single tag.

### Import

```tsx
  Form,
  FormField,
  FormItem,
  FormLabel,
  FormControl,
  FormDescription,
  FormMessage,
  useFormField,
} from '@tetherto/mdk-react-devkit/core'
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `form` | Required | `UseFormReturn` | none | The instance returned by `useForm()` |
| `children` | Required | `ReactNode` | none | Form content |
| `onSubmit` | Required | `function` | none | Submit handler. Wrap with `form.handleSubmit(...)` to get parsed values |

Any other standard `<form>` element attribute (`id`, `action`, `noValidate`, `onReset`, `ref`, `data-*`, `aria-*`, etc.) is forwarded to the underlying DOM element.

### Basic usage

```tsx

function MyForm() {
  const form = useForm({
    defaultValues: { email: '' },
  })

  const onSubmit = (values) => {
    console.log(values)
  }

  return (
    <Form form={form} onSubmit={form.handleSubmit(onSubmit)}>
      <FormField
        control={form.control}
        name="email"
        render={({ field }) => (
          <FormItem>
            <FormLabel>Email</FormLabel>
            <FormControl>
              <Input placeholder="email@example.com" {...field} />
            </FormControl>
            <FormMessage />
          </FormItem>
        )}
      />
      <Button type="submit">Submit</Button>
    </Form>
  )
}
```

### Built-in validators

The package re-exports common Zod schemas and validator helpers so you can wire `useForm` to a resolver without rolling your own.

```tsx
```

### Styling

- `.mdk-form`: Root `<form>` element

## `FormField`

<ComponentDescription name="FormField" />

### Import

```tsx
```

### Props

`FormField` is a thin wrapper over [React hook form's `Controller`](https://react-hook-form.com/docs/usecontroller/controller) and accepts the same props (`control`, `name`, `defaultValue`, `rules`, `render`, etc.). It additionally provides field context to descendants like `FormLabel` and `FormControl` so they can wire up ARIA attributes automatically.

### Basic usage

```tsx
<FormField
  control={form.control}
  name="username"
  render={({ field }) => (
    <FormItem>
      <FormLabel>Username</FormLabel>
      <FormControl>
        <Input {...field} />
      </FormControl>
      <FormMessage />
    </FormItem>
  )}
/>
```

## `FormItem`

<ComponentDescription name="FormItem" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `className` | Optional | `string` | none | Extra classes merged onto the wrapper |

Any other standard `<div>` attribute (`id`, `ref`, `data-*`, `aria-*`, etc.) is forwarded to the underlying DOM element.

### Basic usage

```tsx
<FormField
  control={form.control}
  name="bio"
  render={({ field }) => (
    <FormItem>
      <FormLabel>Bio</FormLabel>
      <FormControl>
        <TextArea {...field} />
      </FormControl>
      <FormDescription>Max 200 characters</FormDescription>
      <FormMessage />
    </FormItem>
  )}
/>
```

### Styling

- `.mdk-form-item`: Root element

## `FormLabel`

<ComponentDescription name="FormLabel" />

### Import

```tsx
```

### Props

Accepts every prop the underlying [`FormLabel`](#formlabel) accepts. The `htmlFor` attribute is set automatically from the `FormItem` context, and an error modifier class is applied when the field has a validation error.

### Basic usage

```tsx
<FormItem>
  <FormLabel>Email address</FormLabel>
  <FormControl>
    <Input type="email" {...field} />
  </FormControl>
</FormItem>
```

### Styling

- `.mdk-form-label`: Root element
- `.mdk-form-label--error`: Applied when the field has a validation error

## `FormControl`

<ComponentDescription name="FormControl" />

### Import

```tsx
```

### Props

`FormControl` uses [Radix `Slot`](https://www.radix-ui.com/primitives/docs/utilities/slot) to merge its props onto its single child without adding an extra DOM node. It autoinjects `id`, `aria-describedby`, and `aria-invalid` based on the surrounding `FormItem`/`FormField` context.

### Basic usage

```tsx
<FormItem>
  <FormLabel>Phone</FormLabel>
  <FormControl>
    <Input type="tel" {...field} />
  </FormControl>
  <FormMessage />
</FormItem>
```

`FormControl` must wrap exactly one child element.

## `FormDescription`

<ComponentDescription name="FormDescription" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `className` | Optional | `string` | none | Extra classes merged onto the wrapper |

Any other standard `<p>` attribute (`id`, `ref`, `data-*`, `aria-*`, etc.) is forwarded to the underlying DOM element.

### Basic usage

```tsx
<FormItem>
  <FormLabel>Password</FormLabel>
  <FormControl>
    <Input type="password" {...field} />
  </FormControl>
  <FormDescription>Must be at least 8 characters</FormDescription>
  <FormMessage />
</FormItem>
```

### Styling

- `.mdk-form-description`: Root element

## `FormMessage`

<ComponentDescription name="FormMessage" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `children` | Required | `ReactNode` | none | Fallback content shown when the field has no validation error |
| `className` | Optional | `string` | none | Extra classes merged onto the wrapper |

Any other standard `<p>` attribute (`id`, `ref`, `data-*`, `aria-*`, etc.) is forwarded to the underlying DOM element.

`FormMessage` always renders to prevent layout shift. When an error is present it shows the validation message and applies `role="alert"`; otherwise it shows `children` (or a non-breaking space placeholder) and adds the `--empty` modifier class.

### Basic usage

```tsx
<FormItem>
  <FormLabel>Email</FormLabel>
  <FormControl>
    <Input type="email" {...field} />
  </FormControl>
  <FormMessage />
</FormItem>
```

### Styling

- `.mdk-form-message`: Root element
- `.mdk-form-message--empty`: Applied when no error and no children are present

# Prebuilt form fields (/v0-4-0/ui/react/core/components/forms/fields)

Form-bound fields pre-wired to React hook form. Each combines [`FormField`](/v0-4-0/ui/react/core/components/forms/composition#formfield), [`FormItem`](/v0-4-0/ui/react/core/components/forms/composition#formitem), [`FormLabel`](/v0-4-0/ui/react/core/components/forms/composition#formlabel), [`FormControl`](/v0-4-0/ui/react/core/components/forms/composition#formcontrol), [`FormDescription`](/v0-4-0/ui/react/core/components/forms/composition#formdescription), and [`FormMessage`](/v0-4-0/ui/react/core/components/forms/composition#formmessage) so you can render a labelled, validated field from a single component.

For raw inputs without form binding, see [Controls](/v0-4-0/ui/react/core/components/forms#controls). To assemble a custom field from the building blocks, see [Composition](/v0-4-0/ui/react/core/components/forms/composition).

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/core#prerequisites)
- Wrap your form in [`<Form>`](/v0-4-0/ui/react/core/components/forms/composition#form) and pass `form.control` to each field

## Components

<ComponentTable category="Forms" subcategory="Fields" pkg="core" />

## Common props

Every prebuilt field accepts the [`Controller`](https://react-hook-form.com/docs/usecontroller/controller) props from React hook form (`control`, `name`, `defaultValue`, `rules`, `shouldUnregister`, `disabled`) plus three presentation props:

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `label` | Optional | `string` | none | Label displayed above the field |
| `description` | Optional | `string` | none | Helper text below the field |
| `placeholder` | Optional | `string` | none | Placeholder text (ignored by `FormCheckbox` and `FormSwitch`) |

Component-specific props are listed under each field below.

## `FormCascader`

<ComponentDescription name="FormCascader" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `options` | Required | `CascaderOption[]` | none | Hierarchical options tree |
| `multiple` | Optional | `boolean` | `false` | Allow multiple selection |
| `cascaderProps` | Required | `object` | none | Pass-through props for the underlying `Cascader` |

### Basic usage

```tsx
<FormCascader
  control={form.control}
  name="category"
  label="Category"
  placeholder="Select category..."
  options={[
    {
      value: 'electronics',
      label: 'Electronics',
      children: [
        { value: 'phones', label: 'Phones' },
        { value: 'laptops', label: 'Laptops' },
      ],
    },
  ]}
  description="Choose a category and subcategory"
/>
```

### Multiple selection

```tsx
<FormCascader
  control={form.control}
  name="categories"
  label="Categories"
  placeholder="Select categories..."
  options={categoryOptions}
  multiple
  description="Select multiple categories"
/>
```

## `FormCheckbox`

<ComponentDescription name="FormCheckbox" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `layout` | Optional | `'row' \| 'column'` | `'row'` | Whether the label sits inline with the checkbox or above it |
| `checkboxProps` | Required | `object` | none | Pass-through props for the underlying `Checkbox` |

### Basic usage

```tsx
<FormCheckbox
  control={form.control}
  name="terms"
  label="Accept terms and conditions"
  description="You must agree to continue"
/>
```

## `FormDatePicker`

<ComponentDescription name="FormDatePicker" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `datePickerProps` | Required | `object` | none | Pass-through props for the underlying `DatePicker` (excluding `selected` and `onSelect`, which are bound to the form) |

### Basic usage

```tsx
<FormDatePicker
  control={form.control}
  name="startDate"
  label="Start Date"
  placeholder="Pick a date"
  description="When should this take effect?"
/>
```

## `FormInput`

<ComponentDescription name="FormInput" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `type` | Optional | `'text' \| 'email' \| 'password' \| 'number' \| ...` | `'text'` | HTML input type |
| `variant` | Optional | `'default' \| 'search'` | `'default'` | Input variant |
| `inputProps` | Required | `object` | none | Pass-through props for the underlying `Input` (excluding `type` and `variant`) |

### Basic usage

```tsx
<FormInput
  control={form.control}
  name="email"
  label="Email"
  type="email"
  placeholder="user@example.com"
  description="We'll never share your email"
/>
```

## `FormRadioGroup`

<ComponentDescription name="FormRadioGroup" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `options` | Required | `FormRadioOption[]` | none | Array of `{ value, label, disabled? }` items |
| `orientation` | Optional | `'horizontal' \| 'vertical'` | `'vertical'` | Radio layout direction |
| `radioGroupProps` | Required | `object` | none | Pass-through props for the underlying `RadioGroup` |

### Basic usage

```tsx
<FormRadioGroup
  control={form.control}
  name="priority"
  label="Priority"
  options={[
    { value: 'low', label: 'Low' },
    { value: 'medium', label: 'Medium' },
    { value: 'high', label: 'High' },
  ]}
  orientation="horizontal"
/>
```

## `FormSelect`

<ComponentDescription name="FormSelect" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `options` | Required | `FormSelectOption[]` | none | Array of `{ value, label, disabled? }` items |
| `selectProps` | Required | `object` | none | Pass-through props for the underlying `Select` (excluding `onValueChange` and `defaultValue`, which are bound to the form) |

### Basic usage

```tsx
<FormSelect
  control={form.control}
  name="role"
  label="Role"
  placeholder="Select a role"
  options={[
    { value: 'admin', label: 'Admin' },
    { value: 'user', label: 'User' },
  ]}
  description="Your access level"
/>
```

## `FormSwitch`

<ComponentDescription name="FormSwitch" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `layout` | Optional | `'row' \| 'column'` | `'row'` | Whether the label sits inline with the switch or above it |
| `switchProps` | Required | `object` | none | Pass-through props for the underlying `Switch` (excluding `checked` and `onCheckedChange`) |

### Basic usage

```tsx
<FormSwitch
  control={form.control}
  name="notifications"
  label="Enable notifications"
  description="Receive alerts when miners go offline"
/>
```

## `FormTagInput`

<ComponentDescription name="FormTagInput" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `options` | Required | `TagInputOption[]` | none | Suggested tags offered as autocomplete options |
| `allowCustomTags` | Optional | `boolean` | `true` | Let users type tags not present in `options` |
| `variant` | Optional | `'default' \| 'search'` | `'search'` | Visual variant |
| `tagInputProps` | Required | `object` | none | Pass-through props for the underlying `TagInput` |

### Basic usage

```tsx
<FormTagInput
  control={form.control}
  name="tags"
  label="Tags"
  placeholder="Add tags..."
  options={['React', 'TypeScript', 'Node.js']}
  description="Select or type tags"
/>
```

## `FormTextArea`

<ComponentDescription name="FormTextArea" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `textAreaProps` | Required | `object` | none | Pass-through props for the underlying `TextArea` |

### Basic usage

```tsx
<FormTextArea
  control={form.control}
  name="bio"
  label="Bio"
  placeholder="Tell us about yourself"
  description="Max 200 characters"
/>
```

# Loading (/v0-4-0/ui/react/core/components/loading)

Components for indicating loading and progress states.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/core#prerequisites)

## `Spinner`

<ComponentDescription name="Spinner" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `size` | Optional | `'sm' \| 'md' \| 'lg'` | `'md'` | Spinner size |
| `color` | Optional | `'primary' \| 'secondary'` | none | Color variant of the spinner |
| `type` | Optional | `'circle' \| 'square'` | none | Type of spinner animation |
| `speed` | Optional | `'slow' \| 'normal' \| 'fast'` | none | Speed of the animation |
| `label` | Optional | `string` | none | Optional label text displayed below the spinner |
| `fullScreen` | Optional | `boolean` | `false` | Display in fullscreen mode |
| `className` | Optional | `string` | none | Additional CSS class |

### Basic usage

```tsx
<Spinner />
<Spinner size="sm" />
<Spinner size="lg" />
<Spinner type="circle" speed="fast" />
<Spinner label="Loading miners..." />
```

### With content

```tsx
<Button disabled>
  <Spinner size="sm" /> Loading...
</Button>
```

### Styling

- `.mdk-spinner`: Root element
- `.mdk-spinner--{size}`: Size modifier

## `Loader`

<ComponentDescription name="Loader" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `count` | Optional | `3 \| 5 \| 7` | `5` | Number of dots to display |
| `color` | Optional | `'red' \| 'gray' \| 'blue' \| 'amber' \| 'orange'` | `'orange'` | Color variant of the loader |
| `size` | Optional | `number` | `10` | Size of each dot in pixels |
| `className` | Optional | `string` | none | Additional CSS class |

### Basic usage

```tsx
<Loader />
<Loader color="blue" />
<Loader count={3} size={8} color="amber" />
<Loader count={7} size={12} color="red" />
```

### In a container

```tsx
<div className="relative min-h-[200px]">
  {isLoading && <Loader />}
  {data && <DataDisplay data={data} />}
</div>
```

### Styling

- `.mdk-loader`: Root container
- `.mdk-loader__dot`: Individual dot
- `.mdk-loader__dot--{color}`: Dot color modifier

# Log components (/v0-4-0/ui/react/core/components/logs)

Components for displaying log entries, activity feeds, and event histories.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/core#prerequisites)

## `LogsCard`

<ComponentDescription name="LogsCard" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `logsData` | Required | `LogData[]` | `[]` | Array of log entries |
| `type` | Required | `string` | none | Log type identifier |
| `label` | Required | `string` | none | Card label |
| `isLoading` | Required | `boolean` | `false` | Show loading skeleton |
| `isDark` | Required | `boolean` | `false` | Dark theme variant |
| `emptyMessage` | Required | `string` | `'No incidents'` | Empty state message |
| `skeletonRows` | Required | `number` | `5` | Number of skeleton rows |
| `pagination` | Required | `LogPagination` | none | Pagination configuration |
| `onLogClicked` | Required | `function` | none | Log click callback |

### `LogData` type

```tsx
type LogData = {
  uuid: string
  body: string
  title: string
  status: string
  subtitle: string
}
```

### `LogPagination` type

```tsx
type LogPagination = {
  current: number
  total: number
  pageSize: number
  handlePaginationChange: (page: number) => void
}
```

### Basic usage

```tsx
const logs = [
  {
    uuid: '1',
    title: 'Miner Offline',
    subtitle: 'Site A - Rack 12',
    body: 'Miner 001 went offline at 14:32',
    status: 'error',
  },
  {
    uuid: '2',
    title: 'Hashrate Drop',
    subtitle: 'Site B - Rack 5',
    body: 'Hashrate dropped below threshold',
    status: 'warning',
  },
]

<LogsCard
  label="Recent Incidents"
  logsData={logs}
  type="incident"
  onLogClicked={(uuid) => handleLogClick(uuid)}
/>
```

### With pagination

```tsx
const [page, setPage] = useState(1)

<LogsCard
  label="Activity Log"
  logsData={logs}
  type="activity"
  pagination={{
    current: page,
    total: 100,
    pageSize: 10,
    handlePaginationChange: setPage,
  }}
/>
```

### Loading state

```tsx
<LogsCard
  label="Loading..."
  isLoading
  skeletonRows={5}
/>
```

### Styling

- `.mdk-logs-card`: Root container
- `.mdk-logs-card__inner-container`: Content wrapper
- `.mdk-logs-card__list-container`: Log list container
- `.mdk-logs-card__pagination`: Pagination area
- `.mdk-logs-card__empty`: Empty state container

## `LogRow`

<ComponentDescription name="LogRow" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `log` | Required | `LogData` | none | Log entry data |
| `type` | Required | `string` | none | Log type identifier |
| `style` | Optional | `CSSProperties` | none | Custom styles |
| `onLogClicked` | Optional | `function` | none | Click callback |

### Basic usage

```tsx
<LogRow
  log={{
    uuid: '1',
    title: 'System Alert',
    subtitle: 'High temperature detected',
    body: 'Rack 5 temperature exceeded 80°C',
    status: 'warning',
  }}
  type="alert"
  onLogClicked={(uuid) => viewDetails(uuid)}
/>
```

## `LogItem`

<ComponentDescription name="LogItem" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Required | `LogData` | none | Log entry data |
| `onLogClicked` | Optional | `function` | none | Click callback |

### Basic usage

```tsx
<LogItem
  data={{
    uuid: '1',
    title: 'Configuration Change',
    subtitle: 'Pool settings updated',
    body: 'Primary pool changed to us-east',
    status: 'info',
  }}
/>
```

## `LogDot`

<ComponentDescription name="LogDot" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `type` | Required | `string` | none | Log type identifier |
| `status` | Required | `string` | none | Status value |

### Basic usage

```tsx
<LogDot type="incident" status="error" />
<LogDot type="activity" status="success" />
<LogDot type="alert" status="warning" />
```

## `LogActivityIcon`

<ComponentDescription name="LogActivityIcon" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `status` | Required | `string` | none | Status value |

### Basic usage

```tsx
<LogActivityIcon status="online" />
<LogActivityIcon status="offline" />
<LogActivityIcon status="warning" />
```

## Status values

Log components support the following status values:

| Status | Color | Use case |
|--------|-------|----------|
| `success` | Green | Successful operations |
| `error` | Red | Errors, failures |
| `warning` | Yellow | Warnings, alerts |
| `info` | Blue | Informational |
| `default` | Gray | Neutral/unknown |

# Navigation (/v0-4-0/ui/react/core/components/navigation)

Components for navigating between views and organizing content.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/core#prerequisites)

## `AppHeader`

<ComponentDescription name="AppHeader" />

### Import

```tsx
```

### Props

`AppHeader` is a three-slot top-bar shell.

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `logo` | Optional | `ReactNode` | none | Left-most slot — typically the app's brand lockup / logo |
| `start` | Optional | `ReactNode` | none | Left-edge content — e.g. a sidebar collapse toggle button |
| `children` | Optional | `ReactNode` | none | Middle slot — typically the dashboard's stats strip |
| `actions` | Optional | `ReactNode` | none | Right-edge action cluster — e.g. alarms bell, profile menu |
| `sticky` | Optional | `boolean` | `true` | Render the header sticky to the top of its scroll container |
| `className` | Optional | `string` | none | Class hook for the outer `<header>` element |

### Basic usage

```tsx
<AppHeader
  start={<strong>MDK</strong>}
  actions={<Button size="sm">Sign out</Button>}
>
  <div>Header content</div>
</AppHeader>
```

## `Sidebar`

<ComponentDescription name="Sidebar" />

### Import

```tsx
  Sidebar,
  useSidebarExpandedState,
  useSidebarSectionState,
  clearSidebarState,
} from '@tetherto/mdk-react-devkit/core'
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `items` | Required | `SidebarMenuItem[]` | none | **Required.** Menu items array |
| `activeId` | Required | `string` | none | Currently active item ID |
| `expanded` | Required | `boolean` | none | Controlled expanded state |
| `defaultExpanded` | Optional | `boolean` | `false` | Initial expanded state |
| `visible` | Optional | `boolean` | `true` | Sidebar visibility |
| `overlay` | Optional | `boolean` | `false` | Overlay mode (mobile) |
| `header` | Optional | `ReactNode` | none | Header content |
| `onItemClick` | Required | `function` | none | Item click callback |
| `onExpandedChange` | Required | `function` | none | Expand state callback |
| `onClose` | Required | `function` | none | Close callback (overlay mode) |

### `SidebarMenuItem` type

```tsx
type SidebarMenuItem = {
  id: string
  label: string
  icon?: ReactNode
  href?: string
  children?: SidebarMenuItem[]
  disabled?: boolean
}
```

### Basic usage

```tsx
const items = [
  { id: 'dashboard', label: 'Dashboard', icon: <DashboardIcon /> },
  { id: 'settings', label: 'Settings', icon: <SettingsIcon /> },
  {
    id: 'reports',
    label: 'Reports',
    icon: <ReportsIcon />,
    children: [
      { id: 'daily', label: 'Daily' },
      { id: 'weekly', label: 'Weekly' },
    ],
  },
]

<Sidebar
  items={items}
  activeId="dashboard"
  onItemClick={(item) => navigate(item.href)}
/>
```

### With persisted state

```tsx
function App() {
  const [expanded, setExpanded] = useSidebarExpandedState(true)
  
  return (
    <Sidebar
      items={items}
      expanded={expanded}
      onExpandedChange={setExpanded}
    />
  )
}
```

### Styling

- `.mdk-sidebar`: Root container
- `.mdk-sidebar--expanded`: Expanded state
- `.mdk-sidebar--hidden`: Hidden state
- `.mdk-sidebar--overlay`: Overlay mode
- `.mdk-sidebar__toggle`: Expand/collapse button
- `.mdk-sidebar__menu`: Menu container
- `.mdk-sidebar__backdrop`: Overlay backdrop

## `Tabs`

<ComponentDescription name="Tabs" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `defaultValue` | Optional | `string` | none | Initially active tab |
| `value` | Optional | `string` | none | Controlled active tab |
| `onValueChange` | Optional | `function` | none | Tab change callback |
| `variant` | Optional | `'default' \| 'side'` | `'default'` | Tab layout variant |

### Basic usage

```tsx
<Tabs defaultValue="overview">
  <TabsList>
    <TabsTrigger value="overview">Overview</TabsTrigger>
    <TabsTrigger value="settings">Settings</TabsTrigger>
    <TabsTrigger value="logs" disabled>Logs</TabsTrigger>
  </TabsList>
  <TabsContent value="overview">
    Overview content here
  </TabsContent>
  <TabsContent value="settings">
    Settings content here
  </TabsContent>
</Tabs>
```

### Side variant

```tsx
<Tabs defaultValue="tab1" variant="side">
  <TabsList variant="side">
    <TabsTrigger value="tab1" variant="side">Tab 1</TabsTrigger>
    <TabsTrigger value="tab2" variant="side">Tab 2</TabsTrigger>
  </TabsList>
  <TabsContent value="tab1">Content 1</TabsContent>
  <TabsContent value="tab2">Content 2</TabsContent>
</Tabs>
```

### Styling

- `.mdk_tabs`: Root container
- `.mdk_tabs__list`: Tab button container
- `.mdk_tabs__list--side`: Side variant list
- `.mdk_tabs__trigger`: Individual tab button
- `.mdk_tabs__content`: Tab panel content

## `LazyTabWrapper`

<ComponentDescription name="LazyTabWrapper" />

### Import

```tsx
```

### Usage

```tsx
<TabsContent value="heavy-content">
  <LazyTabWrapper>
    <ExpensiveComponent />
  </LazyTabWrapper>
</TabsContent>
```

## `Breadcrumbs`

<ComponentDescription name="Breadcrumbs" />

### Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `items` | Required | `BreadcrumbItem[]` | none | Breadcrumb entries to render; each item is `{ label, href?, onClick? }` |
| `showBack` | Optional | `boolean` | `false` | Render a back button before the breadcrumb trail |
| `backLabel` | Optional | `string` | `'Back'` | Accessible label / text for the back button |
| `onBackClick` | Optional | `VoidFunction` | none | Callback fired when the back button is clicked |
| `separator` | Optional | `ReactNode` | `'/'` | Custom node rendered between items |
| `itemClassName` | Optional | `string` | none | Custom class name applied to each breadcrumb item |
| `backClassName` | Optional | `string` | none | Custom class name applied to the back button |
| `className` | Optional | `string` | none | Custom class name for the root element |

### Basic usage

```tsx
const items: BreadcrumbItem[] = [
  { label: 'Home', href: '/' },
  { label: 'Miners', href: '/miners' },
  { label: 'Details' },
]

<Breadcrumbs items={items} />
```

### Styling

- `.mdk-breadcrumbs`: Root container
- `.mdk-breadcrumbs__item`: Individual item
- `.mdk-breadcrumbs__separator`: Separator between items

# Overlays (/v0-4-0/ui/react/core/components/overlays)

Overlay components display content above the main interface. All components are built on [Radix UI](https://www.radix-ui.com/) primitives for accessibility.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/core#prerequisites)

## `Dialog`

<ComponentDescription name="Dialog" />

### Import

```tsx
  Dialog,
  DialogTrigger,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogFooter,
  DialogClose,
} from '@tetherto/mdk-react-devkit/core'
```

## `DialogContent` props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `title` | Optional | `string` | none | Dialog title |
| `description` | Optional | `string` | none | Dialog description |
| `closable` | Optional | `boolean` | `false` | Show close button |
| `onClose` | Optional | `function` | none | Close callback |
| `closeOnClickOutside` | Optional | `boolean` | `true` | Close when clicking outside |
| `closeOnEscape` | Optional | `boolean` | `true` | Close on Escape key |
| `bare` | Optional | `boolean` | `false` | Minimal header styling |

## Basic usage

```tsx
<Dialog>
  <DialogTrigger asChild>
    <Button>Open Dialog</Button>
  </DialogTrigger>
  <DialogContent title="Confirm Action" closable onClose={() => {}}>
    <p>Are you sure you want to proceed?</p>
    <DialogFooter>
      <DialogClose asChild>
        <Button variant="secondary">Cancel</Button>
      </DialogClose>
      <Button>Confirm</Button>
    </DialogFooter>
  </DialogContent>
</Dialog>
```

## Styling

- `.mdk-dialog__overlay`: Background overlay
- `.mdk-dialog__content`: Dialog container
- `.mdk-dialog__header`: Header area
- `.mdk-dialog__title`: Title text
- `.mdk-dialog__description`: Description text
- `.mdk-dialog__footer`: Footer area

## `DropdownMenu`

<ComponentDescription name="DropdownMenu" />

### Import

```tsx
  DropdownMenu,
  DropdownMenuTrigger,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuLabel,
  DropdownMenuGroup,
} from '@tetherto/mdk-react-devkit/core'
```

### Basic usage

```tsx
<DropdownMenu>
  <DropdownMenuTrigger asChild>
    <Button>Options</Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent>
    <DropdownMenuLabel>Actions</DropdownMenuLabel>
    <DropdownMenuItem onClick={() => {}}>Edit</DropdownMenuItem>
    <DropdownMenuItem onClick={() => {}}>Duplicate</DropdownMenuItem>
    <DropdownMenuSeparator />
    <DropdownMenuItem onClick={() => {}} className="text-red-500">
      Delete
    </DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>
```

### Styling

- `.mdk-dropdown-menu__content`: Menu container
- `.mdk-dropdown-menu__item`: Menu item
- `.mdk-dropdown-menu__label`: Label text
- `.mdk-dropdown-menu__separator`: Divider line

## `Popover`

<ComponentDescription name="Popover" />

### Import

```tsx
  Popover,
  PopoverTrigger,
  PopoverContent,
} from '@tetherto/mdk-react-devkit/core'
```

#### Basic usage

```tsx
<Popover>
  <PopoverTrigger asChild>
    <Button>Show Details</Button>
  </PopoverTrigger>
  <PopoverContent>
    <p>Additional information goes here.</p>
  </PopoverContent>
</Popover>
```

### Styling

- `.mdk-popover__content`: Popover container

## `Toast`

<ComponentDescription name="Toast" />

### Import

```tsx
```

### `Toast` props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `title` | Required | `string` | none | **Required.** Toast title |
| `description` | Optional | `string` | none | Additional description |
| `variant` | Optional | `'success' \| 'error' \| 'warning' \| 'info'` | `'info'` | Toast variant |
| `icon` | Optional | `ReactElement` | none | Custom icon |

### `ToastViewport` props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `position` | Optional | `ToastPosition` | `'bottom-right'` | Toast position |

Available positions: `'top-left'`, `'top-center'`, `'top-right'`, `'bottom-left'`, `'bottom-center'`, `'bottom-right'`

### Setup

```tsx

function App() {
  return (
    <ToastProvider>
      {/* Your app content */}
      <ToastViewport position="bottom-right" />
    </ToastProvider>
  )
}
```

### Basic usage

```tsx

function MyComponent() {
  const [open, setOpen] = useState(false)

  return (
    <>
      <Button onClick={() => setOpen(true)}>Show Toast</Button>
      <Toast
        open={open}
        onOpenChange={setOpen}
        title="Success!"
        description="Your changes have been saved."
        variant="success"
      />
    </>
  )
}
```

### Styling

- `.mdk_toast`: Toast container
- `.mdk_toast__header`: Header section
- `.mdk_toast__title`: Title text
- `.mdk_toast__description`: Description text
- `.mdk_toast__close`: Close button

## `Tooltip`

<ComponentDescription name="Tooltip" />

### Import

```tsx
  Tooltip,
  TooltipTrigger,
  TooltipContent,
  TooltipProvider,
} from '@tetherto/mdk-react-devkit/core'
```

### Basic usage

```tsx
<TooltipProvider>
  <Tooltip>
    <TooltipTrigger asChild>
      <Button variant="icon">
        <InfoIcon />
      </Button>
    </TooltipTrigger>
    <TooltipContent>
      Additional information
    </TooltipContent>
  </Tooltip>
</TooltipProvider>
```

### Styling

- `.mdk-tooltip__content`: Tooltip container

# Icons (/v0-4-0/ui/react/core/icons)

The `@tetherto/mdk-react-devkit/core` package includes **70+ icons** to simplify development of Bitcoin mining applications.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/core#prerequisites)

## Import

```tsx
// Import individual icons

// Import the factory function

// Import types
```

### Icon props

All icons accept the `IconProps` interface:

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `size` | Optional | `number \| string` | `24` | Sets both width and height |
| `color` | Optional | `string` | `'currentColor'` | Icon color (single-color icons only) |
| `width` | Optional | `number \| string` | none | Override width |
| `height` | Optional | `number \| string` | none | Override height |
| `className` | Optional | `string` | none | CSS class |
| `style` | Optional | `CSSProperties` | none | Inline styles |

### Basic usage

```tsx

// Default size (24px)
<DashboardNavIcon />

// Custom size
<HashrateStatIcon size={32} />

// Custom color
<MiningStatusIcon color="#72F59E" />

// With className
<SettingsNavIcon className="sidebar-icon" />
```

## Available icons

### Navigation icons

Icons for sidebar and navigation menus:

| Icon | Name | Description |
|------|------|-------------|
| ![dashboard-nav-icon](/images/ui-kit/icons/dashboard-nav-icon.svg) | `DashboardNavIcon` | Dashboard/home |
| <DocIcon src="/images/ui-kit/icons/farms-nav-icon.svg" scale={75} /> | `FarmsNavIcon` | Mining farms |
| ![financials-nav-icon](/images/ui-kit/icons/financials-nav-icon.svg) | `FinancialsNavIcon` | Financial reports |
| ![inventory-nav-icon](/images/ui-kit/icons/inventory-nav-icon.svg) | `InventoryNavIcon` | Equipment inventory |
| ![miners-overview-nav-icon](/images/ui-kit/icons/miners-overview-nav-icon.svg) | `MinersOverviewNavIcon` | Miner overview |
| ![operations-nav-icon](/images/ui-kit/icons/operations-nav-icon.svg) | `OperationsNavIcon` | Operations |
| ![pool-manager-nav-icon](/images/ui-kit/icons/pool-manager-nav-icon.svg) | `PoolManagerNavIcon` | Pool Manager |
| ![reporting-nav-icon](/images/ui-kit/icons/reporting-nav-icon.svg) | `ReportingNavIcon` | Reporting |
| ![settings-nav-icon](/images/ui-kit/icons/settings-nav-icon.svg) | `SettingsNavIcon` | Settings |
| ![user-management-nav-icon](/images/ui-kit/icons/user-management-nav-icon.svg) | `UserManagementNavIcon` | User management |
| ![explorer-nav-icon](/images/ui-kit/icons/explorer-nav-icon.svg) | `ExplorerNavIcon` | Explorer |
| ![alerts-nav-icon](/images/ui-kit/icons/alerts-nav-icon.svg) | `AlertsNavIcon` | Alerts |
| ![cabinet-widget-nav-icon](/images/ui-kit/icons/cabinet-widget-nav-icon.svg) | `CabinetWidgetNavIcon` | Cabinet widgets |
| ![container-widgets-nav-icon](/images/ui-kit/icons/container-widgets-nav-icon.svg) | `ContainerWidgetsNavIcon` | Container widgets |

### Status icons

Icons for displaying operational status:

| Icon | Name | Description |
|------|------|-------------|
| ![mining-status-icon](/images/ui-kit/icons/mining-status-icon.svg) | `MiningStatusIcon` | Active mining |
| ![offline-status-icon](/images/ui-kit/icons/offline-status-icon.svg) | `OfflineStatusIcon` | Offline |
| ![sleep-status-icon](/images/ui-kit/icons/sleep-status-icon.svg) | `SleepStatusIcon` | Sleep/standby mode |
| <DocIcon src="/images/ui-kit/icons/error-status-icon.svg" scale={50} /> | `ErrorStatusIcon` | Error state |
| ![live-icon](/images/ui-kit/icons/live-icon.svg) | `LiveIcon` | Live/active |

{/* OfflineIcon source file exists but isn't exported from @tetherto/mdk-react-devkit/core (upstream barrel gap) — pending fix
| ![offline-icon](/images/ui-kit/icons/offline-icon.svg) | `OfflineIcon` | Offline indicator |
*/}

### Alarm icons

Icons for alerts and alarms:

| Icon | Name | Description |
|------|------|-------------|
| ![temperature-alarm-icon](/images/ui-kit/icons/temperature-alarm-icon.svg) | `TemperatureAlarmIcon` | Temperature alarm |
| ![pressure-alarm-icon](/images/ui-kit/icons/pressure-alarm-icon.svg) | `PressureAlarmIcon` | Pressure alarm |
| ![fluid-alarm-icon](/images/ui-kit/icons/fluid-alarm-icon.svg) | `FluidAlarmIcon` | Fluid/coolant alarm |
| ![offline-alarm-icon](/images/ui-kit/icons/offline-alarm-icon.svg) | `OfflineAlarmIcon` | Offline alarm |
| ![other-alarm-icon](/images/ui-kit/icons/other-alarm-icon.svg) | `OtherAlarmIcon` | Generic alarm |
| ![alert-triangle-icon](/images/ui-kit/icons/alert-triangle-icon.svg) | `AlertTriangleIcon` | Warning triangle |

### Weather icons

Icons for environmental conditions:

{/*CloudyIcon SVG is identical to SunnyIcon (upstream bug) - using emoji placeholder
| <span className="doc-icon doc-icon-tall" style={{'--icon-scale': 0.75}}><img src="/images/ui-kit/icons/cloudy-icon.svg" alt="cloudy-icon" /></span> | `CloudyIcon` | Cloudy |
*/}

| Icon | Name | Description |
|------|------|-------------|
| <span className="doc-icon doc-icon-tall" style={{'--icon-scale': 0.75}}><img src="/images/ui-kit/icons/sunny-icon.svg" alt="sunny-icon" /></span> | `SunnyIcon` | Sunny/clear |
| ⛅ | `CloudyIcon` | Cloudy |
| <span className="doc-icon doc-icon-tall" style={{'--icon-scale': 0.75}}><img src="/images/ui-kit/icons/rainy-icon.svg" alt="rainy-icon" /></span> | `RainyIcon` | Rain |
| <span className="doc-icon doc-icon-tall" style={{'--icon-scale': 0.75}}><img src="/images/ui-kit/icons/snowy-icon.svg" alt="snowy-icon" /></span> | `SnowyIcon` | Snow |
| <span className="doc-icon doc-icon-tall" style={{'--icon-scale': 0.75}}><img src="/images/ui-kit/icons/partly-cloudy-icon.svg" alt="partly-cloudy-icon" /></span> | `PartlyCloudyIcon` | Partly cloudy |
| <span className="doc-icon doc-icon-tall" style={{'--icon-scale': 0.75}}><img src="/images/ui-kit/icons/rain-thunder-icon.svg" alt="rain-thunder-icon" /></span> | `RainThunderIcon` | Thunderstorm |

### Mining/stats icons

Icons for mining metrics and statistics:

| Icon | Name | Description |
|------|------|-------------|
| ![hashrate-card-icon](/images/ui-kit/icons/hashrate-card-icon.svg) | `HashrateCardIcon` | Hashrate display |
| ![hashrate-stat-icon](/images/ui-kit/icons/hashrate-stat-icon.svg) | `HashrateStatIcon` | Hashrate statistic |
| ![miners-stat-icon](/images/ui-kit/icons/miners-stat-icon.svg) | `MinersStatIcon` | Miner count |
| ![farm-star-icon](/images/ui-kit/icons/farm-star-icon.svg) | `FarmStarIcon` | Farm highlight |
| ![farm-alert-icon](/images/ui-kit/icons/farm-alert-icon.svg) | `FarmAlertIcon` | Farm alert |
| ![miner-overview-icon](/images/ui-kit/icons/miner-overview-icon.svg) | `MinerOverviewIcon` | Miner overview |
| ![miner-explorer-icon](/images/ui-kit/icons/miner-explorer-icon.svg) | `MinerExplorerIcon` | Miner explorer |
| ![pools-icon](/images/ui-kit/icons/pools-icon.svg) | `PoolsIcon` | Mining pools |
| <span className="doc-icon doc-icon-tall" style={{'--icon-scale': 0.75}}><img src="/images/ui-kit/icons/production-data-icon.svg" alt="production-data-icon" /></span> | `ProductionDataIcon` | Production data |

### Measurement icons

Icons for measurements and readings:

| Icon | Name | Description |
|------|------|-------------|
| ![power-icon](/images/ui-kit/icons/power-icon.svg) | `PowerIcon` | Power consumption |
| ![fan-icon](/images/ui-kit/icons/fan-icon.svg) | `FanIcon` | Fan/cooling |
| ![frequency-icon](/images/ui-kit/icons/frequency-icon.svg) | `FrequencyIcon` | Frequency |
| ![efficiency-icon](/images/ui-kit/icons/efficiency-icon.svg) | `EfficiencyIcon` | Efficiency |
| ![consumption-icon](/images/ui-kit/icons/consumption-icon.svg) | `ConsumptionIcon` | Consumption |
| ![temperature-celsius-icon](/images/ui-kit/icons/temperature-celsius-icon.svg) | `TemperatureCelsiusIcon` | Temperature (°C) |
| ![temperature-indicator-icon](/images/ui-kit/icons/temperature-indicator-icon.svg) | `TemperatureIndicatorIcon` | Temperature indicator |
| <span className="doc-icon doc-icon-tall" style={{'--icon-scale': 0.75}}><img src="/images/ui-kit/icons/temperature-weather-icon.svg" alt="temperature-weather-icon" /></span> | `TemperatureWeatherIcon` | Temperature/weather |
| ![cooling-drop-icon](/images/ui-kit/icons/cooling-drop-icon.svg) | `CoolingDropIcon` | Cooling/liquid |

### UI icons

General UI icons:

| Icon | Name | Description |
|------|------|-------------|
| ![arrow-icon](/images/ui-kit/icons/arrow-icon.svg) | `ArrowIcon` | Arrow (direction) |
| ![right-arrow-icon](/images/ui-kit/icons/right-arrow-icon.svg) | `RightArrowIcon` | Right arrow |
| ![right-navigate-icon](/images/ui-kit/icons/right-navigate-icon.svg) | `RightNavigateIcon` | Navigate right |
| <DocIcon src="/images/ui-kit/icons/pin-icon.svg" scale={50} /> | `PinIcon` | Pin/favorite |
| <DocIcon src="/images/ui-kit/icons/unpin-icon.svg" scale={50} /> | `UnPinIcon` | Unpin |
| ![menu-icon](/images/ui-kit/icons/menu-icon.svg) | `MenuIcon` | Menu hamburger |
| ![export-icon](/images/ui-kit/icons/export-icon.svg) | `ExportIcon` | Export data |
| ![google-icon](/images/ui-kit/icons/google-icon.svg) | `GoogleIcon` | Google logo |
| ![notification-bell-icon](/images/ui-kit/icons/notification-bell-icon.svg) | `NotificationBellIcon` | Notifications |
| ![settings-header-icon](/images/ui-kit/icons/settings-header-icon.svg) | `SettingsHeaderIcon` | Settings gear |
| ![sign-out-icon](/images/ui-kit/icons/sign-out-icon.svg) | `SignOutIcon` | Sign out |
| ![user-avatar-icon](/images/ui-kit/icons/user-avatar-icon.svg) | `UserAvatarIcon` | User avatar |
| ![volume-on-icon](/images/ui-kit/icons/volume-on-icon.svg) | `VolumeOnIcon` | Volume on |
| ![volume-off-icon](/images/ui-kit/icons/volume-off-icon.svg) | `VolumeOffIcon` | Volume off |
| ![comment-icon](/images/ui-kit/icons/comment-icon.svg) | `CommentIcon` | Comment |
| ![comment-icon-common](/images/ui-kit/icons/comment-icon-common.svg) | `CommentIconCommon` | Comment (common variant) |
| ![increase-icon](/images/ui-kit/icons/increase-icon.svg) | `IncreaseIcon` | Increase/up |
| ![decrease-icon](/images/ui-kit/icons/decrease-icon.svg) | `DecreaseIcon` | Decrease/down |
| ![profit-arrow-icon](/images/ui-kit/icons/profit-arrow-icon.svg) | `ProfitArrowIcon` | Profit indicator |
| ![btc-card-icon](/images/ui-kit/icons/btc-card-icon.svg) | `BtcCardIcon` | Bitcoin card |
| ![mode-icon](/images/ui-kit/icons/mode-icon.svg) | `ModeIcon` | Mode toggle |
| ![scale-control-icon](/images/ui-kit/icons/scale-control-icon.svg) | `ScaleControlIcon` | Scale control |
| ![site-overview-icon](/images/ui-kit/icons/site-overview-icon.svg) | `SiteOverviewIcon` | Site overview |
| ![actions-tick-icon](/images/ui-kit/icons/actions-tick-icon.svg) | `ActionsTickIcon` | Action complete |

## Create custom icons

Use `createIcon` to create custom icons following the same pattern:

```tsx

  displayName: 'MyCustomIcon',
  viewBox: '0 0 24 24',
  defaultWidth: 24,
  defaultHeight: 24,
  path: (
    <path
      d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
    />
  ),
})
```

### `CreateIconOptions`

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `displayName` | `string` | none | **Required:** Component display name |
| `viewBox` | `string` | none | **Required:** SVG `viewBox` |
| `defaultWidth` | `number` | `24` | Default width |
| `defaultHeight` | `number` | `24` | Default height |
| `multiColor` | `boolean` | `false` | Multi-color icon flag |
| `path` | `ReactNode \| function` | none | **Required:** SVG path content |

### Dynamic color icons

For icons that need dynamic colors:

```tsx
  displayName: 'DynamicColorIcon',
  viewBox: '0 0 24 24',
  path: ({ color }) => (
    <circle cx="12" cy="12" r="10" fill={color} />
  ),
})
```

# Theme (/v0-4-0/ui/react/core/theme)

The `@tetherto/mdk-react-devkit/core` package provides a theme system with design tokens and utilities for consistent styling across your application.

Import `@tetherto/mdk-react-devkit/styles.css` once in your app entry (before your own CSS). 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; }
}
```

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/core#prerequisites)

## Import

```tsx
// Import tokens

// Import utilities
  getSystemTheme,
  applyTheme,
  getStoredTheme,
  setStoredTheme,
} from '@tetherto/mdk-react-devkit/core'

// Import types
```

## Design tokens

### Colors

Primary and gray color scales with 10 shades each (50-900).

```tsx

colors.primary[500]  // 'hsl(222, 47%, 50%)'
colors.gray[900]     // 'hsl(222, 47%, 11%)'
```

#### Primary scale

| Token | Value | Usage |
|-------|-------|-------|
| `primary.50` | `hsl(222, 47%, 95%)` | Lightest background |
| `primary.100` | `hsl(222, 47%, 90%)` | Light background |
| `primary.200` | `hsl(222, 47%, 80%)` | Hover states |
| `primary.300` | `hsl(222, 47%, 70%)` | Borders |
| `primary.400` | `hsl(222, 47%, 60%)` | Secondary text |
| `primary.500` | `hsl(222, 47%, 50%)` | Primary color |
| `primary.600` | `hsl(222, 47%, 40%)` | Hover on primary |
| `primary.700` | `hsl(222, 47%, 30%)` | Active states |
| `primary.800` | `hsl(222, 47%, 20%)` | Dark backgrounds |
| `primary.900` | `hsl(222, 47%, 11.2%)` | Darkest |

#### Gray scale

| Token | Value | Usage |
|-------|-------|-------|
| `gray.50` | `hsl(210, 40%, 98%)` | Page background |
| `gray.100` | `hsl(210, 40%, 96.1%)` | Card background |
| `gray.200` | `hsl(214, 32%, 91.4%)` | Borders |
| `gray.300` | `hsl(213, 27%, 84.1%)` | Dividers |
| `gray.400` | `hsl(215, 20%, 65.1%)` | Placeholder text |
| `gray.500` | `hsl(215, 16%, 47%)` | Secondary text |
| `gray.600` | `hsl(215, 19%, 35%)` | Body text |
| `gray.700` | `hsl(215, 25%, 27%)` | Headings |
| `gray.800` | `hsl(217, 33%, 17%)` | Dark mode card |
| `gray.900` | `hsl(222, 47%, 11%)` | Dark mode `bg` |

### Spacing

Consistent spacing scale for margins, padding, and gaps.

```tsx

spacing[4]   // '1rem' (16px)
spacing[8]   // '2rem' (32px)
```

| Token | Value | Pixels |
|-------|-------|--------|
| `0` | `0` | 0px |
| `1` | `0.25rem` | 4px |
| `2` | `0.5rem` | 8px |
| `3` | `0.75rem` | 12px |
| `4` | `1rem` | 16px |
| `5` | `1.25rem` | 20px |
| `6` | `1.5rem` | 24px |
| `8` | `2rem` | 32px |
| `10` | `2.5rem` | 40px |
| `12` | `3rem` | 48px |
| `16` | `4rem` | 64px |
| `20` | `5rem` | 80px |
| `24` | `6rem` | 96px |

### Border radius

Border radius scale for rounded corners.

```tsx

borderRadius.md    // '0.375rem'
borderRadius.full  // '9999px'
```

| Token | Value |
|-------|-------|
| `none` | `0` |
| `sm` | `0.125rem` |
| `DEFAULT` | `0.25rem` |
| `md` | `0.375rem` |
| `lg` | `0.5rem` |
| `xl` | `0.75rem` |
| `2xl` | `1rem` |
| `full` | `9999px` |

### Font size

Font size scale for typography.

```tsx

fontSize.base  // '1rem'
fontSize.lg    // '1.125rem'
```

| Token | Value |
|-------|-------|
| `xs` | `0.75rem` |
| `sm` | `0.875rem` |
| `base` | `1rem` |
| `lg` | `1.125rem` |
| `xl` | `1.25rem` |
| `2xl` | `1.5rem` |
| `3xl` | `1.875rem` |
| `4xl` | `2.25rem` |
| `5xl` | `3rem` |

## Theme utilities

### Theme type

```tsx
type Theme = 'light' | 'dark' | 'system'
```

### `getSystemTheme`

Get the user's system theme preference.

```tsx

const systemTheme = getSystemTheme()  // 'light' or 'dark'
```

### `applyTheme`

Apply a theme to the document root.

```tsx

applyTheme('dark')   // Sets dark mode
applyTheme('light')  // Sets light mode
applyTheme('system') // Uses system preference
```

This function:
- Removes existing `light`/`dark` classes from `<html>`
- Adds the resolved theme class
- Sets the `color-scheme` CSS property

### `getStoredTheme`

Get the stored theme from `localStorage`.

```tsx

const storedTheme = getStoredTheme()  // Theme | null
const customKey = getStoredTheme('my-theme-key')
```

### `setStoredTheme`

Store the theme in `localStorage`.

```tsx

setStoredTheme('dark')
setStoredTheme('light', 'my-theme-key')  // Custom storage key
```

## Usage example

```tsx
  applyTheme,
  getStoredTheme,
  setStoredTheme,
  getSystemTheme,
} from '@tetherto/mdk-react-devkit/core'

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState<Theme>(() => {
    return getStoredTheme() || 'system'
  })

  useEffect(() => {
    applyTheme(theme)
    setStoredTheme(theme)
  }, [theme])

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}
```

## CSS custom properties

The theme system uses CSS custom properties that can be overridden:

```css
:root {
  --mdk-color-primary: #f7931a;
  --mdk-color-background: hsl(210, 40%, 98%);
  --mdk-color-foreground: hsl(222, 47%, 11%);
  /* ... more properties */
}

.dark {
  --mdk-color-background: hsl(222, 47%, 11%);
  --mdk-color-foreground: hsl(210, 40%, 98%);
}
```

# Explore the demo (/v0-4-0/ui/react/explore-the-demo)

This page presents a copy/paste method to clone the MDK UI monorepo, run the demo, and browse the full component library live.

## Prerequisites

- **Node.js** >=24
- **npm** >=11
- **React** 19+ and **react-dom** 19+

{/* npm packages are not yet published to the registry; clone the MDK UI monorepo and use workspace dependencies until publish. */}

## Run the demo

One copy/paste: four steps — clone, install, build, run.

<Tabs>
  <Tab value="ssh" label="SSH" default>

```bash
# Requires an SSH key configured for your GitHub account
git clone git@github.com:tetherto/mdk.git
cd mdk/ui
npm install && npm run build
npm run dev:catalog
```

  </Tab>
  <Tab value="https" label="HTTPS">

```bash
git clone https://github.com/tetherto/mdk.git
cd mdk/ui
npm install && npm run build
npm run dev:catalog
```

  </Tab>
</Tabs>

Open [http://localhost:5173/mdk](http://localhost:5173/mdk). You'll land in the demo browser — a sidebar of the full MDK component library, covering core
primitives (buttons, inputs, tables, charts, dialogs) and mining-domain components (operations centre, vendor containers, alerts, pool manager, settings).

### What the demo wires

The demo app uses [`@tetherto/mdk-ui-core`](/v0-4-0/reference/app-toolkit/ui-core), `@tetherto/mdk-react-adapter`, and `@tetherto/mdk-react-devkit` with `<MdkProvider>`.

## Next steps

- [Quickstart](/v0-4-0/ui/react/quickstart): install all three packages and wrap your app in `MdkProvider`
- [Wire a React app](/v0-4-0/tutorials/ui/react/tutorial): scaffold an app with `MdkProvider` and adapter hooks
- [Learn more about the React MDK UI Kit](/v0-4-0/ui/react/get-started)

# @tetherto/mdk-react-devkit (/v0-4-0/ui/react/foundation)

The [foundation entrypoint](/v0-4-0/resources/repositories) (`@tetherto/mdk-react-devkit/foundation`) provides **domain-specific React components, 
hooks, and constants** assembled from [`@tetherto/mdk-react-devkit/core`](/v0-4-0/ui/react/core). These are higher-level blocks for common 
Bitcoin mining application use cases.

<Callout type="info">
Generic primitives (buttons, tables, charts) import from [`/core`](/v0-4-0/ui/react/core). Both subpaths ship from `@tetherto/mdk-react-devkit`. 
Connected foundation components expect `<MdkProvider>` from [`@tetherto/mdk-react-adapter`](/v0-4-0/ui/react/get-started) and adapter store hooks.
</Callout>

## Prerequisites

- Complete the installation

<Accordions>
  <Accordion title="Show steps">
    ```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
```

  </Accordion>
</Accordions>

- Add the dependency to your app's `package.json`

<Tabs>
  <Tab value="workspace" label="Monorepo workspace" default>

```json
{
  "dependencies": {
    "@tetherto/mdk-react-devkit": "*",
    "@tetherto/mdk-react-adapter": "*",
    "@tetherto/mdk-ui-core": "*"
  }
}
```

  </Tab>
  <Tab value="npm" label="🚧 Registry install">

> **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-core
```

  </Tab>
</Tabs>

Run `npm install` from the `mdk/ui` workspace root after your app is under `apps/` so npm links workspace packages.

## What's included

| Feature | Description |
|---------|-------------|
| [Operations centre](/v0-4-0/ui/react/foundation/operations) | Bitcoin mining operations monitoring and management components |
| [Reporting tool](/v0-4-0/ui/react/foundation/reporting) | Operational reporting surfaces |
| [Settings](/v0-4-0/ui/react/foundation/settings) | Administrative settings views |
| [Hooks](#hooks) | Reusable React hooks |
| [Constants](#constants) | Shared constants and configurations |

### Operations centre

Domain-specific components for Bitcoin mining operations monitoring and management, including device explorers, vendor container UIs, dashboard widgets, 
charts, pool management, and data export.

See the [operations centre reference](/v0-4-0/ui/react/foundation/operations) for the full component list with demo links.

### Settings

Pre-built settings UI for common administrative tasks:

- [`SettingsDashboard`](/v0-4-0/ui/react/foundation/settings): main settings container with accordion sections
- [`FeatureFlagsSettings`](/v0-4-0/ui/react/foundation/settings/feature-flags): toggle feature flags on/off
- [`HeaderControlsSettings`](/v0-4-0/ui/react/foundation/settings/header-controls): configure header display options
- [`ImportExportSettings`](/v0-4-0/ui/react/foundation/settings/import-export): import/export configuration data
- [`RBACControlSettings`](/v0-4-0/ui/react/foundation/settings/access-control): role-based access control settings
- [User management modals](/v0-4-0/ui/react/foundation/settings/user-management): add, manage, and confirm user changes

### Hooks

Reusable React hooks for [notifications and shell layout](/v0-4-0/reference/app-toolkit/hooks/components#notifications) and 
[chart/dashboard transforms](/v0-4-0/reference/app-toolkit/hooks/components#charts) — see the full [Hooks reference](/v0-4-0/reference/app-toolkit/hooks).

### Constants

Shared [constants](/v0-4-0/reference/app-toolkit/ui-kit/constants#foundation-constants) for consistency across your application: permission definitions, 
role configurations, settings defaults, and error codes.

## Import examples

```tsx
// Import components

// Import from domain subpath when you only need settings-domain exports
```

## Next steps

Consider the detailed [reference material](/v0-4-0/reference) for the foundation entrypoint:

- [Constants](/v0-4-0/reference/app-toolkit/ui-kit/constants#foundation-constants): permissions, roles, header controls, settings error codes
- [Hooks: Components](/v0-4-0/reference/app-toolkit/hooks/components) and [Hooks: Utilities > Domain transforms](/v0-4-0/reference/app-toolkit/hooks/utilities#domain-transforms)
- [Types](/v0-4-0/reference/app-toolkit/ui-kit/types#foundation-types): [`Device`](/v0-4-0/reference/app-toolkit/ui-kit/types#device), [`Container`](/v0-4-0/reference/app-toolkit/ui-kit/types#container), [`Alert`](/v0-4-0/reference/app-toolkit/ui-kit/types#alert), [`MinerStats`](/v0-4-0/reference/app-toolkit/ui-kit/types#minerstats), settings shapes
- [Utilities](/v0-4-0/reference/app-toolkit/ui-kit/utilities#foundation-utilities): `settings-utils` (filtering, formatting, import/export)

# Alerts (/v0-4-0/ui/react/foundation/alerts)

<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Alerts components cover the full alarm surface: `AlarmsBellButton` in the app header, the `Alerts` feature composite 
(current and historical tabs), and the standalone `HistoricalAlerts` table. Pair with the [dashboard](/v0-4-0/ui/react/foundation/dashboard) 
for live context.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- Wrap your app in `<MdkProvider>` — see [foundation prerequisites](/v0-4-0/ui/react/foundation#prerequisites)

## Components

<ComponentTable category="Alerts" pkg="foundation" />

### `AlarmsBellButton`

<ComponentDescription name="AlarmsBellButton" />

Header bell button that renders severity-bucketed alarm counts in a stacked badge. Typically placed in the [`AppHeader`](/v0-4-0/ui/react/core/components/navigation#appheader) actions cluster.

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `counts` | Optional | `AlarmsBellButtonCounts` | none | Severity-bucketed alarm counts rendered in the stacked badge |
| `onClick` | Optional | `(event: React.MouseEvent<HTMLButtonElement>) => void` | none | Click handler — typically opens an alerts panel or routes to `/alerts` |
| `onSeverityClick` | Optional | `(severity: AlarmSeverity, event: React.MouseEvent<HTMLButtonElement>) => void` | none | Click handler for an individual severity count; when provided, each badge row becomes its own button |
| `label` | Optional | `string` | `'Active alarms'` | Accessible label |
| `className` | Optional | `string` | none | Root class name from the host app |

#### Basic usage

```tsx
<AlarmsBellButton counts={{ critical: 171, high: 72, medium: 322 }} />

<AlarmsBellButton
  counts={{ critical: 2, high: 5 }}
  onSeverityClick={(severity) => router.push(`/alerts?severity=${severity}`)}
/>
```

### `Alerts`

<ComponentDescription name="Alerts" />

Full-page alerts surface that wraps [`CurrentAlerts`](#currentalerts) and (when `isHistoricalAlertsEnabled`) [`HistoricalAlerts`](#historicalalerts), coordinating their shared filter state, date range, and alert click handling. It manages:

1. **Current alerts** (`devices`, `isCurrentAlertsLoading`): raw devices payload and loading flag passed to `CurrentAlerts`.
2. **Historical alerts** (`historicalAlerts`, `isHistoricalAlertsLoading`, `isHistoricalAlertsEnabled`): log data and visibility toggle.
3. **Filtering** (`selectedAlertId`, `initialSeverity`, `typeFiltersForSite`): deep-link focus, severity pre-selection, and site type filters.
4. **Callbacks** (`onAlertClick`, `onDateRangeChange`): row navigation and date range updates.
5. **Sound** (`isSoundEnabled`, `isDemoMode`): critical-alert beep toggle and demo/preview mode.
6. **Layout** (`header`, `className`): optional header slot and root class.

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `devices` | Optional | `Device[][]` | none | Raw devices payload for current alerts (see [`useCurrentAlertDevices`](/v0-4-0/reference/app-toolkit/hooks/data#usecurrentalertdevices)) |
| `isCurrentAlertsLoading` | Optional | `boolean` | `false` | When `true`, shows the loading state in `CurrentAlerts` |
| `historicalAlerts` | Optional | `Alert[]` | none | Pre-fetched historical alerts (see [`useHistoricalAlerts`](/v0-4-0/reference/app-toolkit/hooks/data#usehistoricalalerts)) |
| `isHistoricalAlertsLoading` | Optional | `boolean` | `false` | When `true`, shows the loading state in `HistoricalAlerts` |
| `isHistoricalAlertsEnabled` | Optional | `boolean` | `false` | When `true`, renders the historical alerts log section |
| `selectedAlertId` | Optional | `string` | none | Focuses the table on a single alert (deep-link from URL) |
| `initialSeverity` | Optional | `string` | none | Pre-selects a severity filter (typically from a `?severity=` URL param) |
| `onAlertClick` | Optional | `function` | none | Called with device `id` and alert `uuid` when the user opens an alert row |
| `onDateRangeChange` | Optional | `function` | none | Called when the user picks a new date range in the historical log |
| `dateRange` | Optional | `HistoricalAlertsRange` | last 14 days | Controlled date range (`{ start: number, end: number }` in ms) |
| `isSoundEnabled` | Optional | `boolean` | `false` | Enables the critical-alert audible beep when a critical alert is present |
| `isDemoMode` | Optional | `boolean` | `false` | When `true`, skips sound notifications (safe for preview environments) |
| `typeFiltersForSite` | Optional | `CascaderOption[]` | none | Site-specific overrides for the `TagFilterBar` type filter |
| `header` | Optional | `ReactNode` | none | Optional header (e.g. breadcrumbs) rendered above the alerts tables |
| `className` | Optional | `string` | none | Root class name from the host app |

#### Hook integration

Wire `Alerts` with the MDK adapter hooks for live data (see [`useCurrentAlertDevices`](/v0-4-0/reference/app-toolkit/hooks/data#usecurrentalertdevices) and [`useHistoricalAlerts`](/v0-4-0/reference/app-toolkit/hooks/data#usehistoricalalerts)):

```tsx

function AlertsPage() {
  const { filterTags } = useDevices()
  const devices = useCurrentAlertDevices({ filterTags })
  const [range, setRange] = useState(() => getDefaultHistoricalAlertsRange())
  const historical = useHistoricalAlerts(range)

  return (
    <Alerts
      devices={devices.data}
      isCurrentAlertsLoading={devices.isLoading}
      historicalAlerts={historical.data}
      isHistoricalAlertsLoading={historical.isLoading}
      isHistoricalAlertsEnabled
      dateRange={range}
      onDateRangeChange={setRange}
      onAlertClick={(id, uuid) => router.push(`/alerts/${uuid}`)}
    />
  )
}
```

<Callout type="tip">See [`useCurrentAlertDevices`](/v0-4-0/reference/app-toolkit/hooks/data#usecurrentalertdevices) and [`useHistoricalAlerts`](/v0-4-0/reference/app-toolkit/hooks/data#usehistoricalalerts) for hook options.</Callout>

### `CurrentAlerts`

<ComponentDescription name="CurrentAlerts" />

Sortable, searchable data table of active alerts derived from a raw devices payload. Plays an audible beep when a 
critical alert is present and sound notifications are enabled — after the user confirms 
via [`AlertConfirmationModal`](#alertconfirmationmodal). The [`Alerts`](#alerts) feature wrapper manages all 
state for most use cases; use `CurrentAlerts` directly when you need a standalone current-alerts table.

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `localFilters` | Required | `AlertLocalFilters` | none | Controlled filter state shared with other alerts components (see [type](#alertlocalfilters-type)) |
| `onLocalFiltersChange` | Required | `function` | none | Called with the updated `AlertLocalFilters` when filters change |
| `filterTags` | Required | `string[]` | none | Active search tags; mirrors the devices-store tag slice |
| `onFilterTagsChange` | Required | `function` | none | Called with updated tags when the user adds or removes a search chip |
| `devices` | Optional | `Device[][]` | none | Raw devices payload; drives alert row derivation |
| `isLoading` | Optional | `boolean` | `false` | When `true`, shows the table loading state |
| `selectedAlertId` | Optional | `string` | none | Focuses the table on a single alert |
| `onAlertClick` | Optional | `function` | none | Called with device `id` and alert `uuid` when the user opens an alert row |
| `isSoundEnabled` | Optional | `boolean` | `false` | Enables the critical-alert audible beep |
| `isDemoMode` | Optional | `boolean` | `false` | When `true`, skips sound notifications |
| `typeFiltersForSite` | Optional | `CascaderOption[]` | none | Site-specific overrides for the type filter in `TagFilterBar` |
| `className` | Optional | `string` | none | Root class name from the host app |

#### `AlertLocalFilters` type

```tsx
type AlertLocalFilters = {
  severity?: string[] | string
  status?: string[]
  type?: string[]
  id?: string[]
  thing?: { id?: string }
  [key: string]: unknown
}
```

#### Basic usage

```tsx

function AlertsView({ devices, isLoading }) {
  const [localFilters, setLocalFilters] = useState<AlertLocalFilters>({})
  const [filterTags, setFilterTags] = useState<string[]>([])

  return (
    <CurrentAlerts
      devices={devices}
      isLoading={isLoading}
      localFilters={localFilters}
      onLocalFiltersChange={setLocalFilters}
      filterTags={filterTags}
      onFilterTagsChange={setFilterTags}
      onAlertClick={(id, uuid) => console.log('Alert clicked', id, uuid)}
    />
  )
}
```

### `HistoricalAlerts`

<ComponentDescription name="HistoricalAlerts" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `alerts` | Optional | `Alert[]` | `[]` | Pre-fetched historical alert entries |
| `isLoading` | Optional | `boolean` | `false` | Show table loading state |
| `localFilters` | Required | `AlertLocalFilters` | none | Filters and search state shared with `CurrentAlerts` |
| `filterTags` | Required | `string[]` | none | Active filter tags |
| `dateRange` | Required | `HistoricalAlertsRange` | none | Controlled start/end timestamps (ms) |
| `onDateRangeChange` | Required | `function` | none | Fires when the user picks a new range |
| `onAlertClick` | Optional | `function` | none | Row click handler |
| `className` | Optional | `string` | none | Additional CSS class |

#### `HistoricalAlertsRange` type

```tsx
type HistoricalAlertsRange = {
  start: number
  end: number
}
```

#### Basic usage

```tsx
<HistoricalAlerts
  alerts={historicalAlerts}
  localFilters={localFilters}
  filterTags={filterTags}
  dateRange={{ start: rangeStart, end: rangeEnd }}
  onDateRangeChange={({ start, end }) => setRange({ start, end })}
  onAlertClick={(id, uuid) => console.log('Alert clicked', id, uuid)}
/>
```

#### Loading state

```tsx
<HistoricalAlerts
  alerts={[]}
  isLoading
  localFilters={localFilters}
  filterTags={filterTags}
  dateRange={dateRange}
  onDateRangeChange={onDateRangeChange}
/>
```

#### Behavior notes

- The component wires a `DateRangePicker` into the table title via `AlertsTableTitle`, so date range changes 
come back through `onDateRangeChange`.
- Rows are sorted by severity (descending) then creation date (descending) by default
- Timestamps are formatted through `useTimezoneFormatter()`'s `getFormattedDate`

### `TagFilterBar`

<ComponentDescription name="TagFilterBar" />

Strip of removable filter chips and a tag-input search field for narrowing the alerts table by severity, status, 
type, and device tags. Used as the subtitle slot of [`AlertsTableTitle`](#alertstabletitle) inside [`CurrentAlerts`](#currentalerts).

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `filterTags` | Required | `string[]` | none | Active search tag chips |
| `localFilters` | Required | `AlertLocalFilters` | none | Controlled filter state (severity, status, type) |
| `onSearchTagsChange` | Required | `function` | none | Called with updated tags when the user adds or removes a chip |
| `onLocalFiltersChange` | Required | `function` | none | Called with updated `AlertLocalFilters` when a cascader selection changes |
| `typeFiltersForSite` | Optional | `CascaderOption[]` | default filter list | Site-specific overrides for the "Type" cascader children |
| `placeholder` | Optional | `string` | none | Placeholder text for the tag-input field |
| `className` | Optional | `string` | none | Root class name from the host app |

#### Basic usage

```tsx

<TagFilterBar
  filterTags={filterTags}
  localFilters={localFilters}
  onSearchTagsChange={setFilterTags}
  onLocalFiltersChange={setLocalFilters}
/>
```

### `AlertsTableTitle`

<ComponentDescription name="AlertsTableTitle" />

Heading strip for an alerts table — renders the section title and an optional subtitle slot (typically used 
for `TagFilterBar` or `DateRangePicker`). Used internally by [`CurrentAlerts`](#currentalerts) and 
[`HistoricalAlerts`](#historicalalerts).

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `title` | Required | `ReactNode` | none | Section heading content |
| `subtitle` | Optional | `ReactNode` | none | Optional content rendered below the title (e.g. `TagFilterBar` or `DateRangePicker`) |
| `className` | Optional | `string` | none | Root class name from the host app |

#### Basic usage

```tsx

<AlertsTableTitle
  title="Current Alerts"
  subtitle={
    <TagFilterBar
      filterTags={filterTags}
      localFilters={localFilters}
      onSearchTagsChange={setFilterTags}
      onLocalFiltersChange={setLocalFilters}
    />
  }
/>
```

### `AlertConfirmationModal`

<ComponentDescription name="AlertConfirmationModal" />

One-time consent dialog that appears before critical-alert sound notifications are activated. 
Renders a non-dismissible `Dialog` with an "Understood" button. Once confirmed (tracked in `sessionStorage`), `CurrentAlerts` 
begins playing the beep.

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `isOpen` | Required | `boolean` | none | Controls modal visibility |
| `onOk` | Required | `function` | none | Called when the user clicks "Understood" |

#### Basic usage

```tsx

<AlertConfirmationModal
  isOpen={showConfirmation}
  onOk={() => setShowConfirmation(false)}
/>
```

### `AlarmContents`

<ComponentDescription name="AlarmContents" />

Building block of the alarm card — renders the body region with the alert details and recommended next actions. Compose it inside your own card chrome when you need a custom alarm layout.

#### Import

```tsx
```

### `AlarmRow`

<ComponentDescription name="AlarmRow" />

Building block of the alarm feed — a single row with a severity dot, timestamp, source device, and the alert message. Map your alert list to a stack of `AlarmRow`s for a custom feed.

#### Import

```tsx
```

## Next steps

- For live incident context, see [`ActiveIncidentsCard`](/v0-4-0/ui/react/foundation/dashboard/feeds#activeincidentscard) on the 
dashboard Feeds page
- For alert data hooks, see [`useCurrentAlertDevices`](/v0-4-0/reference/app-toolkit/hooks/data#usecurrentalertdevices) 
and [`useHistoricalAlerts`](/v0-4-0/reference/app-toolkit/hooks/data#usehistoricalalerts)

# Dashboard (/v0-4-0/ui/react/foundation/dashboard)

The dashboard is the top-level operations surface. It composes live feeds, summary widgets, and chart cards into a single overview of fleet health, pool context, and power trends.

The docs group these components into three intuitive buckets:

- [Feeds](/v0-4-0/ui/react/foundation/dashboard/feeds): active incidents and activity logs
- [Stats](/v0-4-0/ui/react/foundation/dashboard/stats): at-a-glance summary cards for containers, pool details, and export controls
- [Charts](/v0-4-0/ui/react/foundation/dashboard/charts): line, timeline, and multi-series chart cards

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)

## All dashboard components

<ComponentTable category="Dashboard" pkg="foundation" />

## Next steps

For past alerts paired with the dashboard's active-incidents view, see [`HistoricalAlerts`(../alerts#historicalalerts).

# Charts (/v0-4-0/ui/react/foundation/dashboard/charts)

Chart components specialized for Bitcoin mining operations data visualization, including hashrate trends, power consumption, power mode timelines, 
and multi-container comparisons.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)

### `ChartWrapper`

<ComponentDescription name="ChartWrapper" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `children` | Optional | `ReactNode` | none | Chart content rendered when data is present |
| `data` | Optional | `object \| array` | none | Chart data for LineChart-style datasets; used by `useChartDataCheck` |
| `dataset` | Optional | `object \| array` | none | Chart dataset for BarChart-style data; used by `useChartDataCheck` |
| `isLoading` | Optional | `boolean` | `false` | When `true`, shows the loading overlay with default `<Loader />` (animated dots) or `customLoader`, and hides chart children |
| `showNoDataPlaceholder` | Optional | `boolean` | `true` | When `true`, shows empty placeholder when data/dataset has no values |
| `customLoader` | Optional | `ReactNode` | `<Loader />` | Node shown in the loading overlay (default core `Loader`) |
| `customNoDataMessage` | Optional | `string \| ReactNode` | none | Custom empty state message or component |
| `minHeight` | Optional | `number` | `400` | Minimum height in pixels for empty state |
| `loadingMinHeight` | Optional | `number` | `minHeight` | Minimum height in pixels for the loading overlay (default `<Loader />` or `customLoader`) |
| `className` | Optional | `string` | none | Root class name from the host app |

Chart children (`LineChart`, `BarChart`) come from `@tetherto/mdk-react-devkit/core`.

```tsx

const CustomSpinner = () => <div className="my-spinner" aria-hidden />
```

#### Basic usage

```tsx
<ChartWrapper
  data={chartData}
  isLoading={isLoading}
  minHeight={400}
>
  <LineChart data={chartData} />
</ChartWrapper>
```

#### With custom loader

```tsx
<ChartWrapper
  data={chartData}
  isLoading={isLoading}
  customLoader={<CustomSpinner />}
  minHeight={300}
>
  <LineChart data={chartData} />
</ChartWrapper>
```

#### With custom empty message

Pass an empty dataset so `ChartWrapper` shows the empty placeholder instead of the chart. With non-empty `dataset`, children render and the custom 
message is not shown.

```tsx

const emptyBarData = { labels: [], datasets: [] }

<ChartWrapper
  dataset={emptyBarData}
  isLoading={false}
  customNoDataMessage="No hashrate data available for this period"
  minHeight={300}
>
  <BarChart data={emptyBarData} />
</ChartWrapper>
```

#### States

The component handles three states automatically. Chart **children** are hidden while `isLoading` is `true` or when `data` / `dataset` is empty (and `showNoDataPlaceholder` is `true`):

1. **Loading**: Shows default `<Loader />` (animated dots) or `customLoader`
2. **No data**: Shows `EmptyState` placeholder (or `customNoDataMessage`)
3. **Has data**: Shows chart content

#### Styling

- `.mdk-chart-wrapper`: Root element
- `.mdk-chart-wrapper__content`: Chart content container
- `.mdk-chart-wrapper__content--hidden`: Hidden content state
- `.mdk-chart-wrapper__empty`: Empty state container
- `.mdk-chart-wrapper__loading`: Loading state container

### `ContainerCharts`

<ComponentDescription name="ContainerCharts" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `featureEnabled` | Optional | `boolean` | `true` | When `false`, shows `disabledMessage` instead of charts |
| `combinations` | Required | \{ value: string; label: string \}[] | none | Select options (`value` / `label` pairs) |
| `chartRawData` | Optional | `ChartEntry[]` | `null` | Raw time-series rows; shape depends on selected container type |
| `selectedCombination` | Optional | `string \| null` | none | Controlled selected `combinations[].value` |
| `defaultSelectedCombination` | Optional | `string \| null` | `null` | Initial selection when uncontrolled |
| `onSelectedCombinationChange` | Optional | `function` | none | Called with the new combination `value` (or `null`) when the user changes selection |
| `isLoadingCombinations` | Optional | `boolean` | `false` | When `true`, shows loading state on the combination selector |
| `isLoadingCharts` | Optional | `boolean` | `false` | When `true`, shows loading state on the chart grid |
| `disabledMessage` | Optional | `string` | `'Container Charts feature is not enabled'` | Message when `featureEnabled` is `false` |
| `title` | Optional | `string` | `'Container Charts'` | Section heading |
| `getDatasetBorderColor` | Optional | `function` | none | Resolver for dataset border colors; receives dataset metadata from the builder |

#### `ChartEntry` type

```tsx
type EntryData = {
  [key: string]: number | null | undefined
}

type ChartEntry = {
  ts: number | string
  container_specific_stats_group_aggr: Record<string, EntryData | null | undefined>
}
```

Each row’s `container_specific_stats_group_aggr` keys use complete container type ids (for example `container-bd-d40-m30`, `container-as-hk3`). Stat field names inside each entry depend on the container family.

**Bitdeer** (`container-bd-…` keys) — field names per chart block:

| Chart block | Stat field pattern |
|-------------|-------------------|
| Liquid Temp H | `hot_temp_c_w_{n}_group` |
| Liquid Temp L | `cold_temp_c_w_{n}_group` |
| Oil Temp | `cold_temp_c_{n}_group` (no `_w_`) |
| Pressure | `tank{n}_bar_group` |

**Hydro** (`container-as-hk3`): `supply_liquid_temp_group`, `supply_liquid_pressure_group`.

Combination `value` strings must contain the real prefix patterns the component detects (`bd-…`, `as-hk3…`, `as-immersion…`, `mbt-…`). Values such as `bitmain_hydro_b2` (underscores) do not match `isAntspaceHydro()` and show the wrong chart blocks.

```tsx
const chartData: ChartEntry[] = [
  {
    ts: 1700000000,
    container_specific_stats_group_aggr: {
      'container-bd-d40-m30': {
        hot_temp_c_w_1_group: 52,
        hot_temp_c_w_2_group: 54,
        cold_temp_c_w_1_group: 38,
        cold_temp_c_w_2_group: 39,
        cold_temp_c_1_group: 36,
        cold_temp_c_2_group: 37,
        tank1_bar_group: 2.4,
        tank2_bar_group: 2.5,
      },
      'container-as-hk3': {
        supply_liquid_temp_group: 44,
        supply_liquid_pressure_group: 2.8,
      },
    },
  },
]
```

#### Basic usage

```tsx
<ContainerCharts
  combinations={[
    { value: 'bd-d40-m30_wm-m30sp', label: 'Bitdeer M30SP' },
    { value: 'as-hk3_am-s19xp_h', label: 'Bitmain Hydro S19XP' },
  ]}
  defaultSelectedCombination="bd-d40-m30_wm-m30sp"
  chartRawData={chartData}
  onSelectedCombinationChange={setSelected}
/>
```

#### Container type charts

Charts displayed vary by container type:
- **Bitdeer**: Liquid Temp H, Liquid Temp L, Oil Temp, Pressure
- **Bitmain Hydro**: Liquid Temp L, Pressure
- **Bitmain Immersion**: Liquid Temp L, Oil Temp
- **MicroBT**: Liquid Temp L, Pressure

#### Styling

- `.mdk-container-charts`: Root element
- `.mdk-container-charts__title`: Section title
- `.mdk-container-charts__select-row`: Combination selector row
- `.mdk-container-charts__select-label`: Selector label
- `.mdk-container-charts__select`: Select dropdown
- `.mdk-container-charts__layout`: Charts grid layout
- `.mdk-container-charts__chart-block`: Individual chart container
- `.mdk-container-charts__chart-title`: Chart block title
- `.mdk-container-charts__loading`: Loading state container

### `LineChartCard`

<ComponentDescription name="LineChartCard" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Optional | `LineChartCardData` | none | Pre-processed chart payload passed to the inner line chart |
| `rawData` | Optional | `unknown` | none | Raw API payload; use with `dataAdapter` instead of `data` |
| `dataAdapter` | Optional | `function` | none | Maps `rawData` to `LineChartCardData` |
| `timelineOptions` | Optional | `TimelineOption[]` | none | Timeline range buttons |
| `timeline` | Optional | `string` | none | Controlled active timeline value |
| `defaultTimeline` | Optional | `string` | `'5m'` | Initial timeline when uncontrolled |
| `onTimelineChange` | Optional | `function` | none | Called when the user selects a new timeline range |
| `title` | Optional | `string` | none | Chart title in the card header |
| `titleExtra` | Optional | `ReactNode` | none | Node rendered inline beside the title — use for a secondary label or unit badge |
| `headerAction` | Optional | `ReactNode` | none | Node rendered at the trailing edge of the header row — use for action buttons such as an expand toggle |
| `detailLegends` | Optional | `boolean` | `false` | When `true`, shows detailed legends below the title |
| `isLoading` | Optional | `boolean` | `false` | When `true`, shows loading state |
| `shouldResetZoom` | Optional | `boolean` | `true` | When `true`, resets chart zoom when the timeline changes |
| `minHeight` | Optional | `number` | `350` | Minimum chart body height in pixels |
| `className` | Optional | `string` | none | Root class name from the host app |

#### Basic usage

```tsx
<LineChartCard
  title="Temperature"
  data={chartData}
  timelineOptions={[
    { label: '5m', value: '5m' },
    { label: '1h', value: '1h' },
    { label: '24h', value: '24h' },
  ]}
  defaultTimeline="1h"
/>
```

#### With data adapter

```tsx
<LineChartCard
  title="Power Consumption"
  rawData={rawStats}
  dataAdapter={(raw) => processStatsToChartData(raw)}
  timelineOptions={timelineOptions}
  defaultTimeline="24h"
  isLoading={isLoading}
/>
```

#### Styling

- `.mdk-line-chart-card`: Root element
- `.mdk-line-chart-card__legends`: Detail legends container

### `PowerModeTimelineChart`

<ComponentDescription name="PowerModeTimelineChart" />

Mining-specific wrapper around [`TimelineChart`](#timelinechart). Use `TimelineChart` directly when you supply custom row labels and segment data.

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Optional | `PowerModeTimelineEntry[]` | `[]` | Historical power mode segments for `TimelineChart` |
| `dataUpdates` | Optional | `PowerModeTimelineEntry[]` | `[]` | New segments merged into the chart when updates arrive |
| `timezone` | Optional | `string` | `'UTC'` | IANA timezone for segment timestamps |
| `title` | Optional | `string` | `'Power Mode Timeline'` | Chart title passed to the inner timeline |
| `isLoading` | Optional | `boolean` | `false` | When `true`, shows loading state inside the chart card |

#### `PowerModeTimelineEntry` type

```tsx
type PowerModeTimelineEntry = {
  ts?: number
  power_mode_group_aggr?: Record<string, string>
  status_group_aggr?: Record<string, string>
}
```

#### Basic usage

```tsx
<PowerModeTimelineChart
  data={powerModeHistory}
  timezone="America/New_York"
  isLoading={isLoading}
/>
```

#### With real-time updates

```tsx
<PowerModeTimelineChart
  data={powerModeHistory}
  dataUpdates={realtimeUpdates}
  timezone={userTimezone}
/>
```

### `TimelineChart`

<ComponentDescription name="TimelineChart" />

Gantt-like timeline for multiple rows. `labels` lists row names (Y axis). Each dataset groups colored segments; each segment's `y` must 
match one of `labels`, and `x` is `[startMs, endMs]`.

#### Import

```tsx
```

#### Empty initial state

Use `emptyTimelineChartData` (`{ labels: [], datasets: [] }`) for the first render before live timeline data arrives. Pair with `isLoading` while fetching if needed.

```tsx
<TimelineChart
  initialData={emptyTimelineChartData}
  title="Miner state"
  isLoading={isLoading}
/>
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `initialData` | Required | `TimelineChartData` | none | Row labels and segment datasets shown on first render |
| `newData` | Optional | `TimelineChartData` | none | Extra labels and datasets merged when `skipUpdates` is false |
| `skipUpdates` | Optional | `boolean` | `false` | When `true`, ignores `newData` |
| `range` | Optional | `ChartRange` | none | Visible time window (`min` / `max` as `Date` or epoch ms) |
| `axisTitleText` | Optional | `AxisTitleText` | `{ x: 'Time', y: '' }` | Axis title strings |
| `isLoading` | Optional | `boolean` | `false` | Shows loading state inside `ChartContainer` |
| `title` | Optional | `string` | none | Chart title passed to `ChartContainer` |
| `height` | Optional | `number \| string` | none | Root height (CSS length or pixels) |

#### `TimelineChartData` type

See also [Timeline chart types](/v0-4-0/reference/app-toolkit/ui-kit/types#timeline-chart-types).

```tsx
type TimelineChartDataPoint = {
  x: [number, number] // segment start and end (epoch ms)
  y: string | undefined // must match an entry in labels
}

type TimelineChartDataset = {
  label: string
  data: TimelineChartDataPoint[]
  borderColor?: string[]
  backgroundColor?: string[]
  color?: string
}

type TimelineChartData = {
  labels: string[]
  datasets: TimelineChartDataset[]
}

type ChartRange = {
  min: Date | number
  max: Date | number
}
```

#### Basic usage

```tsx
const start = Date.now() - 2 * 60 * 60 * 1000

<TimelineChart
  initialData={{
    labels: ['Task A', 'Task B', 'Task C'],
    datasets: [
      {
        label: 'In Progress',
        color: '#22afff',
        data: [
          { x: [start, start + 30 * 60 * 1000], y: 'Task A' },
          { x: [start + 20 * 60 * 1000, start + 50 * 60 * 1000], y: 'Task B' },
        ],
      },
      {
        label: 'Completed',
        color: '#72f59e',
        data: [{ x: [start + 30 * 60 * 1000, start + 60 * 60 * 1000], y: 'Task A' }],
      },
    ],
  }}
  title="Project timeline"
  axisTitleText={{ x: 'Time', y: 'Tasks' }}
/>
```

#### With `newData` updates

```tsx
<TimelineChart
  initialData={baseline}
  newData={streamingUpdate}
  skipUpdates={false}
/>
```

When `newData` arrives, labels are unioned and datasets are appended. Set `skipUpdates` to keep only `initialData`.

#### Styling

- `.mdk-timeline-chart`: Root element
- `.mdk-timeline-chart__loading`: Loading container
- `.mdk-timeline-chart__legend`, `.mdk-timeline-chart__legend-item`, `.mdk-timeline-chart__legend-color`, `.mdk-timeline-chart__legend-label`: Legend row
- `.mdk-timeline-chart__visualization`: Scrollable chart body

### `WidgetTopRow`

<ComponentDescription name="WidgetTopRow" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `title` | Required | `string` | none | Widget title shown in the header row |
| `power` | Optional | `number` | none | Power consumption value; formatted with `unit` when set |
| `unit` | Optional | `string` | none | Unit suffix for `power` (for example `kW`) |
| `statsErrorMessage` | Optional | `string \| ErrorWithTimestamp[]` | none | Replaces the power readout with `-` and a `formatErrors` tooltip |
| `alarms` | Optional | `object` | none | Map keyed by `liquidAlarms`, `leakageAlarms`, `pressureAlarms`, `otherAlarms`; values are [`Alert`](/v0-4-0/reference/app-toolkit/ui-kit/types#alert) arrays |
| `className` | Optional | `string` | none | Root class name from the host app |

#### Composition

`WidgetTopRow` renders the title, then four fixed alarm slots in order (liquid, leakage, pressure, other), then the power readout. Each slot reads the matching `alarms` key from the table below; when the array is missing or empty, that slot is not shown. Tooltip text lists each [`Alert`](/v0-4-0/reference/app-toolkit/ui-kit/types#alert) with timestamp and description.

| Slot label | `alarms` key | Icon role |
|------------|--------------|-----------|
| Liquid | `liquidAlarms` | Temperature / liquid alarm |
| Leakage | `leakageAlarms` | Fluid / leakage alarm |
| Pressure | `pressureAlarms` | Pressure alarm |
| Other | `otherAlarms` | Other alarm |

When `power` and `unit` are set and `statsErrorMessage` is unset, the power label shows `formatNumber(unitToKilo(power))` followed by the unit in a nested `<span>`.

#### Basic usage

```tsx
<WidgetTopRow
  title="Container A1"
  power={125000}
  unit="kW"
/>
```

#### More examples

<Accordions>
  <Accordion title="With alarms">

Each `alarms` key must match the table above (`liquidAlarms`, `leakageAlarms`, `pressureAlarms`, `otherAlarms`), not generic names like `temperature`. Items use the [`Alert`](/v0-4-0/reference/app-toolkit/ui-kit/types#alert) shape.

```tsx
<WidgetTopRow
  title="Container A1"
  power={125000}
  unit="kW"
  alarms={{
    liquidAlarms: [
      {
        severity: 'critical',
        createdAt: Date.now(),
        name: 'Al.Liq.#1',
        description: '1st Liquid Alarm',
      },
    ],
    leakageAlarms: [
      {
        severity: 'high',
        createdAt: Date.now(),
        name: 'Al.Lkg.#1',
        description: '1st Leakage Alarm',
      },
    ],
    pressureAlarms: [
      {
        severity: 'medium',
        createdAt: Date.now(),
        name: 'Al.Prs.#1',
        description: '1st Pressure Alarm',
      },
    ],
    otherAlarms: [
      {
        severity: 'low',
        createdAt: Date.now(),
        name: 'Al.Oth.#1',
        description: '1st Other Alarm',
      },
    ],
  }}
/>
```

  </Accordion>
  <Accordion title="With stats error">

When `statsErrorMessage` is set, the power label is hidden and `-` is shown with a tooltip built from `formatErrors` (string or [`ErrorWithTimestamp`](/v0-4-0/reference/app-toolkit/ui-kit/types#errorwithtimestamp) array).

```tsx
<WidgetTopRow title="Container A1" statsErrorMessage="Example Error Message" />
```

  </Accordion>
</Accordions>

#### Styling

- `.mdk-widget-top-row`: Root element
- `.mdk-widget-top-row__inner`: Inner container
- `.mdk-widget-top-row__title`: Title text
- `.mdk-widget-top-row__alarm-info-icon`: Alarm slot icon (tooltip trigger)
- `.mdk-widget-top-row__alarm-info-title`: Alarm tooltip heading (`{slot} issues`)
- `.mdk-widget-top-row__power`: Power display or error placeholder

## Related hooks

The following adapter and component hooks supply data to the chart components on this page.

| Hook | Supplies |
|------|---------|
| [`useHashrateChartData`](/v0-4-0/reference/app-toolkit/hooks/data#usehashratechartdata) | `ChartCardData` combining site and per-pool hashrate series for `<LineChartCard />` |
| [`useSiteConsumptionChartData`](/v0-4-0/reference/app-toolkit/hooks/data#usesiteconsumptionchartdata) | Consumption `ChartCardData` in MW for `<LineChartCard />` |
| [`usePowerModeTimelineData`](/v0-4-0/reference/app-toolkit/hooks/data#usepowermodetimelinedata) | `PowerModeTimelineEntry[]` for `<PowerModeTimelineChart />` |
| [`useChartDataCheck`](/v0-4-0/reference/app-toolkit/hooks/components#usechartdatacheck) | Empty-state guard used by `<ChartWrapper />` to decide when to render the no-data placeholder |

# Feeds (/v0-4-0/ui/react/foundation/dashboard/feeds)

Feeds surface streams of operational events on the dashboard. `ActiveIncidentsCard` renders the current alerts feed; `LogsCard` (from `@tetherto/mdk-react-devkit/core`) renders activity and event logs.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)

### `ActiveIncidentsCard`

<ComponentDescription name="ActiveIncidentsCard" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `label` | Optional | `string` | `'Active Alerts'` | Card header label |
| `items` | Optional | `TIncidentRowProps[]` | `[]` | Array of incident items to display |
| `isLoading` | Optional | `boolean` | `false` | Show skeleton loading state |
| `skeletonRows` | Optional | `number` | `4` | Number of skeleton rows when loading |
| `emptyMessage` | Required | `string` | none | Message when no incidents |
| `onItemClick` | Required | `function` | none | Callback when an incident row is clicked |
| `className` | Optional | `string` | none | Additional CSS class |

#### `TIncidentRowProps`

Each incident item requires these properties:

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `id` | Optional | `string` | none | Unique incident identifier |
| `title` | Required | `string` | none | Incident title |
| `subtitle` | Required | `string` | none | Secondary text (e.g., timestamp or source) |
| `body` | Required | `string` | none | Incident description |
| `severity` | Required | `'critical' \| none | 'high' \| 'medium'` | Severity level |

#### Basic usage

```tsx
<ActiveIncidentsCard
  label="Active Alerts"
  items={[
    {
      id: '1',
      title: 'High Temperature Alert',
      subtitle: 'Miner #A2341',
      body: 'Temperature exceeded 85°C threshold',
      severity: 'critical',
    },
    {
      id: '2',
      title: 'Network Connection Lost',
      subtitle: 'Pool: pool.example.com',
      body: 'Connection timeout after 30 seconds',
      severity: 'high',
    },
  ]}
  onItemClick={(id) => console.log('Clicked:', id)}
/>
```

#### Loading state

```tsx
<ActiveIncidentsCard label="Active Alerts" isLoading skeletonRows={4} />
```

#### Empty state

```tsx
<ActiveIncidentsCard
  label="Active Alerts"
  items={[]}
  emptyMessage="No active incidents"
/>
```

#### Styling

- `.mdk-active-incidents-card`: Root element
- `.mdk-active-incidents-card__header`: Header container
- `.mdk-active-incidents-card__label`: Header label text
- `.mdk-active-incidents-card__list`: Incidents list container
- `.mdk-active-incidents-card__row`: Individual incident row
- `.mdk-active-incidents-card__row--clickable`: Clickable row modifier
- `.mdk-active-incidents-card__row-title`: Incident title
- `.mdk-active-incidents-card__row-subtitle`: Incident subtitle
- `.mdk-active-incidents-card__row-body`: Incident body text
- `.mdk-active-incidents-card__empty`: Empty state container
- `.mdk-active-incidents-card__skeleton-container`: Loading skeleton container

### `LogsCard`

[`LogsCard`](/v0-4-0/ui/react/core/components/logs#logscard) is a `@tetherto/mdk-react-devkit/core` component used on the dashboard to render paginated activity and event log feeds..

Import from `@tetherto/mdk-react-devkit/core`:

```tsx
```

## Related hooks

| Hook | Supplies |
|------|---------|
| [`useActiveIncidents`](/v0-4-0/reference/app-toolkit/hooks/data#useactiveincidents) | Returns `IncidentRow[]` ready for `<ActiveIncidentsCard items={...} />` |

## Next steps

For past alerts to pair with the active incidents feed, see [`HistoricalAlerts`](/v0-4-0/ui/react/foundation/alerts#historicalalerts).

# Stats (/v0-4-0/ui/react/foundation/dashboard/stats)

Stat widgets are the compact, at-a-glance cards that line the dashboard. They summarize container health, pool configuration, liquid supply, tanks, miner roll-ups, and export controls.

For the persistent app header bar components (HeaderStatsBar, HeaderConsumptionBox, etc.) see [Settings › Header controls › Header items](/v0-4-0/ui/react/foundation/settings/header-controls/header-items).

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- Most widgets expect a container [`Device`](/v0-4-0/reference/app-toolkit/ui-kit/types#device) record; read the individual Props sections for field shapes
- In TypeScript, import the type from the foundation barrel: `import type { Device } from '@tetherto/mdk-react-devkit/foundation'`. Snippet fixtures may be plain objects that satisfy `Device` without an import

## Supported widgets

| Scope | Components |
|-------|------------|
| All vendors | [`MinersSummaryBox`](./generic-widgets#minerssummarybox), [`PoolDetailsCard`](./generic-widgets#pooldetailscard), [`PoolDetailsPopover`](./generic-widgets#pooldetailspopover), [`StatsExport`](./generic-widgets#statsexport), [`SupplyLiquidBox`](./generic-widgets#supplyliquidbox), [`TanksBox`](./generic-widgets#tanksbox) |
| Bitmain Immersion | [`BitMainImmersionSummaryBox`](./bitmain#bitmainimmersionsummarybox) |
| MicroBT | [`MicroBTWidgetBox`](./microbt#microbtwidgetbox) |

## Widget groups

- [Generic widgets](./generic-widgets): vendor-agnostic summary cards for containers, pool details, and data export
- [Bitmain](./bitmain): summary widget for Bitmain Immersion containers
- [MicroBT](./microbt): summary widget for MicroBT containers

## All stat components

<ComponentTable category="Dashboard" subcategory="Generic widgets" pkg="foundation" />
<ComponentTable category="Dashboard" subcategory="Bitmain" pkg="foundation" />
<ComponentTable category="Dashboard" subcategory="MicroBT" pkg="foundation" />

# Bitmain (/v0-4-0/ui/react/foundation/dashboard/stats/bitmain)

Summary widget for Bitmain Immersion containers. For other vendor widgets see [MicroBT](/v0-4-0/ui/react/foundation/dashboard/stats/microbt). For vendor-agnostic cards see [Generic widgets](/v0-4-0/ui/react/foundation/dashboard/stats/generic-widgets).

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- A [`Device`](/v0-4-0/reference/app-toolkit/ui-kit/types#device) record of type `bitmain-immersion`

## Components

<ComponentTable category="Dashboard" subcategory="Bitmain" pkg="foundation" />

### `BitMainImmersionSummaryBox`

<ComponentDescription name="BitMainImmersionSummaryBox" />

{/*Returns `null` when `data` is not provided. Reads pump flags (`second_pump1`, `second_pump2`, `one_pump`) and temperature fields from
`container_specific_stats`.*/}

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Optional | `Device` | none | Bitmain immersion container device record; returns `null` when omitted |
| `containerSettings` | Optional | `BitMainImmersionSummaryBoxContainerSettings \| null` | `null` | Custom temperature thresholds passed to immersion color helpers |

#### `BitMainImmersionSummaryBoxContainerSettings` type

```tsx
type BitMainImmersionSummaryBoxContainerSettings = {
  thresholds?: Record<string, unknown>
}
```

```tsx

const immersionContainer: Device = {
  id: 'bitmain-immersion-1',
  type: 'bitmain-immersion',
  last: {
    snap: {
      stats: {
        status: 'running',
        container_specific: {
          second_supply_temp1: 40,
          second_supply_temp2: 41,
          primary_supply_temp: 42,
          second_pump1: true,
          second_pump2: true,
          second_pump1_fault: false,
          second_pump2_fault: false,
          one_pump: true,
        },
      },
    },
  },
}

const customThresholds = {
  oilTemperature: {
    COLD: 30,
    LIGHT_WARM: 34,
    WARM: 38,
    HOT: 42,
    SUPERHOT: 45,
  },
}
```

#### Basic usage

```tsx
<BitMainImmersionSummaryBox data={immersionContainer} />
```

#### More examples

<Accordions>
  <Accordion title="With custom thresholds">

```tsx
<BitMainImmersionSummaryBox
  data={immersionContainer}
  containerSettings={{ thresholds: customThresholds }}
/>
```

  </Accordion>
  <Accordion title="Oil pump fault">

When `second_pump1_fault` is `true`, oil pump #1 shows an `Error` indicator while pump #2 can remain `Running`.

```tsx

const immersionWithPump1Fault: Device = {
  id: 'bitmain-immersion-1',
  type: 'bitmain-immersion',
  last: {
    snap: {
      stats: {
        status: 'running',
        container_specific: {
          second_pump1: true,
          second_pump2: true,
          second_pump1_fault: true,
          one_pump: true,
          primary_supply_temp: 42,
          second_supply_temp1: 40,
          second_supply_temp2: 41,
        },
      },
    },
  },
}

<BitMainImmersionSummaryBox data={immersionWithPump1Fault} />
```

  </Accordion>
</Accordions>

#### Composition

- Oil pump #1 and oil pump #2 indicators derived from `second_pump1` / `second_pump2` and their `_fault` flags
- Water pump indicator derived from `one_pump`
- Three `SingleStatCard`s for primary supply temp, secondary supply Temp1, and secondary supply Temp2, each colored and flashed via `getImmersionTemperatureColor` and `shouldImmersionTemperatureFlash`

#### Styling

- `.mining-sdk-bitmain-immersion-summary-box`: Root element
- `.mining-sdk-bitmain-immersion-summary-box__pumps`: Pumps row
- `.mining-sdk-bitmain-immersion-summary-box__pump`: Single pump cell
- `.mining-sdk-bitmain-immersion-summary-box__pump-title`: Pump label text
- `.mining-sdk-bitmain-immersion-summary-box__liquid-stats`: Liquid stat cards row

# Generic widgets (/v0-4-0/ui/react/foundation/dashboard/stats/generic-widgets)

Widgets that work with any container vendor and pool configuration. For header bar controls see [Header items](/v0-4-0/ui/react/foundation/settings/header-controls/header-items). For vendor-specific summary boxes see [Bitmain](/v0-4-0/ui/react/foundation/dashboard/stats/bitmain) and [MicroBT](/v0-4-0/ui/react/foundation/dashboard/stats/microbt).

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- Most widgets expect a container [`Device`](/v0-4-0/reference/app-toolkit/ui-kit/types#device) record; read the individual Props sections for field shapes

## Components

<ComponentTable category="Dashboard" subcategory="Generic widgets" pkg="foundation" />

### `MinersSummaryBox`

<ComponentDescription name="MinersSummaryBox" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `params` | Required | `MinersSummaryParam[]` | none | Label-value pairs rendered in a two-column grid |
| `className` | Optional | `string` | none | Additional CSS class |

#### `MinersSummaryParam` type

```tsx
type MinersSummaryParam = {
  label: string   // Parameter name
  value: string   // Pre-formatted display value (including units)
}
```

#### Basic usage

```tsx
<MinersSummaryBox
  params={[
    { label: 'Efficiency', value: '32.5 W/TH/S' },
    { label: 'Hash Rate', value: '1.24 PH/s' },
    { label: 'Max Temp', value: '72 °C' },
    { label: 'Avg Temp', value: '65 °C' },
  ]}
/>
```

#### Notes

- Values must be pre-formatted strings (the component does not format data)
- Text size automatically adjusts for long values (>12 or >15 characters)

#### Styling

- `.mdk-miners-summary-box`: Root element
- `.mdk-miners-summary-box__param`: Individual parameter row
- `.mdk-miners-summary-box__label`: Parameter label
- `.mdk-miners-summary-box__label--small`: Smaller text for long values
- `.mdk-miners-summary-box__label--tiny`: Tiny text for very long values
- `.mdk-miners-summary-box__value`: Parameter value

### `PoolDetailsCard`

<ComponentDescription name="PoolDetailsCard" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `details` | Required | `PoolDetailItem[]` | none | Pool metadata rows; an empty array shows "No data available" |
| `label` | Optional | `string` | none | Card header label |
| `underline` | Optional | `boolean` | `false` | Underline the header |
| `className` | Optional | `string` | none | Additional CSS class |

#### `PoolDetailItem` type

```tsx
type PoolDetailItem = {
  title: string
  value?: string | number
}
```

#### Basic usage

```tsx
<PoolDetailsCard
  label="Pool Configuration"
  details={[
    { title: 'Pool URL', value: 'stratum+tcp://pool.example.com:3333' },
    { title: 'Worker', value: 'worker001' },
    { title: 'Algorithm', value: 'SHA-256' },
  ]}
/>
```

#### More examples

<Accordions>
  <Accordion title="With underline">

```tsx
<PoolDetailsCard
  label="Primary Pool"
  underline
  details={[
    { title: 'URL', value: 'stratum://pool1.example.com:3333' },
    { title: 'Status', value: 'Active' },
    { title: 'Hashrate', value: '95.2 TH/s' },
    { title: 'Accepted', value: 12847 },
    { title: 'Rejected', value: 23 },
  ]}
/>
```

  </Accordion>
  <Accordion title="Empty state">

When `details` is an empty array, displays "No data available":

```tsx
<PoolDetailsCard
  label="Backup Pool"
  details={[]}
/>
```

  </Accordion>
</Accordions>

#### Styling

- `.mdk-pool-details-card`: Root element
- `.mdk-pool-details-card__header`: Header container
- `.mdk-pool-details-card__header--underline`: Underlined header modifier
- `.mdk-pool-details-card__label`: Header label text
- `.mdk-pool-details-card__list`: Details list container
- `.mdk-pool-details-card__item`: Individual detail row
- `.mdk-pool-details-card__item-title`: Detail title
- `.mdk-pool-details-card__item-value`: Detail value
- `.mdk-pool-details-card__empty`: Empty state container

### `PoolDetailsPopover`

<ComponentDescription name="PoolDetailsPopover" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `details` | Required | `PoolDetailItem[]` | none | Rows rendered inside the dialog via `PoolDetailsCard` |
| `title` | Optional | `string` | none | Dialog title |
| `description` | Optional | `string` | none | Dialog description |
| `triggerLabel` | Optional | `string` | none | Trigger button label |
| `disabled` | Optional | `boolean` | `false` | Disable the trigger button |
| `className` | Optional | `string` | none | Additional CSS class |

#### Basic usage

```tsx
<PoolDetailsPopover
  triggerLabel="View Pool Details"
  title="Primary Pool"
  details={[
    { title: 'URL', value: 'stratum://pool.example.com:3333' },
    { title: 'Worker', value: 'worker001' },
    { title: 'Status', value: 'Active' },
  ]}
/>
```

#### More examples

<Accordions>
  <Accordion title="With description">

```tsx
<PoolDetailsPopover
  triggerLabel="Pool Info"
  title="Pool Configuration"
  description="Current mining pool settings"
  details={poolDetails}
/>
```

  </Accordion>
  <Accordion title="Disabled state">

```tsx
<PoolDetailsPopover
  triggerLabel="View Details"
  details={[]}
  disabled={!hasPoolData}
/>
```

  </Accordion>
</Accordions>

#### Styling

- `.mdk-pool-details-popover`: Root element
- `.mdk-pool-details-popover__body`: Dialog body container

### `StatsExport`

<ComponentDescription name="StatsExport" />

{/*Demo nav lists this as "Stats Export Dropdown"; the code export is `StatsExport`.*/}

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `onCsvExport` | Required | `function` | none | Called when CSV is selected; awaited while a spinner is shown |
| `onJsonExport` | Required | `function` | none | Called when JSON is selected; awaited while a spinner is shown |
| `disabled` | Optional | `boolean` | `false` | Disable the trigger button |
| `showLabel` | Optional | `boolean` | `false` | When `false` (default), shows the "Export" text label next to the icon; when `true`, hides the label (icon-only trigger) |

`fetchStats`, `downloadCsv`, and `downloadJson` are **app-level** helpers—you implement them (or call your API) inside `onCsvExport` and `onJsonExport`. They are not exported from `@tetherto/mdk-react-devkit`.

#### Basic usage

```tsx
<StatsExport
  onCsvExport={async () => {
    const data = await yourApp.fetchDashboardStats()
    yourApp.downloadCsv(data, 'mining-stats.csv')
  }}
  onJsonExport={async () => {
    const data = await yourApp.fetchDashboardStats()
    yourApp.downloadJson(data, 'mining-stats.json')
  }}
/>
```

#### More examples

<Accordions>
  <Accordion title="Disabled state">

```tsx
<StatsExport
  disabled={!hasData}
  onCsvExport={handleCsvExport}
  onJsonExport={handleJsonExport}
/>
```

  </Accordion>
  <Accordion title="With loading state">

The component automatically shows a loading spinner during export operations:

```tsx
<StatsExport
  onCsvExport={async () => {
    await generateAndDownloadCsv()
  }}
  onJsonExport={async () => {
    await generateAndDownloadJson()
  }}
/>
```

  </Accordion>
  <Accordion title="Export formats">

The dropdown provides two export options:

1. **CSV**: Comma-separated values for spreadsheet applications
2. **JSON**: JavaScript Object Notation for programmatic use

  </Accordion>
</Accordions>

#### Styling

- `.stats-export__button`: Trigger button
- `.stats-export__button--disabled`: Disabled state
- `.stats-export__label`: Button label text
- `.stats-export__divider`: Divider between icon and arrow
- `.stats-export__dropdown`: Dropdown menu container
- `.stats-export__item`: Dropdown menu item
- `.stats-export__item--bordered`: Item with bottom border

### `SupplyLiquidBox`

<ComponentDescription name="SupplyLiquidBox" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Optional | `Device` | none | Container device record with supply liquid snap stats |
| `containerSettings` | Optional | `SupplyLiquidBoxContainerSettings \| null` | `null` | Hydro threshold overrides (`waterTemperature`, `supplyLiquidPressure`) for color and flash |

#### `SupplyLiquidBoxContainerSettings` type

```tsx
type SupplyLiquidBoxContainerSettings = {
  thresholds?: Record<string, Record<string, number>>
}
```

Use hydro keys under `thresholds` (not snap stat names such as `supply_liquid_temp`).

```tsx

const containerDevice: Device = {
  id: 'bitmain-hydro-1',
  type: 'bitmain-hydro',
  last: {
    snap: {
      stats: {
        status: 'running',
        supply_liquid_temp: 35,
        supply_liquid_set_temp: 32,
        supply_liquid_pressure: 2.5,
      },
    },
  },
}
```

#### Basic usage

```tsx
<SupplyLiquidBox data={containerDevice} />
```

#### More examples

<Accordions>
  <Accordion title="With threshold settings">

`containerSettings.thresholds` uses hydro keys `waterTemperature` and `supplyLiquidPressure`. Snap fields such as `supply_liquid_temp` on the device record are separate and are not threshold config keys.

```tsx
<SupplyLiquidBox
  data={containerDevice}
  containerSettings={{
    thresholds: {
      waterTemperature: {
        criticalLow: 21,
        alarmLow: 21,
        normal: 30,
        alarmHigh: 37,
        criticalHigh: 40,
      },
      supplyLiquidPressure: {
        criticalLow: 2,
        alarmLow: 2,
        normal: 2.3,
        alarmHigh: 3.5,
        criticalHigh: 4,
      },
    },
  }}
/>
```

  </Accordion>
</Accordions>

#### Features

The component displays three `SingleStatCard` items:
- **Supply Liquid** / subtitle **Temp**: current liquid temperature
- **Supply Liquid** / subtitle **Set Temp**: target set temperature
- **Supply Liquid** / subtitle **Pressure**: current pressure

Each stat automatically shows color and flash indicators based on threshold settings.

#### Styling

- `.mdk-supply-liquid-box`: Root element
- `.mdk-supply-liquid-box__stats`: Stats container

### `TanksBox`

<ComponentDescription name="TanksBox" />

For the per-row primitive and standalone usage, see [`TankRow`](/v0-4-0/ui/react/foundation/operations/containers#tankrow) on the containers page.

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Optional | `TanksBoxData` | none | Oil pump, water pump, and pressure tank readings; renders nothing when omitted |

#### `TanksBoxData` type

```tsx
type Tank = {
  cold_temp_c: number
  enabled: boolean
  color?: string      // Visual indicator color
  flash?: boolean     // Enable flash animation
  tooltip?: string    // Tooltip text
}

type TanksBoxData = {
  oil_pump: Tank[]
  water_pump: { enabled: boolean }[]
  pressure: { value?: number; flash?: boolean; color?: string; tooltip?: string }[]
}
```

#### Composition

`TanksBox` maps each `oil_pump` entry to one [`TankRow`](/v0-4-0/ui/react/foundation/operations/containers#tankrow) by array index:

```tsx
// Per-row mapping (index i):
oil_pump[i].enabled       -> oilPumpEnabled
oil_pump[i].cold_temp_c   -> temperature (always Celsius)
water_pump[i]?.enabled    -> waterPumpEnabled
pressure[i] ?? {}         -> pressure
oil_pump[i].{color,flash,tooltip} -> temperature cell hints
```

#### Basic usage

```tsx
<TanksBox
  data={{
    oil_pump: [
      { cold_temp_c: 45, enabled: true },
      { cold_temp_c: 42, enabled: true },
    ],
    water_pump: [
      { enabled: true },
      { enabled: false },
    ],
    pressure: [
      { value: 1.2 },
      { value: 1.1 },
    ],
  }}
/>
```

#### More examples

<Accordions>
  <Accordion title="With visual alerts">

```tsx
<TanksBox
  data={{
    oil_pump: [
      { cold_temp_c: 85, enabled: true, color: 'red', flash: true, tooltip: 'High temp!' },
    ],
    water_pump: [{ enabled: true }],
    pressure: [{ value: 2.5, color: 'orange', flash: true }],
  }}
/>
```

  </Accordion>
  <Accordion title="Multiple tanks">

```tsx
<TanksBox
  data={{
    oil_pump: [
      { cold_temp_c: 42, enabled: true },
      { cold_temp_c: 44, enabled: true },
      { cold_temp_c: 41, enabled: false },
    ],
    water_pump: [{ enabled: true }, { enabled: false }, { enabled: true }],
    pressure: [{ value: 1.1 }, { value: 1.3 }, {}],
  }}
/>
```

  </Accordion>
  <Accordion title="With custom tooltips">

```tsx
<TanksBox
  data={{
    oil_pump: [
      { cold_temp_c: 42, enabled: true, tooltip: 'Within normal range' },
      {
        cold_temp_c: 55,
        enabled: true,
        tooltip: 'Approaching threshold – consider cooling',
      },
    ],
    water_pump: [{ enabled: true }, { enabled: true }],
    pressure: [{ value: 1.2 }, { value: 1.4, tooltip: 'Pressure slightly elevated' }],
  }}
/>
```

  </Accordion>
  <Accordion title="Mixed pump states">

```tsx
<TanksBox
  data={{
    oil_pump: [
      { cold_temp_c: 43, enabled: true },
      { cold_temp_c: 44, enabled: false },
      { cold_temp_c: 42, enabled: true },
    ],
    water_pump: [{ enabled: false }, { enabled: true }, { enabled: false }],
    pressure: [{ value: 1.1 }, { value: 1.2 }, { value: 1.0 }],
  }}
/>
```

  </Accordion>
  <Accordion title="No data">

When `data` is omitted, `TanksBox` returns `null` and renders nothing:

```tsx
<TanksBox />
```

  </Accordion>
</Accordions>

#### Styling

- `.mdk-tanks-box`: Root element
- Uses [`TankRow`](/v0-4-0/ui/react/foundation/operations/containers#tankrow) internally for each tank display

## Related hooks

The following adapter data hooks supply the live values consumed by the widgets on this page.

| Hook | Supplies |
|------|---------|
| [`useSiteHashrate`](/v0-4-0/reference/app-toolkit/hooks/data#usesitehashrate) | Aggregate hashrate in PH/s for hashrate stat boxes |
| [`useSiteMinerCounts`](/v0-4-0/reference/app-toolkit/hooks/data#usesiteminercounts) | Online, offline, and error miner counts |
| [`useSiteMinerStats`](/v0-4-0/reference/app-toolkit/hooks/data#usesiteminerstats) | Live MOS total and realtime miner breakdown (stat-rtd) |
| [`useSiteEfficiency`](/v0-4-0/reference/app-toolkit/hooks/data#usesiteefficiency) | W/TH/s efficiency value |
| [`useSiteConsumption`](/v0-4-0/reference/app-toolkit/hooks/data#usesiteconsumption) | Latest site power consumption in MW |
| [`usePoolStats`](/v0-4-0/reference/app-toolkit/hooks/data#usepoolstats) | Aggregate pool worker counts and hashrate for pool summary cards |
| [`useHashrateChartData`](/v0-4-0/reference/app-toolkit/hooks/data#usehashratechartdata) | Site + per-pool hashrate time-series for chart widgets |

# MicroBT (/v0-4-0/ui/react/foundation/dashboard/stats/microbt)

Summary widget for MicroBT containers. For other vendor widgets see [Bitmain](/v0-4-0/ui/react/foundation/dashboard/stats/bitmain). For vendor-agnostic cards see [Generic widgets](/v0-4-0/ui/react/foundation/dashboard/stats/generic-widgets).

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- A [`Device`](/v0-4-0/reference/app-toolkit/ui-kit/types#device) record of type `microbt`

## Components

<ComponentTable category="Dashboard" subcategory="MicroBT" pkg="foundation" />

### `MicroBTWidgetBox`

<ComponentDescription name="MicroBTWidgetBox" />

{/*Returns `null` when `data` is not provided. The first tile is labelled `"Cicle Pump"` in the source (typo for `"Cycle Pump"`); the label is
intentionally mirrored here to match the rendered UI.*/}

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Optional | `Device` | none | MicroBT container device record; returns `null` when omitted |

```tsx

const microbtContainer: Device = {
  id: 'microbt-1',
  type: 'microbt',
  last: {
    snap: {
      stats: {
        status: 'running',
        container_specific: {
          cdu: {
            circulation_pump_running_status: 'running',
            cooling_fan_control: true,
          },
        },
      },
    },
  },
}
```

#### Basic usage

```tsx
<MicroBTWidgetBox data={microbtContainer} />
```

#### Indicators rendered

{/* Maintainer note: `Cicle Pump` is a known typo in foundation source (`micro-bt-widget-box.tsx`); docs mirror the UI string until product fixes the label. */}

The label **`Cicle Pump`** matches the rendered UI and foundation source (typo for "Cycle Pump"); keep documentation aligned until the component string is corrected in code.

- `Cicle Pump`: green `Running` when `cdu.circulation_pump_running_status` equals `CONTAINER_STATUS.RUNNING`, gray `Off` otherwise
- `Cooling Fan`: green `Running` when `cdu.cooling_fan_control` is truthy, red `Error` otherwise

#### Styling

- `.mdk-micro-bt-widget-box`: Root grid element
- `.mdk-micro-bt-widget-box__item`: Single indicator cell
- `.mdk-micro-bt-widget-box__title`: Indicator label text

# Inventory (/v0-4-0/ui/react/foundation/inventory)

<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Inventory components display historical device movement details. Pair with the [Operations centre](/v0-4-0/ui/react/foundation/operations) for full device-management context.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)

## Components

<ComponentTable category="Inventory" pkg="foundation" />

### `MovementDetailsModal`

<ComponentDescription name="MovementDetailsModal" />

Modal showing the details of a historical device movement — the device summary plus the origin → destination transition of location and status. All three props are optional (the component renders `null` when `movement` is absent), so you can gate the modal on a selection without separate conditional rendering:

1. **Device details** (`movement.device`): code, type, site, container, serial number, MAC address.
2. **Movement preview**: side-by-side origin and destination panels showing location and status with colour-coded badges.
3. **Comments** (`movement.comments`): optional free-text or node rendered below the movement panels.

The component does no data fetching — the parent is responsible for passing the selected movement.

#### Import

```tsx
  MovementDetailsModalProps,
  MovementData,
  MovementDevice,
} from '@tetherto/mdk-react-devkit/foundation'
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `isOpen` | Optional | `boolean` | `false` | When `true`, the modal is visible |
| `onClose` | Optional | `function` | none | Called when the user dismisses the modal |
| `movement` | Optional | `MovementData` | none | Movement record to display; modal renders `null` when absent |

#### `MovementData` type

```tsx
type MovementData = {
  origin: string
  destination: string
  previousStatus: string
  newStatus: string
  device?: MovementDevice
  comments?: ReactNode
}
```

#### `MovementDevice` type

```tsx
type MovementDevice = Partial<{
  code: string
  tags: string[]
  type: string
  info: Partial<{
    subType: string
    site: string
    container: string
    serialNum: string
    macAddress: string
  }>
}>
```

#### Basic usage

```tsx

function InventoryTable({ movements }) {
  const [selected, setSelected] = useState<MovementData | null>(null)

  return (
    <>
      {/* … table that calls setSelected(row.movement) on row click … */}

      <MovementDetailsModal
        isOpen={selected !== null}
        movement={selected ?? undefined}
        onClose={() => setSelected(null)}
      />
    </>
  )
}
```

#### Behavior notes

- `movement` is the primary gate: when it is absent the component returns `null` and nothing is mounted, regardless of `isOpen`. `isOpen` only controls visibility once a `movement` is provided
- Location and status badge colours come from the MDK design-token maps (`MINER_LOCATION_*`, `MINER_STATUS_*`)
- The modal title is fixed: **"Historical Device Update"**

## Next steps

- For device-level details, see the [Operations centre](/v0-4-0/ui/react/foundation/operations) pages

# Operations centre (/v0-4-0/ui/react/foundation/operations)

Domain-specific components for Bitcoin mining operations monitoring and management.

<FoundationComponentTablesByCategory />

## Next steps

- **[@tetherto/mdk-react-devkit/core components](/v0-4-0/ui/react/core/components)**: Base components used by the operations centre (Cards, Charts, Tables)
- **[Settings](/v0-4-0/ui/react/foundation/settings)**: Administrative settings UI for feature flags, user management, and configuration
- **[Hooks](/v0-4-0/reference/app-toolkit/hooks/components#charts)**: React hooks for real-time monitoring data

# Container components (/v0-4-0/ui/react/foundation/operations/containers)

Components for viewing, navigating, and controlling Bitcoin mining containers, racks, and associated hardware.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)

## Container subpages

Dedicated pages for the socket primitive and vendor-specific UI:

- [Socket](/v0-4-0/ui/react/foundation/operations/containers/socket) — PDU socket tile primitive
- [Bitdeer](/v0-4-0/ui/react/foundation/operations/containers/bitdeer)
- [Bitmain](/v0-4-0/ui/react/foundation/operations/containers/bitmain)
- [Bitmain Immersion](/v0-4-0/ui/react/foundation/operations/containers/bitmain-immersion)
- [MicroBT](/v0-4-0/ui/react/foundation/operations/containers/microbt)

## All container components

Every component in this category, including shared chrome and vendor-specific widgets.

<ComponentTable category="Containers" pkg="foundation" />

## Shared components

Container-authoring primitives that work with any container device record. Use these as building blocks inside the vendor-specific pages above, or 
when composing your own container view. For the PDU socket tile, see the dedicated [Socket](/v0-4-0/ui/react/foundation/operations/containers/socket) page.

### `EnabledDisableToggle`

<ComponentDescription name="EnabledDisableToggle" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `value` | Required | `unknown` | none | Current toggle value; boolean enables switch display |
| `tankNumber` | Optional | `number \| string` | required | Tank identifier; empty falls back to "Air Exhaust System" |
| `isButtonDisabled` | Required | `boolean` | none | Disables both Enable and Disable buttons |
| `isOffline` | Required | `boolean` | none | Marks container offline and disables controls |
| `onToggle` | Required | `function` | none | Fires when user clicks Enable or Disable |

#### Basic usage

```tsx
<EnabledDisableToggle
  value={tank.circulationEnabled}
  tankNumber={1}
  isButtonDisabled={false}
  isOffline={false}
  onToggle={({ tankNumber, isOn }) => submitToggle(tankNumber, isOn)}
/>
```

#### More examples

<Accordions>
  <Accordion title="Air exhaust variant">

```tsx
<EnabledDisableToggle
  value={exhaustEnabled}
  tankNumber=""
  isButtonDisabled={false}
  isOffline={false}
  onToggle={({ isOn }) => setExhaust(isOn)}
/>
```

  </Accordion>
</Accordions>

#### Styling

- `.mdk-enabled-disable-toggle`: Root element
- `.mdk-enabled-disable-toggle__toggle`: Label and `Switch` row shown when `value` is boolean

### `GenericDataBox`

<ComponentDescription name="GenericDataBox" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Optional | `DataItem[]` | `[]` | Rows to render as label, value, and units |
| `fallbackValue` | Optional | `unknown` | none | Value used when a row's `value` is undefined |

#### `DataItem` type

```tsx
type DataItem = {
  label?: string
  value?: unknown
  units?: string
  unit?: string          // Alternative unit field
  isHighlighted?: boolean
  color?: string
  flash?: boolean
}
```

#### Basic usage

```tsx
<GenericDataBox
  data={[
    { label: 'Temperature', value: 45, units: '°C' },
    { label: 'Pressure', value: 2.5, units: 'bar', isHighlighted: true },
    { label: 'Status', value: 'Running', color: 'green' },
  ]}
/>
```

#### Styling

- `.mdk-generic-data-box`: Root element
- Renders each row through `DataRow`

### `TankRow`

<ComponentDescription name="TankRow" />

`TankRow` is composed by [`TanksBox`](/v0-4-0/ui/react/foundation/dashboard/stats/generic-widgets#tanksbox) for dashboard tank status; use `TankRow` 
directly when building custom container layouts.

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `label` | Required | `string` | none | Row label shown on the left |
| `temperature` | Required | `number` | none | Temperature reading displayed in the row |
| `unit` | Required | `string` | none | Unit suffix appended to the temperature |
| `oilPumpEnabled` | Required | `boolean` | none | Drives the oil pump indicator state |
| `waterPumpEnabled` | Required | `boolean` | none | Drives the water pump indicator state |
| `color` | Required | `string` | none | Text color for temperature and pressure cells; empty string uses default theme colors |
| `pressure` | Required | `TankRowPressure` | none | [`Pressure readout object`](#tankrowpressure-type); block not rendered when `value` is omitted |
| `flash` | Optional | `boolean` | none | Enables flash animation on the temp cell |
| `tooltip` | Optional | `string` | none | Overrides the default temperature tooltip |

#### `TankRowPressure` type

```tsx
type TankRowPressure = Partial<{
  value: number
  flash: boolean
  color: string
  tooltip: string
}>
```

#### Basic usage

```tsx
<TankRow
  label="Tank 1"
  temperature={45}
  unit="°C"
  oilPumpEnabled
  waterPumpEnabled={false}
  color=""
  pressure={{ value: 1.2 }}
/>
```

#### More examples

<Accordions>
  <Accordion title="Temperature only">

```tsx
<TankRow
  label="Tank 1"
  temperature={42}
  unit="°C"
  oilPumpEnabled
  waterPumpEnabled
  color=""
  pressure={{}}
/>
```

  </Accordion>
  <Accordion title="With alert state">

```tsx
<TankRow
  label="Tank 2"
  temperature={82}
  unit="°C"
  oilPumpEnabled
  waterPumpEnabled
  color="red"
  flash
  tooltip="Oil temperature above threshold"
  pressure={{ value: 2.4, color: 'orange', flash: true }}
/>
```

  </Accordion>
</Accordions>

#### Styling

- `.mdk-tanks-box__row`: Root row element
- `.mdk-tanks-box__params`: Temperature and pressure group
- `.mdk-tanks-box__param` / `__param-label` / `__param-value`: Individual readouts
- `.mdk-tanks-box__pump-statuses`: Pump indicator group
- `.mdk-tanks-box__pump-status` / `__pump-status-title`: Per-pump indicator

### `ContainerFanLegend`

<ComponentDescription name="ContainerFanLegend" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `index` | Optional | `number \| null` | none | Fan number shown in the badge |
| `enabled` | Optional | `boolean` | `false` | Renders on/off styling and icon color |
| `className` | Optional | `string` | none | Additional CSS class on the root element |

#### Basic usage

```tsx
<ContainerFanLegend index={1} enabled />
<ContainerFanLegend index={2} enabled={false} />
```

#### Styling

- `.mdk-container-fan-legend`: Root element
- `.mdk-container-fan-legend--on` / `--off`: On/off state modifier
- `.mdk-container-fan-legend__number`: Fan number badge
- `.mdk-container-fan-legend__icon` / `--on` / `--off`: Fan icon container

### `ContainerFansCard`

<ComponentDescription name="ContainerFansCard" />

{/*`fansData` items reuse the `PumpItem` type from `PumpBox`. The component renders `fan.index + 1`, so pass zero-based indexes.*/}

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `fansData` | Required | `PumpItem[]` | none | Fan entries; returns `null` when omitted |

#### `PumpItem` type

```tsx
type PumpItem = {
  enabled?: boolean
  index: number
}
```

#### Basic usage

```tsx
<ContainerFansCard
  fansData={[
    { enabled: true, index: 0 },
    { enabled: false, index: 1 },
    { enabled: true, index: 2 },
  ]}
/>
```

#### Styling

- `.mdk-container-fans-card`: Root grid element
- Renders one `ContainerFanLegend` per entry

### `DryCooler`

<ComponentDescription name="DryCooler" />

{/*`data` is typed as `UnknownRecord`. The component reads `container_specific_stats.cooling_system.{dry_cooler, oil_pump, water_pump}` internally.*/}

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Required | `UnknownRecord` | none | Container device record with cooling system stats |

```tsx
const containerDevice = {
  container_specific_stats: {
    cooling_system: {
      dry_cooler: [{ index: 0, enabled: true, fans: [{ enabled: true, index: 0 }] }],
      oil_pump: [{ enabled: true, index: 0 }],
      water_pump: [{ enabled: true, index: 0 }],
    },
  },
}
```

#### Basic usage

```tsx
<DryCooler data={containerDevice} />
```

#### Expected data shape

The component reads the following path from `data`:

```tsx
// data.container_specific_stats.cooling_system
{
  dry_cooler: [
    { index: 0, enabled: true, fans: [{ enabled: true, index: 0 }] },
    { index: 1, enabled: false, fans: [] },
  ],
  oil_pump:   [{ enabled: true, index: 0 }, { enabled: false, index: 1 }],
  water_pump: [{ enabled: true, index: 0 }, { enabled: true,  index: 1 }],
}
```

#### Styling

- `.mdk-dry-cooler`: Root element
- `.mdk-dry-cooler__segment`: Per-cooler group
- `.mdk-dry-cooler__card`: Cooler card with status and fans
- `.mdk-dry-cooler__status`: Title and indicator row
- `.mdk-dry-cooler__title`: Cooler title
- `.mdk-dry-cooler__pumps`: Oil and water pump column

### `PumpBox`

<ComponentDescription name="PumpBox" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `pumpTitle` | Required | `string` | none | Pump label prefix (e.g. `"Oil"`, `"Water"`) |
| `pumpItem` | Required | `PumpItem` | none | Pump entry; returns `null` without a boolean `enabled` |

#### `PumpItem` type

```tsx
type PumpItem = {
  enabled?: boolean
  index: number        // zero-based; shown as index + 1
}
```

#### Basic usage

```tsx
<PumpBox pumpTitle="Oil" pumpItem={{ enabled: true, index: 0 }} />
<PumpBox pumpTitle="Water" pumpItem={{ enabled: false, index: 1 }} />
```

#### Styling

- `.mdk-pump-box`: Root element
- `.mdk-pump-box__status`: Title and indicator row
- `.mdk-pump-box__title`: Pump title text

## Next steps

- For browsing containers, racks, and miners, see the [device explorer](/v0-4-0/ui/react/foundation/operations/device-explorer).
- For container-level controls, see [`ContainerControlsBox`](/v0-4-0/ui/react/foundation/operations/details-view/controls#containercontrolsbox) 
on the Controls page.
- For container summary widgets that surface on the dashboard, see [`TanksBox`](/v0-4-0/ui/react/foundation/dashboard/stats/generic-widgets#tanksbox), 
[`SupplyLiquidBox`](/v0-4-0/ui/react/foundation/dashboard/stats/generic-widgets#supplyliquidbox), and 
[`MinersSummaryBox`](/v0-4-0/ui/react/foundation/dashboard/stats/generic-widgets#minerssummarybox).

# Bitdeer container components (/v0-4-0/ui/react/foundation/operations/containers/bitdeer)

UI for Bitdeer containers. These components consume Bitdeer-shaped device records and render pump state, exhaust fan status, tank charts, and settings.

For primitives shared across all container vendors (`EnabledDisableToggle`, `DryCooler`, etc.), see the [Containers overview](/v0-4-0/ui/react/foundation/operations). The PDU socket tile has its own [Socket](/v0-4-0/ui/react/foundation/operations/containers/socket) page.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- A Bitdeer container device record. The charts and pumps components read from `container_specific_stats.cooling_system`.

## Components

<ComponentTable category="Containers" subcategory="Bitdeer" pkg="foundation" />

### `BitdeerPumps`

<ComponentDescription name="BitdeerPumps" />

{/*Despite the plural name, this renders a single exhaust-fan status pulled from `getBitdeerCoolingSystemData(data)`. Returns `null` if `exhaustFanEnabled` is not a boolean.*/}

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Required | [`UnknownRecord`](/v0-4-0/reference/app-toolkit/ui-kit/types#unknownrecord) | none | Bitdeer container record; must contain exhaust fan status |

#### Basic usage

```tsx
<BitdeerPumps data={bitdeerContainer} />
```

#### Styling

- `.mdk-bitdeer-pumps`: Root element
- `.mdk-bitdeer-pumps__status`: Title and indicator row
- `.mdk-bitdeer-pumps__title`: Label text (`"Exhaust Fan"`)

### `BitdeerSettings`

<ComponentDescription name="BitdeerSettings" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Optional | `UnknownRecord` | `{}` | Bitdeer container device record |

#### Basic usage

```tsx
<BitdeerSettings data={bitdeerContainer} />
```

#### Composition

- Renders `ContainerParamsSettings` with container identity data
- Renders `EditableThresholdForm` wired with Bitdeer-specific color, flash, and superflash helpers for oil temperature and tank pressure

#### Styling

- `.mdk-bitdeer-settings`: Root element
- `.mdk-bitdeer-settings__params`: Container params section
- `.mdk-bitdeer-settings__thresholds`: Editable threshold section

### `BitdeerTankPressureCharts`

<ComponentDescription name="BitdeerTankPressureCharts" />

{/*Thin wrapper around `ContainerChartsBuilder` with a fixed Tank1/Tank2 pressure payload. Accepts `ContainerChartsBuilderProps`; `chartDataPayload` is internal and cannot be overridden.*/}

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `tag` | Required | `string` | none | Container tag used to scope chart data |
| `chartTitle` | Optional | `string` | `'Tank Pressure'` | Chart header title |
| `data` | Required | `UnknownRecord[]` | none | Time-series entries to plot |
| `timeline` | Optional | `string` | `'24h'` | Initial time range (e.g. `'5m'`, `'3h'`, `'1D'`) |
| `dateRange` | Optional | \{ start?: number; end?: number \} | none | Explicit start/end window in ms |
| `fixedTimezone` | Required | `string` | none | Forces a timezone for axis labels |
| `height` | Required | `number` | none | Chart height in pixels |

#### Basic usage

```tsx
<BitdeerTankPressureCharts
  tag="container1"
  data={pressureData}
  timeline="24h"
/>
```

#### Lines rendered

- **Tank1 Pressure**: yellow, backend attribute `tank1_bar_group`
- **Tank2 Pressure**: violet, backend attribute `tank2_bar_group`
- Values are shown in `bar` with 1 decimal place

### `BitdeerTankTempCharts`

<ComponentDescription name="BitdeerTankTempCharts" />

{/*Extends `ContainerChartsBuilderProps` with a `tankNumber` prop. Chart title is derived from `CHART_TITLES.TANK_OIL_TEMP` using `tankNumber`; `chartDataPayload` is internal.*/}

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `tag` | Required | `string` | none | Container tag used to scope chart data |
| `tankNumber` | Optional | `number \| string` | `1` | Tank index used to build backend attributes |
| `data` | Required | `UnknownRecord[]` | none | Time-series entries to plot |
| `timeline` | Optional | `string` | `'24h'` | Initial time range |
| `dateRange` | Optional | \{ start?: number; end?: number \} | none | Explicit start/end window in ms |
| `fixedTimezone` | Required | `string` | none | Forces a timezone for axis labels |
| `height` | Required | `number` | none | Chart height in pixels |

#### Basic usage

```tsx
<BitdeerTankTempCharts
  tag="container1"
  tankNumber={1}
  data={tempData}
  timeline="24h"
/>
```

#### Lines rendered

- `Tank{n} Oil TempL`: yellow (4px), backend attribute `cold_temp_c_{n}_group`
- `Tank{n} Oil TempH`: violet, `hot_temp_c_{n}_group`
- `Tank{n} Water TempL`: blue, `cold_temp_c_w_{n}_group`
- `Tank{n} Water TempH`: sky blue, `hot_temp_c_w_{n}_group`
- Values are shown in `°C`

# Bitmain container components (/v0-4-0/ui/react/foundation/operations/containers/bitmain)

UI for Bitmain hydro containers. These components read from Bitmain-shaped device records and render cooling-system status, power and positioning, liquid-flow charts, and hydro settings.

For primitives shared across all container vendors (`EnabledDisableToggle`, `DryCooler`, etc.), see the [Containers overview](/v0-4-0/ui/react/foundation/operations). The PDU socket tile has its own [Socket](/v0-4-0/ui/react/foundation/operations/containers/socket) page.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- A Bitmain container device record. Most components read from `container_specific_stats` and `container_specific_stats_group_aggr` on the device.

## Components

<ComponentTable category="Containers" subcategory="Bitmain" pkg="foundation" />

### `BitMainBasicSettings`

<ComponentDescription name="BitMainBasicSettings" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Required | [`Device`](/v0-4-0/reference/app-toolkit/ui-kit/types#device) | none | Bitmain container device record |

#### Basic usage

```tsx
<BitMainBasicSettings data={bitmainContainer} />
```

#### Composition

- Renders `BitMainCoolingSystem` with pump and fan indicators
- Renders a `"Power & Positioning"` section containing `BitMainPowerAndPositioning`

#### Styling

- `.mdk-bitmain-basic-settings`: Root element
- `.mdk-bitmain-basic-settings__section`: Section wrapper
- `.mdk-bitmain-basic-settings__title`: Section heading

### `BitMainCoolingSystem`

<ComponentDescription name="BitMainCoolingSystem" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Required | `Device` | none | Bitmain container device record |

#### Basic usage

```tsx
<BitMainCoolingSystem data={bitmainContainer} />
```

#### Indicators rendered

- `Circulating pump`: `circulating_pump`, fault from `circulating_pump_fault`
- `Fluid Infusion pump`: `fluid_infusion_pump`, fault from `fluid_infusion_pump_fault`
- `Fan #1`, `Fan #2`: `fan1`, `fan2`, faults from `fan1_fault`, `fan2_fault`
- `Cooling tower fan #1..#3`: `cooling_tower_fan1..3` with matching `_fault` attributes
- Indicator color is red on fault, green when running, gray otherwise

#### Styling

- `.mdk-bitmain-cooling-system`: Root element
- `.mdk-bitmain-cooling-system__wrapper`: Indicator grid wrapper
- `.mdk-bitmain-cooling-system__row`: Row of indicators
- `.mdk-bitmain-cooling-system__item`: Indicator cell
- `.mdk-bitmain-cooling-system__label`: Label text

### `BitMainHydroLiquidTemperatureCharts`

<ComponentDescription name="BitMainHydroLiquidTemperatureCharts" />

{/*Thin wrapper around `ContainerChartsBuilder` with a fixed secondary supply temperature payload. Accepts `ContainerChartsBuilderProps`; `chartDataPayload` is internal and cannot be overridden.*/}

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `tag` | Required | `string` | none | Container tag used to scope chart data |
| `chartTitle` | Optional | `string` | `'Hydro Liquid Temperature'` | Chart header title |
| `data` | Required | `UnknownRecord[]` | none | Time-series entries to plot |
| `timeline` | Optional | `string` | `'24h'` | Initial time range (e.g. `'5m'`, `'3h'`, `'1D'`) |
| `dateRange` | Optional | \{ start?: number; end?: number \} | none | Explicit start/end window in ms |
| `fixedTimezone` | Required | `string` | none | Forces a timezone for axis labels |
| `height` | Required | `number` | none | Chart height in pixels |
| `showLegend` | Optional | `boolean` | `true` | Shows the series legend |
| `showRangeSelector` | Optional | `boolean` | `true` | Shows the timeline range selector |
| `footer` | Optional | `ReactNode` | none | Custom footer rendered below the chart |

#### Basic usage

```tsx
<BitMainHydroLiquidTemperatureCharts
  tag="container1"
  data={tempData}
  timeline="24h"
/>
```

#### Lines rendered

- **Sec. Liquid supply Temp1**: sky blue, backend attribute `second_supply_temp1_group`
- **Sec. Liquid supply Temp2**: violet, backend attribute `second_supply_temp2_group`
- Values are shown in `°C` with 1 decimal place

### `BitMainHydroSettings`

<ComponentDescription name="BitMainHydroSettings" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Required | `Device` | none | Bitmain Hydro container device record |

#### Basic usage

```tsx
<BitMainHydroSettings data={bitmainContainer} />
```

#### Composition

- Renders `BitMainBasicSettings` for cooling, power, and positioning
- Renders `HydroEditableThresholdForm` wired with Bitmain-specific color, flash, and superflash helpers for supply liquid temperature and pressure

#### Styling

- `.mdk-bitmain-hydro-settings`: Root element
- `.mdk-bitmain-hydro-settings__params`: Basic settings section
- `.mdk-bitmain-hydro-settings__thresholds`: Editable threshold section

### `BitMainLiquidPressureCharts`

<ComponentDescription name="BitMainLiquidPressureCharts" />

{/*Thin wrapper around `ContainerChartsBuilder` with a fixed supply/return liquid pressure payload. Values are converted from MPa to bar; `chartDataPayload` is internal and cannot be overridden.*/}

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `tag` | Required | `string` | none | Container tag used to scope chart data |
| `chartTitle` | Optional | `string` | `'Liquid Pressure'` | Chart header title |
| `data` | Required | `UnknownRecord[]` | none | Time-series entries to plot |
| `timeline` | Optional | `string` | `'24h'` | Initial time range |
| `dateRange` | Optional | \{ start?: number; end?: number \} | none | Explicit start/end window in ms |
| `fixedTimezone` | Required | `string` | none | Forces a timezone for axis labels |
| `height` | Required | `number` | none | Chart height in pixels |
| `showLegend` | Optional | `boolean` | `true` | Shows the series legend |
| `showRangeSelector` | Optional | `boolean` | `true` | Shows the timeline range selector |
| `footer` | Optional | `ReactNode` | none | Custom footer rendered below the chart |

#### Basic usage

```tsx
<BitMainLiquidPressureCharts
  tag="container1"
  data={pressureData}
  timeline="24h"
/>
```

#### Lines rendered

- **Supply Liquid Pressure**: sky blue, backend attribute `supply_liquid_pressure_group`
- **Return Liquid Pressure**: violet, backend attribute `return_liquid_pressure_group`
- Values are shown in `bar` with 3 decimal places; source MPa values are autoconverted with [`convertMpaToBar`](/v0-4-0/reference/app-toolkit/ui-kit/utilities#convertmpatobar).

### `BitMainLiquidTempCharts`

<ComponentDescription name="BitMainLiquidTempCharts" />

{/*Thin wrapper around `ContainerChartsBuilder` with a fixed supply/return liquid temperature payload. `chartDataPayload` is internal and cannot be overridden.*/}

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `tag` | Required | `string` | none | Container tag used to scope chart data |
| `chartTitle` | Optional | `string` | `'Liquid Temperature'` | Chart header title |
| `data` | Required | `UnknownRecord[]` | none | Time-series entries to plot |
| `timeline` | Optional | `string` | `'24h'` | Initial time range |
| `dateRange` | Optional | \{ start?: number; end?: number \} | none | Explicit start/end window in ms |
| `fixedTimezone` | Required | `string` | none | Forces a timezone for axis labels |
| `height` | Required | `number` | none | Chart height in pixels |
| `showLegend` | Optional | `boolean` | `true` | Shows the series legend |
| `showRangeSelector` | Optional | `boolean` | `true` | Shows the timeline range selector |
| `footer` | Optional | `ReactNode` | none | Custom footer rendered below the chart |

#### Basic usage

```tsx
<BitMainLiquidTempCharts
  tag="container1"
  data={tempData}
  timeline="24h"
/>
```

#### Lines rendered

- **Supply Liquid Temp**: sky blue, backend attribute `supply_liquid_temp_group`
- **Return Liquid Temp**: violet, backend attribute `return_liquid_temp_group`
- Values are shown in `°C` with 1 decimal place

### `BitMainPowerAndPositioning`

<ComponentDescription name="BitMainPowerAndPositioning" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Required | `Device` | none | Bitmain container device record |

#### Basic usage

```tsx
<BitMainPowerAndPositioning data={bitmainContainer} />
```

#### Composition

- Renders a `Power` `ContentBox` with distribution box #1 and #2 kW values (read from `distribution_box1_power_w`, `distribution_box2_power_w`)
- Renders a `Location` `ContentBox` with latitude, longitude, and their direction fields

#### Styling

- `.mdk-bitmain-power-positioning`: Root element
- `.mdk-bitmain-power-positioning__panel`: Power or Location panel
- `.mdk-bitmain-power-positioning__section`: Section body
- `.mdk-bitmain-power-positioning__power-item`: Distribution box row
- `.mdk-bitmain-power-positioning__location-grid`: Latitude and longitude grid
- `.mdk-bitmain-power-positioning__location-item`: Coordinate cell
- `.mdk-bitmain-power-positioning__location-details`: Coordinate details
- `.mdk-bitmain-power-positioning__subtitle`: Panel subtitle
- `.mdk-bitmain-power-positioning__value`: Power value text

### `BitMainPowerCharts`

<ComponentDescription name="BitMainPowerCharts" />

{/*Wraps `ContainerChartsBuilder` with a fixed power payload and preprocesses `container_specific_stats_group_aggr` to add a computed `total_power_computed` field (box1 + box2). `chartDataPayload` is internal.*/}

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `tag` | Required | `string` | none | Container tag used to scope chart data |
| `chartTitle` | Optional | `string` | `'Power Consumption'` | Chart header title |
| `data` | Required | `UnknownRecord[]` | none | Time-series entries to plot |
| `timeline` | Optional | `string` | `'24h'` | Initial time range |
| `dateRange` | Optional | \{ start?: number; end?: number \} | none | Explicit start/end window in ms |
| `fixedTimezone` | Required | `string` | none | Forces a timezone for axis labels |
| `height` | Required | `number` | none | Chart height in pixels |
| `showLegend` | Optional | `boolean` | `true` | Shows the series legend |
| `showRangeSelector` | Optional | `boolean` | `true` | Shows the timeline range selector |
| `footer` | Optional | `ReactNode` | none | Custom footer rendered below the chart |

#### Basic usage

```tsx
<BitMainPowerCharts
  tag="container1"
  data={powerData}
  timeline="24h"
/>
```

#### Lines rendered

- **Total Power**: sky blue (3px), computed attribute `total_power_computed` (sum of boxes)
- **Dist. Box 1 Power**: violet, backend attribute `distribution_box1_power_group`
- **Dist. Box 2 Power**: red, backend attribute `distribution_box2_power_group`
- Values are shown in `W` (stored kW is converted with `convertKwToW`) with 2 decimal places

### `BitMainSupplyLiquidFlowCharts`

<ComponentDescription name="BitMainSupplyLiquidFlowCharts" />

{/*Thin wrapper around `ContainerChartsBuilder` with a single supply liquid flow series. `showLegend` defaults to `false` because only one line is drawn; `chartDataPayload` is internal.*/}

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `tag` | Required | `string` | none | Container tag used to scope chart data |
| `chartTitle` | Optional | `string` | `'Supply Liquid Flow'` | Chart header title |
| `data` | Required | `UnknownRecord[]` | none | Time-series entries to plot |
| `timeline` | Optional | `string` | `'24h'` | Initial time range |
| `dateRange` | Optional | \{ start?: number; end?: number \} | none | Explicit start/end window in ms |
| `fixedTimezone` | Required | `string` | none | Forces a timezone for axis labels |
| `height` | Required | `number` | none | Chart height in pixels |
| `showLegend` | Optional | `boolean` | `false` | Shows the series legend |
| `showRangeSelector` | Optional | `boolean` | `true` | Shows the timeline range selector |
| `footer` | Optional | `ReactNode` | none | Custom footer rendered below the chart |

#### Basic usage

```tsx
<BitMainSupplyLiquidFlowCharts
  tag="container1"
  data={flowData}
  timeline="24h"
/>
```

#### Lines rendered

- **Supply Liquid Flow**: sky blue (2px), backend attribute `supply_liquid_flow_group`
- Values are shown in `m³/h` with 2 decimal places

### `StatusItem`

<ComponentDescription name="StatusItem" />

{/*Generic-looking name, but classified under the Bitmain subcategory because that is the only vendor that consumes it today.*/}

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `label` | Required | `string` | none | Label text shown next to the indicator |
| `status` | Optional | `'normal' \| 'warning' \| 'fault' \| 'unavailable'` | `'unavailable'` | Status type; selects color and label |

#### Status mapping

- `normal`: green indicator, label `Normal`
- `warning`: amber indicator, label `Warning`
- `fault`: red indicator, label `Fault`
- `unavailable`: gray indicator, label `Unavailable`

#### Basic usage

```tsx
<StatusItem label="Temperature" status="normal" />
<StatusItem label="Pressure" status="warning" />
<StatusItem label="Flow" status="fault" />
```

#### Styling

- `.mdk-status-item`: Root element
- `.mdk-status-item__content`: Label and indicator row
- `.mdk-status-item__label`: Label text

# Bitmain Immersion container components (/v0-4-0/ui/react/foundation/operations/containers/bitmain-immersion)

UI for Bitmain Immersion containers. These components render unit control boxes, pump-station status, immersion settings, and system-status views on top of Bitmain immersion-shaped device records.

For primitives shared across all container vendors (`EnabledDisableToggle`, `DryCooler`, etc.), see the [Containers overview](/v0-4-0/ui/react/foundation/operations). The PDU socket tile has its own [Socket](/v0-4-0/ui/react/foundation/operations/containers/socket) page.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- A Bitmain Immersion container device record. Most components read pump and temperature fields from `container_specific_stats`.

## Components

<ComponentTable category="Containers" subcategory="Bitmain Immersion" pkg="foundation" />

### `BitMainControlsTab`

<ComponentDescription name="BitMainControlsTab" />

{/*File is named `bitmain-immersion-controls-tab.tsx`, but the exported component is `BitMainControlsTab`. Import path is unchanged.*/}

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Required | [`Device`](/v0-4-0/reference/app-toolkit/ui-kit/types#device) | none | Bitmain immersion container device record |

#### Basic usage

```tsx
<BitMainControlsTab data={immersionContainer} />
```

#### Composition

- Container fan indicator from `container_fan` and `fan_fault` (red on fault, green running, gray off)
- Tank levels for Tank A to Tank D read from `tank_a_level` to `tank_d_level` in cm
- GPS latitude and longitude with direction fields

#### Styling

- `.mdk-bitmain-controls-tab`: Root element
- `.mdk-bitmain-controls-tab__section`: Section wrapper
- `.mdk-bitmain-controls-tab__section-header`: Fan status row
- `.mdk-bitmain-controls-tab__section-label`: Fan label text
- `.mdk-bitmain-controls-tab__title`: Section heading
- `.mdk-bitmain-controls-tab__grid`: Tank or GPS grid
- `.mdk-bitmain-controls-tab__item`: Tank cell
- `.mdk-bitmain-controls-tab__label`: Tank label
- `.mdk-bitmain-controls-tab__value`: Tank value
- `.mdk-bitmain-controls-tab__location`: GPS cell
- `.mdk-bitmain-controls-tab__location-label`: GPS axis label
- `.mdk-bitmain-controls-tab__location-value`: GPS coordinate value
- `.mdk-bitmain-controls-tab__location-dir`: GPS direction text

### `BitMainImmersionControlBox`

<ComponentDescription name="BitMainImmersionControlBox" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `title` | Required | `string` | none | Box title shown above the left column |
| `leftContent` | Required | `ReactNode` | none | Content for the left column |
| `rightContent` | Required | `ReactNode` | none | Content for the right column |
| `bottomContent` | Optional | `ReactNode` | none | Optional content for the bottom row |
| `secondary` | Optional | `boolean` | `false` | Borderless variant |
| `className` | Optional | `string` | none | Additional class name on the root |

#### Basic usage

```tsx
<BitMainImmersionControlBox
  title="Power Status"
  leftContent={<div>Current: 35°C</div>}
  rightContent={<div>Target: 30°C</div>}
  bottomContent={<div>Additional info</div>}
/>
```

#### Styling

- `.mdk-bitmain-immersion-control-box`: Root element
- `.mdk-bitmain-immersion-control-box--secondary`: Borderless variant modifier
- `.mdk-bitmain-immersion-control-box__top`: Two-column wrapper
- `.mdk-bitmain-immersion-control-box__left`: Left column
- `.mdk-bitmain-immersion-control-box__right`: Right column
- `.mdk-bitmain-immersion-control-box__title`: Title text
- `.mdk-bitmain-immersion-control-box__bottom`: Bottom row

### `BitMainImmersionPumpStationControlBox`

<ComponentDescription name="BitMainImmersionPumpStationControlBox" />

{/*A state row (`ready`, `operation`, `start`) is hidden when its prop is `undefined`. When provided as `false`, the label is prefixed with `Not`.*/}

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `title` | Required | `string` | none | Box title |
| `alarmStatus` | Optional | `boolean` | `false` | Shows `Fault` tag when true, `Normal` otherwise |
| `ready` | Required | `boolean` | none | Ready state; row hidden when omitted |
| `operation` | Required | `boolean` | none | Operating state; row hidden when omitted |
| `start` | Required | `boolean` | none | Started state; row hidden when omitted |
| `className` | Optional | `string` | none | Additional class name on the root |

#### Basic usage

```tsx
<BitMainImmersionPumpStationControlBox
  title="Pump Station #1"
  alarmStatus={false}
  ready={true}
  operation={true}
  start={true}
/>
```

#### Styling

- `.mdk-bitmain-immersion-pump-station-control-box`: Root element
- `.mdk-bitmain-immersion-pump-station-control-box__title`: Title text
- `.mdk-bitmain-immersion-pump-station-control-box__content`: Tag and states row
- `.mdk-bitmain-immersion-pump-station-control-box__status`: Alarm tag wrapper
- `.mdk-bitmain-immersion-pump-station-control-box__state`: State label
- `.mdk-bitmain-immersion-pump-station-control-box__state--on`: Active state modifier
- `.mdk-bitmain-immersion-pump-station-control-box__state--off`: Inactive state modifier

### `BitMainImmersionSettings`

<ComponentDescription name="BitMainImmersionSettings" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Required | `Device` | none | Bitmain immersion container device record |
| `containerSettings` | Optional | `object \| null` | `null` | Optional custom oil temperature thresholds. Shape: `{ thresholds?: Record<string, unknown> }` |

#### Basic usage

```tsx
<BitMainImmersionSettings data={immersionContainer} />
```

#### With custom thresholds

```tsx
<BitMainImmersionSettings
  data={immersionContainer}
  containerSettings={{ thresholds: customThresholds }}
/>
```

#### Composition

- Renders `ImmersionEditableThresholdForm` wired with Bitmain immersion color, flash, and superflash helpers for oil temperature
- When `status` is missing on `data`, it defaults to `'active'`

### `BitMainImmersionSystemStatus`

<ComponentDescription name="BitMainImmersionSystemStatus" />

{/*The `Server Start` row only renders when `server_on` is truthy; the `Connection` row always renders and flips between `Connected` and `Disconnected` based on `disconnect`.*/}

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Required | `Device` | none | Bitmain immersion container device record |

#### Basic usage

```tsx
<BitMainImmersionSystemStatus data={immersionContainer} />
```

#### Styling

- `.mdk-bitmain-immersion-system-status`: Root element
- `.mdk-bitmain-immersion-system-status__header`: Header row
- `.mdk-bitmain-immersion-system-status__title`: Title text
- `.mdk-bitmain-immersion-system-status__content`: Status rows wrapper
- `.mdk-bitmain-immersion-system-status__item`: Single status row
- `.mdk-bitmain-immersion-system-status__label`: Row label text

### `BitMainImmersionUnitControlBox`

<ComponentDescription name="BitMainImmersionUnitControlBox" />

{/*Built on top of `BitMainImmersionControlBox`. When `isDryCooler` is true, the status label is forced to `"Dry Cooler"` regardless of `title`. Frequency row is hidden when `frequency` is nullish.*/}

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `title` | Required | `string` | none | Box title shown above the left column |
| `alarmStatus` | Optional | `boolean` | `false` | Shows `Fault` tag when true, `Normal` otherwise |
| `frequency` | Required | `number` | none | Frequency value in Hz; row hidden when omitted |
| `isDryCooler` | Optional | `boolean` | `false` | Forces status label to `"Dry Cooler"` |
| `running` | Optional | `boolean` | `false` | Shows a running or off indicator |
| `showFrequencyInLeftColumn` | Optional | `boolean` | `false` | Renders the frequency row in the left column instead of the right |
| `secondary` | Optional | `boolean` | `false` | Borderless variant |
| `className` | Optional | `string` | none | Additional class name on the root |

#### Basic usage

```tsx
<BitMainImmersionUnitControlBox
  title="Unit #1"
  alarmStatus={false}
  running={true}
  frequency={50}
/>
```

#### Dry cooler variant

```tsx
<BitMainImmersionUnitControlBox
  isDryCooler
  running
  frequency={45}
  showFrequencyInLeftColumn
/>
```

#### Styling

- `.mdk-bitmain-immersion-unit-control-box`: Root element
- `.mdk-bitmain-immersion-unit-control-box__item`: Label and value row (status or frequency)
- `.mdk-bitmain-immersion-unit-control-box__item-label`: Row label text
- `.mdk-bitmain-immersion-unit-control-box__item-value`: Row value text

# MicroBT container components (/v0-4-0/ui/react/foundation/operations/containers/microbt)

UI for MicroBT containers. These components read from MicroBT-shaped device records and render cooling status, fire and water sensors, power meters, and a gauge chart used for temperature readouts.

For primitives shared across all container vendors (`EnabledDisableToggle`, `DryCooler`, etc.), see the [Containers overview](/v0-4-0/ui/react/foundation/operations). The PDU socket tile has its own [Socket](/v0-4-0/ui/react/foundation/operations/containers/socket) page.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- A MicroBT container device record. Cooling components read from `container_specific_stats.cdu`; `PowerMeters` reads from `container_specific_stats.power_meters`.

## Components

<ComponentTable category="Containers" subcategory="MicroBT" pkg="foundation" />

### `GaugeChartComponent`

<ComponentDescription name="GaugeChartComponent" />

{/*Generic-looking name, but classified under the MicroBT subcategory because that is the only vendor that consumes it today. Thin wrapper around `GaugeChart` from `@tetherto/mdk-react-devkit/core` that adds a title and a value/unit readout.*/}

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `max` | Required | `number` | none | Maximum value for the gauge |
| `value` | Required | `number` | none | Current value |
| `unit` | Required | `string` | none | Unit of measurement |
| `label` | Optional | `string` | `''` | Title shown above the gauge |
| `chartStyle` | Optional | `React.CSSProperties` | `{}` | Inline style on the chart wrapper |
| `colors` | Optional | `string[]` | `[COLOR.EMERALD, COLOR.SOFT_TEAL]` | Arc colors in HEX |
| `hideText` | Optional | `boolean` | `true` | Hides the built-in percentage text |
| `height` | Optional | `number` | `200` | Chart height in pixels |
| `className` | Optional | `string` | none | Additional class name on the root |

#### Basic usage

```tsx
<GaugeChartComponent
  max={100}
  value={75.5}
  label="Temperature"
  unit="°C"
/>
```

#### Styling

- `.mdk-gauge-chart-component`: Root element
- `.mdk-gauge-chart-component__title`: Gauge title
- `.mdk-gauge-chart-component__chart`: Chart wrapper
- `.mdk-gauge-chart-component__value`: Value readout row
- `.mdk-gauge-chart-component__value-number`: Numeric value text
- `.mdk-gauge-chart-component__value-unit`: Unit text

### `MicroBTCooling`

<ComponentDescription name="MicroBTCooling" />

{/*Returns `null` when `data` is not provided. Reads cooling status from the `cdu` object on `container_specific_stats`. Pump speed is shown in `%` for Kehua units (`isMicroBTKehua`) and in `Hz` otherwise.*/}

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Required | [`Device`](/v0-4-0/reference/app-toolkit/ui-kit/types#device) | none | MicroBT container device record |

#### Basic usage

```tsx
<MicroBTCooling data={microbtContainer} />
```

#### Composition

- `Cycle Pump` indicator from `cdu.cycle_pump_control` (green `Running`, gray `Off`)
- `Main Circulation Pump` panel: status from `circulation_pump_running_status`, switch from `circulation_pump_switch`, speed from `circulation_pump_speed` in `%` or `Hz`
- `Cooling Fan` panel: status from `cooling_fan_control`, Kehua-only speed from `cooling_system_status`, switch from `cooling_fan_switch`
- `Make Up Pump` panel: status is red on `makeup_water_pump_fault`, green on `makeup_water_pump_control`, gray otherwise; switch from `makeup_water_pump_switch`.

#### Styling

- `.mdk-micro-bt-cooling`: Root element
- `.mdk-micro-bt-cooling__section`: Section wrapper
- `.mdk-micro-bt-cooling__section-title`: Section heading
- `.mdk-micro-bt-cooling__divider`: Horizontal divider between sections
- `.mdk-micro-bt-cooling__row`: Row of items inside a section
- `.mdk-micro-bt-cooling__item-row`: Single-row layout (cycle pump)
- `.mdk-micro-bt-cooling__item`: Item cell
- `.mdk-micro-bt-cooling__label`: Item label text
- `.mdk-micro-bt-cooling__value`: Item value text

### `MicroBTSettings`

<ComponentDescription name="MicroBTSettings" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Required | `Device` | none | MicroBT container device record |
| `containerSettings` | Optional | `object \| null` | `null` | Optional custom inlet temperature thresholds. Shape: `{ thresholds?: Record<string, unknown> }` |

#### Basic usage

```tsx
<MicroBTSettings data={microbtContainer} />
```

#### With custom thresholds

```tsx
<MicroBTSettings
  data={microbtContainer}
  containerSettings={{ thresholds: customThresholds }}
/>
```

#### Composition

- Renders `ContainerParamsSettings` for the common container parameters block
- Renders `MicroBTEditableThresholdForm` wired with MicroBT-specific color, flash, and superflash helpers (`getMicroBtInletTempColor`, `shouldMicroBtTemperatureFlash`, `shouldMicroBtTemperatureSuperflash`) for inlet water temperature

#### Styling

- `.mdk-micro-bt-settings`: Root element
- `.mdk-micro-bt-settings__divider`: Spacer between the params and threshold sections

### `PowerMeters`

<ComponentDescription name="PowerMeters" />

{/*Returns `null` when `data` is not provided or when `container_specific_stats.power_meters` is empty. Renders one `ContentBox` per meter titled `Power Meter {n}`, starting at `1`.*/}

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Required | `Device` | none | MicroBT container device record |

#### Basic usage

```tsx
<PowerMeters data={microbtContainer} />
```

#### Rows rendered

- `Communication Status`: green `Normal` when `status === 1`, red `Error` otherwise
- `Voltage A-B`, `Voltage B-C`, `Voltage C-A`: `voltage_ab`, `voltage_bc`, `voltage_ca` in `V`
- `Total Power Factor`: `total_power_factor`, unitless
- `Frequency`: `freq` in `Hz`
- `Total Active Power`: `total_active_power` in `kW`
- `Total Apparent Power`: `total_apparent_power` in `kVA`
- `Total Active Energy`: `total_active_energy` in `kWh`

#### Styling

- `.mdk-power-meters`: Root element
- `.mdk-power-meters__panel`: Single power-meter panel wrapper

# Socket (/v0-4-0/ui/react/foundation/operations/containers/socket)

`Socket` renders a single PDU socket cell used inside container floor plans and detail views. It pairs socket identity with live power and current readouts, reflects miner mount state, and supports an add-miner flow in edit mode.

For the container vendors that compose sockets into larger layouts, see the [Containers overview](/v0-4-0/ui/react/foundation/operations).

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- A socket payload: index, enabled flag, optional `power_w` / `current_a`,
- (Optional) a mounted [`Miner`](/v0-4-0/reference/app-toolkit/ui-kit/types#device)

### `Socket`

<ComponentDescription name="Socket" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `socket` | Optional | `number \| null` | `null` | Socket index shown in the cell |
| `enabled` | Optional | `boolean` | `false` | Whether the socket is powered |
| `selected` | Optional | `boolean` | `false` | Applies selected visual state |
| `current_a` | Optional | `number \| null` | `null` | Current draw in amperes |
| `power_w` | Optional | `number \| null` | `null` | Power draw in watts |
| `miner` | Optional | `Miner \| null` | `null` | Miner mounted in the socket |
| `heatmap` | Optional | `Heatmap \| null` | `null` | Heatmap configuration; see type below |
| `cooling` | Optional | `boolean \| undefined` | `undefined` | Shows cooling fan state when set |
| `isEditFlow` | Optional | `boolean` | `false` | Renders the add-miner flow UI |
| `clickDisabled` | Optional | `boolean` | `false` | Disables socket click interactions |
| `isEmptyPowerDashed` | Optional | `boolean` | `false` | Shows zero power with dashed border |
| `isContainerControlSupported` | Optional | `boolean` | `false` | Affects tooltip copy for controls |
| `pdu` | Optional | `object` | none | PDU metadata used for `data-pdu-index`. Shape: `{ pdu?: string \| number }` |
| `innerRef` | Optional | `ForwardedRef<HTMLDivElement>` | none | Forwarded ref to the root element |

#### `Heatmap` type

```tsx
type Heatmap = {
  isHeatmapMode?: boolean
  mode?: string       // e.g. 'hashrate' or a temperature field
  ranges?: Record<string, { min?: number; max?: number }>
}
```

#### Basic usage

```tsx
<Socket
  socket={1}
  enabled
  power_w={3250}
  current_a={14.5}
  miner={minerData}
/>
```

#### With heatmap

```tsx
<Socket
  socket={1}
  enabled
  miner={minerData}
  heatmap={{
    isHeatmapMode: true,
    mode: 'chip_avg',
    ranges: { chip_avg: { min: 40, max: 85 } },
  }}
/>
```

#### Styling

- `.mdk-socket`: Root element
- `.mdk-socket--has-cooling`: Cooling fan visible
- `.mdk-socket--heatmap`: Heatmap mode active
- `.mdk-socket--selected`: Selected state
- `.mdk-socket--disabled`: Click disabled
- `.mdk-socket__wrapper`: Content wrapper
- `.mdk-socket__consumption-box`: Power and current readouts
- `.mdk-socket__fan-icon` / `--on`: Cooling fan icon
- `.mdk-socket__value`: Individual power or current cell
- `.mdk-socket__index`: Socket number badge
- `.mdk-socket__add-icon`: Add-miner icon shown in edit flow
- `.mdk-socket__connection-icon`: Clock icon shown while connecting

# Details view (/v0-4-0/ui/react/foundation/operations/details-view)

UI for the details view opened from the [device explorer](/v0-4-0/ui/react/foundation/operations/device-explorer). These components render device identity, telemetry, hardware state, and operator controls for the currently selected miner, selection of miners, or container.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- `<MdkProvider>` from `@tetherto/mdk-react-adapter` wrapping your app tree

  <Accordions>
    <Accordion title="Adapter store hooks used across these components">
      - Several components read the current selection through `useDevices()` (selected devices or containers)
      - They queue operator actions through `useActions()` for power mode, reboot, LED, frequency, and maintenance flows
    </Accordion>
  </Accordions>

- A miner [`Device`](/v0-4-0/reference/app-toolkit/ui-kit/types#device) record (or array of records) shaped per `@tetherto/mdk-react-devkit/foundation`'s `Device` type

  <Accordions>
    <Accordion title="Device fields these components read">
      - Identity and metadata fields on the `Device` itself
      - Live telemetry components additionally read `device.last.snap.stats`
      - Config-driven components additionally read `device.last.snap.config`
    </Accordion>
  </Accordions>

## Component groups

The category is split into four subpages plus this index, which hosts the shared identity card:

- [Charts](/v0-4-0/ui/react/foundation/operations/details-view/charts): fleet-level activity visualizations
- [Chips](/v0-4-0/ui/react/foundation/operations/details-view/chips): per-hashboard ASIC chip telemetry
- [Controls](/v0-4-0/ui/react/foundation/operations/details-view/controls): miner power mode, reboot, LEDs, frequency, maintenance flow, plus the batch and per-container controls cards
- [Fleet management](/v0-4-0/ui/react/foundation/operations/details-view/fleet-management): dialogs for adding, replacing, moving, and removing miners across containers
- [Stats](/v0-4-0/ui/react/foundation/operations/details-view/stats): single and secondary stat cards, aggregated stats, and the combined miner metric card

## All details view components

<ComponentTable category="Details view" pkg="foundation" />

## Shared identity card

`MinerInfoCard` is the only component in this category without a subgroup. It anchors every details view as the identity header (model, serial, firmware, IP) and is reused in batch and single-selection layouts alike.

### `MinerInfoCard`

<ComponentDescription name="MinerInfoCard" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `label` | Optional | `string` | `'Miner info'` | Card header label, rendered uppercase |
| `data` | Required | `InfoItem[]` | none | Title and value rows to display |

The `InfoItem` shape is re-exported from `@tetherto/mdk-react-devkit/foundation`:

```tsx
type InfoItem = {
  title?: string
  value?: string | string[] | number
}
```

When `value` is an array, each entry is rendered on its own line under the title.

#### Basic usage

```tsx
<MinerInfoCard
  label="Miner info"
  data={[
    { title: 'Model', value: 'Antminer S19 Pro' },
    { title: 'Serial', value: 'ANT-2024-00142' },
    { title: 'Firmware', value: '2.1.3' },
    { title: 'IP address', value: '192.168.1.42' },
  ]}
/>
```

#### Multi-line values

Pass an array as `value` to stack rows under a single title (useful for pool URLs, worker names, hashboard ids):

```tsx
<MinerInfoCard
  data={[
    {
      title: 'Pools',
      value: ['stratum+tcp://pool1.example.com:3333', 'stratum+tcp://pool2.example.com:3333'],
    },
    { title: 'Workers', value: ['worker-1', 'worker-2', 'worker-3'] },
  ]}
/>
```

#### Composition

`MinerInfoCard` wraps [`DeviceInfo`](/v0-4-0/reference/app-toolkit/ui-kit/types#deviceinfo) (from `@tetherto/mdk-react-devkit/foundation`'s `info-container`) and adds the uppercase header label. If you need the bare row layout without the card chrome, use `DeviceInfo` directly.

#### Styling

- `.mdk-miner-info-card`: root element
- `.mdk-miner-info-card__label`: uppercase header label
- `.mdk-info-container`: nested per-row title and value wrapper
- `.mdk-info-container__title`: row title
- `.mdk-info-container__value`: row value (one inner `<div>` per array entry)

# Charts (/v0-4-0/ui/react/foundation/operations/details-view/charts)

Chart components that summarize fleet-wide miner state at a glance.

For single-value and aggregated stat cards, see [Stats](/v0-4-0/ui/react/foundation/operations/details-view/stats). For miner identity (`MinerInfoCard`), see the [details view overview](/v0-4-0/ui/react/foundation/operations).

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- A `MinersActivityData` record keyed by status name. Typically derived by grouping a [`Device`](/v0-4-0/reference/app-toolkit/ui-kit/types#device) list by status.

## Components

<ComponentTable category="Details view" subcategory="Charts" pkg="foundation" />

### `MinersActivityChart`

<ComponentDescription name="MinersActivityChart" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Optional | `MinersActivityData` | `{}` | Counts keyed by status name |
| `large` | Optional | `boolean` | `false` | Switch to large indicator and label sizing |
| `isLoading` | Optional | `boolean` | `false` | Render a spinner instead of the chart |
| `isError` | Optional | `boolean` | `false` | Render `CoreAlert` instead of the chart (unless `isDemoMode`) |
| `error` | Optional | `object \| null` | `null` | Message surfaced inside the error alert. Shape: `{ data?: { message?: string } }` |
| `showLabel` | Optional | `boolean` | `true` | Show the per-status text label under each indicator |
| `isDemoMode` | Optional | `boolean` | `false` | Render zeros on error rather than the alert (for demos) |

`MinersActivityData` is an open record. The component renders one indicator per status key it knows about; unknown keys are ignored.

#### Statuses rendered

The chart fixes the order and color of each indicator:

- **Offline**: gray
- **Error**: red, with a tooltip noting that minor errors not affecting hash rate are excluded
- **Sleep**: power mode
- **Low**: yellow, low power mode
- **Normal**: green, normal power mode
- **High**: purple, high power mode
- **Empty**: socket with no miner connected (rendered as `empty` label)

#### Basic usage

```tsx
<MinersActivityChart
  data={{
    total: 100,
    offline: 4,
    error: 1,
    low: 8,
    normal: 80,
    high: 7,
  }}
/>
```

#### Loading and error states

```tsx
<MinersActivityChart isLoading />

<MinersActivityChart
  isError
  error={{ data: { message: 'Failed to fetch activity data' } }}
/>

{/* Demo dashboards: render zeros on error instead of the alert */}
<MinersActivityChart isError isDemoMode />
```

#### Styling

- `.mdk-miners-activity-chart__root`: root flex row
- `.mdk-miners-activity-chart__item`: per-status indicator wrapper
- `.mdk-miners-activity-chart__item--large`: large variant
- `.mdk-miners-activity-chart__label`: status label
- `.mdk-miners-activity-chart__label--large`: large label
- `.mdk-miners-activity-chart__spinner`: loading spinner
- `.mdk-miners-activity-chart__spinner--large`: large spinner

# Chips (/v0-4-0/ui/react/foundation/operations/details-view/chips)

UI for the per-chip thermal and frequency telemetry that surfaces alongside the rest of the details view. `MinerChipsCard` renders a card with one `MinerChip` tile per ASIC; `MinerChip` is also exported on its own for callers that already have per-chip data and want to render their own grid.

For miner identity, see the [details view overview](/v0-4-0/ui/react/foundation/operations). For aggregated stats, see [Stats](/v0-4-0/ui/react/foundation/operations/details-view/stats). For fleet activity, see [Charts](/v0-4-0/ui/react/foundation/operations/details-view/charts).

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- A [`ContainerStats`](/v0-4-0/reference/app-toolkit/ui-kit/types#containerstats) payload with paired `frequency_mhz.chips` and `temperature_c.chips` arrays

  <Accordions>
    <Accordion title="How chip entries are matched and filtered">
      - Chips are matched by `index` across the `frequency_mhz.chips` and `temperature_c.chips` arrays
      - Entries missing any of `min`, `max`, or `avg` temperature are skipped
    </Accordion>
  </Accordions>

## Components

<ComponentTable category="Details view" subcategory="Chips" pkg="foundation" />

### `MinerChipsCard`

<ComponentDescription name="MinerChipsCard" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Required | `ContainerStats` | none | Stats payload containing per-chip frequency and temperature arrays |

The component reads `data.frequency_mhz.chips` and `data.temperature_c.chips`. Chips are joined by `index`. The card renders nothing when no chip pair is complete.

#### Expected chip payload

```tsx
type ContainerStats = {
  frequency_mhz?: {
    chips?: { index: number; current: number }[]
  }
  temperature_c?: {
    chips?: { index: number; max: number; min: number; avg: number }[]
  }
}
```

#### Basic usage

```tsx
<MinerChipsCard
  data={{
    frequency_mhz: {
      chips: [
        { index: 0, current: 500 },
        { index: 1, current: 498 },
      ],
    },
    temperature_c: {
      chips: [
        { index: 0, max: 72, min: 65, avg: 68 },
        { index: 1, max: 70, min: 63, avg: 66 },
      ],
    },
  }}
/>
```

#### Composition

`MinerChipsCard` renders one `MinerChip` per joined entry inside a flex layout. Use `MinerChip` directly when you have already-joined per-chip data and want to skip the join step.

#### Styling

- `.mdk-miner-chips-card`: root element
- `.mdk-miner-chips-card__label`: `Chips` header
- `.mdk-miner-chips-card__chips`: flex container holding the chip tiles

### `MinerChip`

<ComponentDescription name="MinerChip" />

`MinerChip` is the tile rendered inside `MinerChipsCard`. It is exported for callers that already have per-chip data and want to render their own grid (for example, custom layouts or test fixtures).

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `index` | Required | `number` | none | Chip index, displayed as the tile title (`Chip {index}`) |
| `frequency` | Required | \{ current: number \} | none | Current frequency in MHz |
| `temperature` | Required | \{ avg: number; min: number; max: number \} | none | Temperature readings in °C |

#### Basic usage

```tsx
<MinerChip
  index={0}
  frequency={{ current: 500 }}
  temperature={{ avg: 68, min: 65, max: 72 }}
/>
```

#### Layout

The tile shows, in order:

- **Title**: `Chip {index}`
- **Temperature**: average value with °C unit
- **Min and max**: small paired values labelled `min (°C)` and `max (°C)`
- **Frequency**: current value with MHz unit

#### Styling

- `.mdk-miner-chip`: root element
- `.mdk-miner-chip__title`: `Chip {index}` header
- `.mdk-miner-chip__property`: `Temperature` and `Frequency` row labels
- `.mdk-miner-chip__value`: numeric value with unit
- `.mdk-miner-chip__minmax`: min and max value row
- `.mdk-miner-chip__value-type`: small label appended to min and max values

# Controls (/v0-4-0/ui/react/foundation/operations/details-view/controls)

Operator controls for the currently selected miner or container. These components queue actions through `useActions()` and surface confirmation through the standard notification hooks.

For miner identity, see the [details view overview](/v0-4-0/ui/react/foundation/operations). For aggregated stats, see [Stats](/v0-4-0/ui/react/foundation/operations/details-view/stats). For fleet activity, see [Charts](/v0-4-0/ui/react/foundation/operations/details-view/charts).

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- `<MdkProvider>` from `@tetherto/mdk-react-adapter` wrapping your app tree

  <Accordions>
    <Accordion title="Adapter store hooks required by these components">
      - `MinerControlsCard` reads the current device selection from `useDevices()` and pending submissions from `useActions()`, then queues new submissions through `useActions()`
      - `BatchContainerControlsCard` reads the selected containers from `useDevices()`
    </Accordion>
  </Accordions>

- The notification hook [`useNotification`](/v0-4-0/reference/app-toolkit/hooks/components#usenotification) (from `@tetherto/mdk-react-devkit/foundation`) wired up; control actions notify on submit.

## Components

<ComponentTable category="Details view" subcategory="Controls" pkg="foundation" />

### `MinerControlsCard`

<ComponentDescription name="MinerControlsCard" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `buttonsStates` | Optional | `Record<string, boolean \| undefined>` | none | Per-action disable flags. Currently consumed: `isSetUpFrequencyButtonDisabled` |
| `isLoading` | Required | `boolean` | none | Renders the loading spinner and disables every action |
| `showPowerModeSelector` | Optional | `boolean` | `true` | Show the embedded `MinerPowerModeSelectionButtons` row |

#### Available controls

The component renders a different control set depending on the selection and maintenance state:

**Standard mode** (any non-maintenance selection):

- **Power mode selector**: embedded `MinerPowerModeSelectionButtons` (toggle via `showPowerModeSelector`)
- **Reboot**: dispatches `ACTION_TYPES.REBOOT` for every selected miner
- **Setup Freq. Settings**: opens the frequency dropdown and dispatches `ACTION_TYPES.SETUP_FREQUENCY_SPEED` with the chosen value
- **LEDs on / LEDs off**: dispatches `ACTION_TYPES.SET_LED` for miners whose LED is not already in the requested state
- **Move to Maintenance**, **Change miner info**, **Change position**: single-device actions that open the matching dialog flow

**Single-miner-in-maintenance mode**:

- **Change Miner Info**: opens the change-info dialog
- **Back from Maintenance**: opens the container-selection dialog (disabled until the device has a MAC address)
- **Remove Miner**: opens the remove-miner dialog

The card returns `null` when more than one miner is selected and every selected miner is already in maintenance.

#### Basic usage

```tsx
<MinerControlsCard
  buttonsStates={{ isSetUpFrequencyButtonDisabled: false }}
  isLoading={false}
/>
```

#### Hide the power mode selector

```tsx
<MinerControlsCard
  buttonsStates={buttonsStates}
  isLoading={isLoading}
  showPowerModeSelector={false}
/>
```

#### Composition

`MinerControlsCard` composes `MinerPowerModeSelectionButtons`, `MinerSetupFrequencyDropdown`, and four dialogs (`ContainerSelectionDialog`, `RemoveMinerDialog`, `AddReplaceMinerDialog`, `PositionChangeDialog`). The dialogs are mounted unconditionally and toggle visibility through internal state; no wiring required from the caller.

#### Styling

- `.mdk-miner-controls`: root element
- `.mdk-miner-controls__label`: `Miner Controls` header
- `.mdk-miner-controls__content`: controls container
- `.mdk-miner-controls__loader`: loading spinner
- `.mdk-miner-controls__grid`: standard mode button grid
- `.mdk-miner-controls__full-width`: full-width button slot
- `.mdk-miner-controls__maintenance-stack`: maintenance mode action stack
- `.mdk-miner-controls__single-device-stack`: single-device action stack

### `MinerPowerModeSelectionButtons`

<ComponentDescription name="MinerPowerModeSelectionButtons" />

`MinerPowerModeSelectionButtons` is rendered inside `MinerControlsCard` by default but is exported as a peer for layouts that want the power-mode dropdown without the rest of the control card (for example, an `Alarm` rail or a custom toolbar).

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `selectedDevices` | Optional | [`Device`](/v0-4-0/reference/app-toolkit/ui-kit/types#device)`[]` | `[]` | Miners targeted by the dropdown; used to derive the model groups |
| `setPowerMode` | Required | `function` | none | Callback fired with the matching device subset when a mode is chosen |
| `connectedMiners` | Required | `Device[]` | none | Used to compute the current power mode summary when `powerModesLog` is not provided |
| `powerModesLog` | Required | [`UnknownRecord`](/v0-4-0/reference/app-toolkit/ui-kit/types#unknownrecord) | none | Pre-computed per-model current power modes (overrides the derived value) |
| `disabled` | Optional | `boolean` | none | Disable every dropdown |
| `hasMargin` | Optional | `boolean` | `false` | Add the `--with-margin` modifier on the root |

The component groups `selectedDevices` by miner model (via `getDeviceModel`) and renders one dropdown per group. When the selection contains a single model, the button reads `Set Power Mode`; with multiple models, the model name is appended (`Set Power Mode (S19 Pro)`).

#### Basic usage

```tsx
<MinerPowerModeSelectionButtons
  selectedDevices={selectedMiners}
  setPowerMode={(devices, mode) => {
    console.log('Set power mode:', mode, 'for', devices.length, 'miners')
  }}
/>
```

#### With pre-computed power modes

```tsx
<MinerPowerModeSelectionButtons
  selectedDevices={selectedMiners}
  powerModesLog={powerModesByModel}
  setPowerMode={handlePowerModeChange}
  disabled={isLoading}
/>
```

#### Styling

- `.mdk-miner-power-mode-selection-buttons`: root element
- `.mdk-miner-power-mode-selection-buttons--with-margin`: margin modifier

### `BatchContainerControlsCard`

<ComponentDescription name="BatchContainerControlsCard" />

`BatchContainerControlsCard` is the container-level counterpart to `MinerControlsCard`. It renders inside the details view when one or more containers are selected and switches between batch and single-container layouts based on selection size.

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `isBatch` | Optional | `boolean` | `true` | Toggle the title between `Batch Container Controls` and `Container Controls` |
| `isCompact` | Optional | `boolean` | none | Forwarded to `ContainerControlsBox` for the compact layout |
| `connectedMiners` | Optional | `unknown` | none | Forwarded to `ContainerControlsBox` when exactly one container is selected |
| `alarmsDataItems` | Optional | `TimelineItemData[]` | none | Forwarded alarm timeline data |
| `onNavigate` | Optional | `function` | no-op | Forwarded navigation handler |

The card reads the selected containers from `useDevices()` and uses the selection to derive the device record passed into `ContainerControlsBox`. With a single container selected, the full record (plus `connectedMiners`) is forwarded; with multiple containers, only `type` is forwarded, and only when every selected container shares the same model (otherwise `type` is `undefined`, which forces the batch layout).

#### Basic usage

```tsx
<BatchContainerControlsCard
  alarmsDataItems={alarmTimelineItems}
  onNavigate={(path) => router.push(path)}
/>
```

#### Single-container compact layout

```tsx
<BatchContainerControlsCard
  isBatch={false}
  isCompact
  connectedMiners={connectedMinerCount}
  alarmsDataItems={alarmTimelineItems}
  onNavigate={(path) => router.push(path)}
/>
```

#### Composition

`BatchContainerControlsCard` is a thin shell that renders `ContentBox` (with the batch or single title) wrapping `ContainerControlsBox`. Use `ContainerControlsBox` directly if you need to drive the controls from props instead of the adapter store.

#### Styling

- `.mdk-batch-container-controls-card`: root element
- `.mdk-batch-container-controls-card__controls`: controls wrapper inside the content box

### `ContainerControlsBox`

<ComponentDescription name="ContainerControlsBox" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Optional | [`Device`](/v0-4-0/reference/app-toolkit/ui-kit/types#device) | none | Container device data |
| `isBatch` | Optional | `boolean` | `false` | Enable batch operations mode |
| `selectedDevices` | Optional | `Device[]` | `[]` | Selected devices for batch operations |
| `pendingSubmissions` | Optional | `PendingSubmission[]` | `[]` | Pending action submissions |
| `alarmsDataItems` | Optional | `TimelineItemData[]` | `[]` | Active alarms to display |
| `tailLogData` | Optional | [`UnknownRecord`](/v0-4-0/reference/app-toolkit/ui-kit/types#unknownrecord)`[]` | none | Tail log data for power modes |
| `onNavigate` | Required | `function` | none | Navigation handler |

#### Basic usage

```tsx
<ContainerControlsBox
  data={container}
  onNavigate={(path) => router.push(path)}
/>
```

#### Batch operations

```tsx
<ContainerControlsBox
  isBatch
  selectedDevices={selectedContainers}
  pendingSubmissions={pending}
  alarmsDataItems={alarms}
  onNavigate={handleNavigate}
/>
```

#### Container type features

The component automatically adapts based on container type:

- **Bitdeer**: Start/Stop, Reset Alarm, Tank 1/2 toggles, Air Exhaust toggle, Socket controls
- **Bitmain Hydro**: Start/Stop Cooling, PID Mode display
- **Bitmain Immersion**: Start/Stop Cooling, PID Mode, Running Mode, System Status
- **MicroBT**: Start/Stop Cooling, Socket controls

#### Styling

- `.mdk-container-controls-box`: Root element
- `.mdk-container-controls-box__buttons-row`: Action buttons row
- `.mdk-container-controls-box__toggles-row`: Toggle switches row
- `.mdk-container-controls-box__toggle`: Individual toggle container
- `.mdk-container-controls-box__bulk-row`: Bulk action buttons row

# Fleet management (/v0-4-0/ui/react/foundation/operations/details-view/fleet-management)

Dialogs that orchestrate the miner lifecycle launched from the details view: registering a new miner in an empty socket, replacing an existing miner, moving a miner to or from maintenance, changing position between sockets, and permanently removing a miner.

For miner identity, see the [details view overview](/v0-4-0/ui/react/foundation/operations). For the buttons that open most of these dialogs, see [Controls](/v0-4-0/ui/react/foundation/operations/details-view/controls).

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- `<MdkProvider>` from `@tetherto/mdk-react-adapter` wrapping your app tree

  <Accordions>
    <Accordion title="Adapter store hooks required by these dialogs">
      - All dialogs queue position, maintenance, and removal updates through `useActions()`
      - Submissions are validated against existing pending entries, so pending submissions from `useActions()` must be readable
      - The host application is responsible for draining the pending queue and submitting the action batch upstream
    </Accordion>
  </Accordions>

- A miner [`Device`](/v0-4-0/reference/app-toolkit/ui-kit/types#device) record (or socket payload containing one) passed in via dialog props

  <Accordions>
    <Accordion title="Socket payload shape these dialogs read">
      - `selectedEditSocket` and `selectedSocketToReplace` are socket payloads from the device explorer, each carrying a `miner` (`Device`), a `containerInfo` object holding `container`, plus `pos` and `socket` strings
      - The dialogs read `miner.code`, `miner.tags`, `miner.rack`, `miner.id`, and `miner.info` for short-codes, position text, and the dispatched payload
    </Accordion>
  </Accordions>

- The notification helpers from `@tetherto/mdk-react-devkit/foundation` wired up; submit actions fire info or success notifications

## Components

<ComponentTable category="Details view" subcategory="Fleet management" pkg="foundation" />

### `PositionChangeDialog`

<ComponentDescription name="PositionChangeDialog" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `open` | Required | `boolean` | none | Controls dialog visibility |
| `onClose` | Required | `function` | none | Called when the dialog closes; receives the active flow key so callers can decide whether to reset selection state |
| `selectedSocketToReplace` | Required | [`UnknownRecord`](/v0-4-0/reference/app-toolkit/ui-kit/types#unknownrecord) | none | Socket payload for the destination socket in a position-change or replace flow |
| `selectedEditSocket` | Required | [`UnknownRecord`](/v0-4-0/reference/app-toolkit/ui-kit/types#unknownrecord) | none | Socket payload for the miner being edited, replaced, or moved |
| `onChangePositionClicked` | Required | `function` | none | Fired when the user picks "Change position" in the default flow |
| `onPositionChangedSuccess` | Required | `function` | none | Fired after a successful confirm-change-position submission |
| `isContainerEmpty` | Optional | `boolean` | `false` | Hint for the container-selection step that the destination container has no occupied sockets |
| `dialogFlow` | Optional | `string` | `''` | Initial flow key from [`POSITION_CHANGE_DIALOG_FLOWS`](/v0-4-0/reference/app-toolkit/ui-kit/constants#position_change_dialog_flows); when omitted the default chooser is shown |

#### Internal flows

The dialog routes between step-specific surfaces based on the active `dialogFlow` value:

| Flow key | Surface rendered |
|----------|------------------|
| `confirmChange` | `ConfirmChangePositionDialogContent` |
| `containerSelection` | `ContainerSelectionDialogContent` |
| `replaceMiner`, `changeInfo` | `AddReplaceMinerDialogContent` |
| `confirmRemove` | `RemoveMinerDialogContent` |
| `maintenance` | `MaintenanceDialogContent` |
| _none_ | `DefaultPositionChangeDialogContent` chooser |

When both `selectedSocketToReplace` and `selectedEditSocket` are set, the dialog auto advances to the `confirmChange` flow.

#### Basic usage

```tsx
const [open, setOpen] = useState(false)

<PositionChangeDialog
  open={open}
  onClose={() => setOpen(false)}
  selectedEditSocket={selectedEditSocket}
  dialogFlow={POSITION_CHANGE_DIALOG_FLOWS.CHANGE_INFO}
  onPositionChangedSuccess={() => refetchInventory()}
/>
```

#### Move-to-maintenance flow

```tsx
<PositionChangeDialog
  open={open}
  onClose={handleClose}
  selectedEditSocket={selectedEditSocket}
  dialogFlow={POSITION_CHANGE_DIALOG_FLOWS.MAINTENANCE}
/>
```

#### Styling

- `.mdk-position-change-dialog`: Root dialog element

### `AddReplaceMinerDialog`

<ComponentDescription name="AddReplaceMinerDialog" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `open` | Required | `boolean` | none | Controls dialog visibility |
| `onClose` | Required | `function` | none | Called when the dialog closes |
| `selectedSocketToReplace` | Required | [`UnknownRecord`](/v0-4-0/reference/app-toolkit/ui-kit/types#unknownrecord) | none | Socket payload for the empty socket receiving a new miner |
| `selectedEditSocket` | Required | [`UnknownRecord`](/v0-4-0/reference/app-toolkit/ui-kit/types#unknownrecord) | none | Socket payload for an existing miner when running the change-info flow |
| `currentDialogFlow` | Required | `string` | none | Flow key from [`POSITION_CHANGE_DIALOG_FLOWS`](/v0-4-0/reference/app-toolkit/ui-kit/constants#position_change_dialog_flows); typically `replaceMiner` or `changeInfo` |
| `isDirectToMaintenanceMode` | Optional | `boolean` | `false` | Register the new miner straight into the maintenance container instead of the selected socket |
| `minersType` | Required | `string` | none | Restricts the form to a specific miner type (used to gate model-specific validation) |
| `isContainerEmpty` | Optional | `boolean` | none | Forwarded to internal validation when registering into an empty container |

#### Validation

- MAC and serial are validated against pending submissions before dispatch
- `isDirectToMaintenanceMode` skips the position picker and uses the maintenance container as the target

#### Basic usage

```tsx
<AddReplaceMinerDialog
  open={open}
  onClose={() => setOpen(false)}
  selectedSocketToReplace={emptySocket}
  currentDialogFlow={POSITION_CHANGE_DIALOG_FLOWS.REPLACE_MINER}
/>
```

#### Direct-to-maintenance registration

```tsx
<AddReplaceMinerDialog
  open={open}
  onClose={() => setOpen(false)}
  selectedSocketToReplace={emptySocket}
  isDirectToMaintenanceMode
/>
```

### `ContainerSelectionDialog`

<ComponentDescription name="ContainerSelectionDialog" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `open` | Required | `boolean` | none | Controls dialog visibility |
| `onClose` | Required | `function` | none | Called when the dialog closes |
| `miner` | Required | [`Device`](/v0-4-0/reference/app-toolkit/ui-kit/types#device) | none | The miner being moved out of maintenance |
| `containers` | Optional | [`Device[]`](/v0-4-0/reference/app-toolkit/ui-kit/types#device) | `[]` | Candidate destination containers shown in the picker |
| `isLoading` | Optional | `boolean` | none | Shows a loading state while `containers` is being fetched |

#### Behavior

- Presets the source as the maintenance container and lets the user pick a destination container and socket
- Internally renders `ContainerSelectionDialogContent` with a synthesised `selectedSocketToReplace` of `{ miner, containerInfo: { container: 'maintenance' }, pos: '', socket: '' }`

#### Basic usage

```tsx
<ContainerSelectionDialog
  open={open}
  onClose={() => setOpen(false)}
  miner={minerUnderMaintenance}
  containers={availableContainers}
  isLoading={isContainersLoading}
/>
```

### `MaintenanceDialogContent`

<ComponentDescription name="MaintenanceDialogContent" />

This is a content body intended to be rendered inside [`PositionChangeDialog`](#positionchangedialog) (or any host `Dialog`); it does not render its own dialog chrome.

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `selectedEditSocket` | Optional | `object` | none | Socket payload for the miner being moved to maintenance. Shape: `Partial<{ miner: Device, containerInfo: { container?: string } }>` |
| `onCancel` | Required | `function` | none | Called when the user cancels or after a successful submission |

#### Behavior

- Confirms the action with the miner short-code and its current container and position
- On submit, queues an update action through `useActions()` with the maintenance container, an empty `pos`, and an empty `subnet`, preserving the prior position in `posHistory`
- Fires an info notification ("Action added") and calls `onCancel`

#### Basic usage

```tsx
<MaintenanceDialogContent
  selectedEditSocket={selectedEditSocket}
  onCancel={() => setOpen(false)}
/>
```

#### Styling

- `.mdk-maintenance-confirm`: Root container
- `.mdk-maintenance-confirm__warning`: Confirmation copy
- `.mdk-maintenance-confirm__highlight`: Inline miner short-code
- `.mdk-maintenance-confirm__footer`: Cancel and confirm action row

### `RemoveMinerDialog`

<ComponentDescription name="RemoveMinerDialog" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `headDevice` | Optional | [`Device`](/v0-4-0/reference/app-toolkit/ui-kit/types#device) | `{}` | The miner being removed; the dialog reads `info.pos` for the confirmation copy |
| `isRemoveMinerFlow` | Required | `boolean` | none | Controls dialog visibility |
| `onCancel` | Required | `function` | none | Called when the user cancels or after a successful submission |

#### Behavior

- Wraps `RemoveMinerDialogContent` inside a `Dialog` with the title "Are you sure to permanently remove miner?"
- On confirm, queues the remove action through `useActions()` for the head device

#### Basic usage

```tsx
<RemoveMinerDialog
  headDevice={selectedMiner}
  isRemoveMinerFlow={isRemoveOpen}
  onCancel={() => setRemoveOpen(false)}
/>
```

# Stats (/v0-4-0/ui/react/foundation/operations/details-view/stats)

Single-value and grouped stat cards plus the combined miner metric layout used by the details view.

For miner identity (`MinerInfoCard`), see the [details view overview](/v0-4-0/ui/react/foundation/operations). For fleet activity visualizations, see [Charts](/v0-4-0/ui/react/foundation/operations/details-view/charts). For per-chip thermal data, see [Chips](/v0-4-0/ui/react/foundation/operations/details-view/chips).

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- Components on this page read live metrics from `device.last.snap.stats` on a [`Device`](/v0-4-0/reference/app-toolkit/ui-kit/types#device)

  <Accordions>
    <Accordion title="Metric fields and adapter store hooks">
      - Hash rate in MH/s, temperature in °C, frequency in MHz, and power in W
      - `StatsGroupCard` additionally requires `useDevices()` so the current device selection is available
    </Accordion>
  </Accordions>

## Components

<ComponentTable category="Details view" subcategory="Stats" pkg="foundation" />

### `SingleStatCard`

<ComponentDescription name="SingleStatCard" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `name` | Optional | `string` | none | Stat label |
| `subtitle` | Optional | `string` | `''` | Secondary label rendered below the name |
| `value` | Optional | `number \| string \| null` | `null` | Stat value; a tooltip is shown when `value` is set |
| `unit` | Optional | `string` | `''` | Unit suffix; combined with `value` via [`formatValueUnit`](/v0-4-0/reference/app-toolkit/ui-kit/utilities#formatvalueunit) |
| `color` | Optional | `string` | `'inherit'` | CSS color applied via the `--stat-color` custom property |
| `flash` | Optional | `boolean` | `false` | Apply the standard flash animation |
| `superflash` | Optional | `boolean` | `false` | Apply the faster flash animation |
| `tooltipText` | Optional | `string` | `''` | Override for the tooltip label (defaults to `name`) |
| `variant` | Optional | `'primary' \| 'secondary' \| 'tertiary' \| 'highlighted'` | `'primary'` | Layout variant |
| `row` | Optional | `boolean` | `false` | Stack name and value horizontally |

A tooltip is rendered automatically whenever `value` is set; pass `tooltipText` to override the label portion. Values longer than six characters get a `--long-value` modifier for narrower number formatting.

Use `primary` through `tertiary` for explorer and device metrics. Use `highlighted` for financial or reporting summary metric strips (same presentation as OSS **MetricCard** `$isHighlighted`, for example EBITDA sell-scenario tiles: large emphasized value, subdued label).

#### Basic usage

```tsx
<SingleStatCard name="Temperature" value={42} unit="°C" />
<SingleStatCard name="Hash rate" value="95.5" unit="TH/s" />
<SingleStatCard name="Uptime" value="99.7" unit="%" />
```

#### Variants

```tsx
<SingleStatCard name="Temperature" value={42} unit="°C" variant="primary" />
<SingleStatCard name="Fan speed" value={4200} unit="RPM" variant="secondary" />
<SingleStatCard name="Power" value={3250} unit="W" variant="tertiary" />
```

#### Highlighted (reporting metrics)

```tsx
<SingleStatCard name="Revenue" value={4.2} unit="M$" variant="highlighted" />
<SingleStatCard name="OpEx" value={1.8} unit="M$" variant="highlighted" />
<SingleStatCard
  name="(Sell scenario - all BTC sold)"
  value={-123.123}
  unit="$"
  variant="highlighted"
  color="#FF8C42"
/>
<SingleStatCard name="Margin" value={57} unit="%" variant="highlighted" />
```

#### Alerts with flash

```tsx
<SingleStatCard name="Temperature" value={92} unit="°C" color="red" flash />
<SingleStatCard name="Status" value="Overheat" color="#ff0000" superflash />
```

#### Row layout

```tsx
<SingleStatCard name="Efficiency" value="32.5" unit="J/TH" row />
```

#### Styling

- `.mdk-single-stat-card`: root element
- `.mdk-single-stat-card--primary`, `--secondary`, `--tertiary`, `--highlighted`: layout variants (`--highlighted` uses a large bold value via `--stat-color`, muted name and subtitle)
- `.mdk-single-stat-card--flash`, `--superflash`: animation modifiers
- `.mdk-single-stat-card--row`: row layout
- `.mdk-single-stat-card--long-value`: applied automatically when value exceeds 6 characters
- `.mdk-single-stat-card__name`, `__subtitle`, `__value`, `__text`: inner elements

### `SecondaryStatCard`

<ComponentDescription name="SecondaryStatCard" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `name` | Optional | `string` | `''` | Stat label |
| `value` | Optional | `string \| number` | `''` | Stat value |
| `className` | Optional | `string` | none | Additional class merged onto the root |

#### Basic usage

```tsx
<SecondaryStatCard name="Hash rate" value="95.5 TH/s" />
<SecondaryStatCard name="Uptime" value={99.8} />
<SecondaryStatCard name="Efficiency" value="92%" />
```

#### Styling

- `.mdk-secondary-stat-card`: root element
- `.mdk-secondary-stat-card__name`: stat label
- `.mdk-secondary-stat-card__value`: stat value

### `StatsGroupCard`

<ComponentDescription name="StatsGroupCard" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `miners` | Optional | `Device[] \| DeviceData[]` | none | Devices to aggregate for stat values; falls back to the adapter store selection from `useDevices()` when omitted |
| `isMinerMetrics` | Optional | `boolean` | `false` | When `true`, renders `MinerMetricCard` instead of the default `SingleStatCard` grid |

#### Aggregated stats

Primary stats (always rendered):

- **Hash rate**: sum of `snap.stats.hashrate_mhs.t_5m` across the selection, formatted via `getHashrateUnit`
- **Temperature**: max of `snap.stats.temperature_c.max`, in °C
- **Frequency**: average of `snap.stats.frequency_mhz.avg`, in MHz
- **Consumption**: sum of `snap.stats.power_w` (suppressed for miner types where power is not reported)
- **Efficiency**: derived `consumption / hash rate` in J/TH, only when both are present

Secondary stats depend on layout mode:

- **Default grid layout** (`isMinerMetrics={false}`): a second row of `SecondaryStatCard`s when the effective selection (`miners` or the `useDevices()` fallback) has **length 1** (any device type). Values come from the first entry in that set.
- **Miner-metrics layout** (`isMinerMetrics={true}`): secondary stats inside `MinerMetricCard` only when the adapter store selection from `useDevices()` contains **exactly one** device whose `type` satisfies `isMiner` (prefix `miner-`). Passing `miners={…}` alone does not enable the secondary strip. Vendor types such as `antminer-s19` are not counted unless `type` uses the `miner-` prefix.

#### Basic usage

```tsx
<StatsGroupCard miners={selectedMiners} />
```

#### Render with the miner metric layout

```tsx
<StatsGroupCard isMinerMetrics />
```

When `isMinerMetrics` is true, wire device selection through `useDevices()` and use `miner-*` device types if you need the secondary strip; see **Aggregated stats** above.

#### Styling

- `.mdk-stats-group-card`: root element
- `.mdk-stats-group-card__row`: stat row container

### `MinerMetricCard`

<ComponentDescription name="MinerMetricCard" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `primaryStats` | Optional | `StatItem[]` | none | Primary metrics keyed by fixed labels (see below) |
| `secondaryStats` | Optional | `StatItem[]` | none | Free-form stats rendered in the optional secondary grid |
| `showSecondaryStats` | Optional | `boolean` | `true` | When `false`, hides the secondary stats grid |

The `StatItem` type is exported from the same module:

```tsx
type StatItem = {
  name?: string
  value?: number | string
  unit?: string
  tooltipText?: string
}
```

#### Recognised primary stat names

`primaryStats` is read by name. Unknown entries are ignored; only these labels render:

- `Efficiency`: rendered top-right. Hidden if value is undefined or non-numeric.
- `Hash rate`: top-left primary box
- `Temperature`: top-right primary box. Also drives the displayed Frequency value (the source rounds and formats the temperature value as a Frequency stand-in; pass both stats explicitly to keep them aligned).
- `Frequency`: bottom-left primary box (uses the formatted `Temperature` numeric for backwards-compat with upstream callers)
- `Consumption`: bottom-right primary box. Formatted with three decimals; falls back to `kWh` unit when value is `0`.

#### Basic usage

```tsx
<MinerMetricCard
  primaryStats={[
    { name: 'Efficiency', value: 32.5, unit: 'J/TH' },
    { name: 'Hash rate', value: 95.5, unit: 'TH/s' },
    { name: 'Temperature', value: 65, unit: '°C' },
    { name: 'Frequency', value: 500, unit: 'MHz' },
    { name: 'Consumption', value: 3.25, unit: 'kW' },
  ]}
  secondaryStats={[
    { name: 'Fan speed', value: '6000 RPM' },
    { name: 'Uptime', value: '3 days' },
  ]}
/>
```

#### Hide the secondary grid

```tsx
<MinerMetricCard
  primaryStats={primaryStats}
  secondaryStats={[]}
  showSecondaryStats={false}
/>
```

#### Styling

- `.mdk-miner-metric-card`: root element
- `.mdk-miner-metric-card__title`: card title (`Miner Metrics`)
- `.mdk-miner-metric-card__body`: layout wrapper
- `.mdk-miner-metric-card__efficiency`: top-right efficiency block
- `.mdk-miner-metric-card__content`: primary and secondary stat grid
- `.mdk-miner-metric-card__box`: pair of primary stat items
- `.mdk-miner-metric-card__item`: single label/value cell
- `.mdk-miner-metric-card__label`: stat label
- `.mdk-miner-metric-card__value`: stat value
- `.mdk-miner-metric-card__grid-box`: secondary stats grid

# Device explorer (/v0-4-0/ui/react/foundation/operations/device-explorer)

The device explorer is the primary navigation surface for browsing containers, racks, and miners. It combines a filter `Cascader`, a tag-based search, device-type tabs, and a selectable data table so operators can drill into fleet inventory.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)

### `DeviceExplorer`

<ComponentDescription name="DeviceExplorer" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `deviceType` | Required | `'container' \| 'miner' \| 'cabinet'` | required | Current view type |
| `data` | Required | `Device[]` | none | Array of devices to display |
| `filterOptions` | Required | `DeviceExplorerFilterOption[]` | none | `Cascader` filter options |
| `searchOptions` | Required | `DeviceExplorerSearchOption[]` | none | Search autocomplete options |
| `searchTags` | Required | `string[]` | none | Current search tags |
| `onSearchTagsChange` | Required | `function` | none | Search tags change handler |
| `onDeviceTypeChange` | Required | `function` | none | Device type tab change handler |
| `onFiltersChange` | Required | `function` | none | Filter change handler |
| `selectedDevices` | Optional | `DataTableRowSelectionState` | `{}` | Selected row state |
| `onSelectedDevicesChange` | Optional | `function` | none | Selection change handler |
| `renderAction` | Optional | `function` | none | Custom action column renderer |
| `getFormattedDate` | Required | `function` | none | Date formatter function |
| `className` | Optional | `string` | none | Additional CSS class |

#### Basic usage

```tsx
<DeviceExplorer
  deviceType="container"
  data={containers}
  filterOptions={[
    { value: 'site-a', label: 'Site A' },
    { value: 'site-b', label: 'Site B' },
  ]}
  searchOptions={[
    { value: 'container-001', label: 'Container 001' },
  ]}
  searchTags={searchTags}
  onSearchTagsChange={setSearchTags}
  onDeviceTypeChange={setDeviceType}
  onFiltersChange={handleFilters}
  getFormattedDate={(date) => date.toLocaleDateString()}
/>
```

#### With selection

```tsx
<DeviceExplorer
  deviceType="miner"
  data={miners}
  filterOptions={filterOptions}
  searchOptions={searchOptions}
  searchTags={[]}
  onSearchTagsChange={setSearchTags}
  onDeviceTypeChange={setDeviceType}
  onFiltersChange={handleFilters}
  selectedDevices={selected}
  onSelectedDevicesChange={setSelected}
  getFormattedDate={formatDate}
  renderAction={(device) => (
    <Button size="sm" onClick={() => viewDetails(device.id)}>
      View
    </Button>
  )}
/>
```

#### Styling

- `.mdk-device-explorer`: Root element
- `.mdk-device-explorer__toolbar`: Toolbar container
- `.mdk-device-explorer__toolbar__filter`: Filter `Cascader`
- `.mdk-device-explorer__toolbar__search`: Search input
- `.mdk-device-explorer__toolbar__tabs`: Device type tabs
- `.mdk-device-explorer__toolbar__tabs-list`: Tabs list container
- `.mdk-device-explorer__toolbar__tab-trigger`: Individual tab trigger

## Related hooks

| Hook | Supplies |
|------|---------|
| [`useGetAvailableDevices`](/v0-4-0/reference/app-toolkit/hooks/components#usegetavailabledevices) | Transforms the device list into available container and miner type sets consumed by `<DeviceExplorer />` |

## Next steps

- For selecting and acting on an individual device, see the [details view](/v0-4-0/ui/react/foundation/operations/details-view) pages.
- For container-level controls alongside the explorer, see [`ContainerControlsBox`(../details-view/controls#containercontrolsbox) on the Controls page.

# Pool Manager (/v0-4-0/ui/react/foundation/pool-manager)

The Pool Manager section provides UI for landing, exploring miners, and managing pool configurations. It is rendered as a top-level product area with three sibling workflows.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- `<MdkProvider>` from `@tetherto/mdk-react-adapter` wrapping your app tree

  <Accordions>
    <Accordion title="Adapter store hooks used across these components">
      - `PoolManagerMinerExplorer` reads pool configs through `usePoolConfigs` and queues miner-action events through `useActions()`
      - `PoolManagerPools` reads and mutates pool configuration state through `useActions()`; formatting relies on `useTimezone()` for display-local timestamps
      - The standalone dashboard has no adapter store dependencies
    </Accordion>
  </Accordions>

## Component groups

The section is split into five subpages plus this index:

- [Dashboard](/v0-4-0/ui/react/foundation/pool-manager/dashboard): landing page composing stats, navigation blocks, and a recent alerts feed
- [Miner explorer](/v0-4-0/ui/react/foundation/pool-manager/miner-explorer): cross-pool miner browser with filter, search, and selection; used to assign miners to pool configurations
- [Pools](/v0-4-0/ui/react/foundation/pool-manager/pools): pools page with the collapsible pool list and the **Add Pool** dialog for authoring new pool configurations
- [Sites overview](/v0-4-0/ui/react/foundation/pool-manager/sites-overview): multi-site landing screen showing every site as a status card with pool assignment, miner counts, and hashrate
- [Site overview details](/v0-4-0/ui/react/foundation/pool-manager/site-overview-details): rack-level miner grid detail view for a single site with drag-to-select and pool assignment

## All pool manager components

<ComponentTable category="Pool Manager" pkg="foundation" />

# Assign pool (/v0-4-0/ui/react/foundation/pool-manager/assign-pool)

<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

`AssignPoolModal` opens from the miner explorer to let operators assign a configured pool to one or more selected miners. It shows the selected miner list, a pool selector with live metadata, an endpoints preview, and an optional credential template preview. Submission is async; the modal stays open with a loading state until the parent resolves.

For the miner selection surface see [Miner explorer](/v0-4-0/ui/react/foundation/pool-manager/miner-explorer). For pool authoring see [Pools](/v0-4-0/ui/react/foundation/pool-manager/pools).

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- `<MdkProvider>` from `@tetherto/mdk-react-adapter` with `useActions()` wired up
- Pool configuration data (`PoolConfigData[]`) sourced from your API layer

## Components

<ComponentTable category="Pool Manager" subcategory="Assign pool" pkg="foundation" />

### `AssignPoolModal`

<ComponentDescription name="AssignPoolModal" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `isOpen` | Required | `boolean` | none | Controls dialog visibility |
| `onClose` | Required | `function` | none | Called when the user cancels or closes the dialog |
| `onSubmit` | Required | `function` | none | Async handler called on form submission; modal stays open with a loading state until the promise resolves |
| `miners` | Required | `Device[]` | none | Selected miners to assign; displayed in the miner summary table |
| `poolConfig` | Required | `PoolConfigData[]` | none | Pool configuration data from your API layer |

#### `PoolConfigData` type

The input shape passed to `poolConfig`. Each entry maps to one option in the pool selector.

```tsx
type PoolConfigData = {
  id: string
  poolConfigName: string
  description: string
  poolUrls: Array<{
    url: string
    pool: string
    workerName?: string
    workerPassword?: string
  }>
  miners: number
  containers: number
  updatedAt: string | number  // Unix timestamp (ms) or ISO string
}
```

#### `PoolSummary` type

The resolved pool object passed back through `onSubmit`.

```tsx
type PoolSummary = {
  id: string
  name: string
  description: string
  units: number
  miners: number
  workerName: string | undefined
  workerPassword: string | undefined
  endpoints: PoolEndpoint[]
  validation?: { status: string }
  updatedAt: Date
}

type PoolEndpoint = {
  role?: string   // Primary, Failover, etc.
  host: string
  port: string
  pool: string
  url?: string
}
```

#### Basic usage

```tsx

const poolConfig: PoolConfigData[] = [
  {
    id: 'pool-1',
    poolConfigName: 'Alpha Pool',
    description: 'Primary pool with failover',
    poolUrls: [
      { url: 'stratum+tcp://pool-primary.example.com:3333', pool: 'pool1', workerName: 'wn-1', workerPassword: 'x' },
      { url: 'stratum+tcp://pool-failover.example.com:3333', pool: 'pool1', workerName: 'wn-1', workerPassword: 'x' },
    ],
    miners: 120,
    containers: 4,
    updatedAt: Date.now(),
  },
]

  const [isOpen, setIsOpen] = useState(false)
  const [selectedMiners, setSelectedMiners] = useState<Device[]>([])

  const handleSubmit = async ({ pool }: { pool: PoolSummary }) => {
    await yourApi.assignMinersToPool(selectedMiners.map(m => m.id), pool.id)
    setIsOpen(false)
  }

  return (
    <>
      <button onClick={() => setIsOpen(true)}>Assign Pool</button>
      <AssignPoolModal
        isOpen={isOpen}
        onClose={() => setIsOpen(false)}
        onSubmit={handleSubmit}
        miners={selectedMiners}
        poolConfig={poolConfig}
      />
    </>
  )
}
```

#### Behaviour

The modal renders three sections:

1. **Selected miners** — a `DataTable` showing each miner's short code, unit, current pool assignment, and status. Row count is shown in the section header.
2. **Choose pool** — a `FormSelect` dropdown built from `poolConfig`. On selection, a metadata row appears showing unit count, miner count, and last-updated time (displayed as a relative string via `intlFormatDistance`). Below the metadata, an **Endpoints Preview** lists each endpoint with its role label (Primary, Failover), host, and port.
3. **Credential template preview** (optional) — shown only when the `SHOW_CREDENTIAL_TEMPLATE` feature flag is enabled.

The **Assign** button is disabled during submission and shows "Assigning…" while the `onSubmit` promise is pending. The form resets automatically after a successful submission.

A `Loader` replaces the form body while pool configs are loading; a `CoreAlert` error is shown if pool config loading fails.

#### Styling

- `.mdk-pm-assign-pool-modal`: Root dialog element
- `.mdk-pm-assign-pool-modal__body`: Form body container
- `.mdk-pm-assign-pool-modal__section`: Each labelled content section
- `.mdk-pm-assign-pool-modal__section-header`: Section header row (title + optional value)
- `.mdk-pm-assign-pool-modal__section-title`: Section label text
- `.mdk-pm-assign-pool-modal__section-value`: Section summary value (e.g. "3 miners selected")
- `.mdk-pm-assign-pool-modal__pool-meta`: Metadata row shown after a pool is selected
- `.mdk-pm-assign-pool-modal__endpoints-list`: Endpoints preview list container
- `.mdk-pm-assign-pool-modal__endpoint`: Single endpoint row
- `.mdk-pm-assign-pool-modal__endpoint-role`: Endpoint role label wrapper
- `.mdk-pm-assign-pool-modal__endpoint-role-name`: Role label text
- `.mdk-pm-assign-pool-modal__endpoint-fields`: Host and port fields wrapper
- `.mdk-pm-assign-pool-modal__endpoint-host`: Endpoint hostname
- `.mdk-pm-assign-pool-modal__endpoint-port`: Endpoint port
- `.mdk-pm-assign-pool-modal__credential-template`: Credential template preview section
- `.mdk-pm-assign-pool-modal__footer`: Dialog footer containing Cancel and Assign buttons

# Dashboard (/v0-4-0/ui/react/foundation/pool-manager/dashboard)

The Pool Manager dashboard aggregates pool health stats, quick-navigation blocks into the miner explorer and pools workflows, and a feed of recent alerts. It is the entry point rendered when operators land on the Pool Manager area.

For miner-to-pool assignment, see [Miner explorer](/v0-4-0/ui/react/foundation/pool-manager/miner-explorer). For pool and endpoint management, see [Pools](/v0-4-0/ui/react/foundation/pool-manager/pools).

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)

## Components

<ComponentTable category="Pool Manager" subcategory="Dashboard" pkg="foundation" />

### `PoolManagerDashboard`

<ComponentDescription name="PoolManagerDashboard" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `stats` | Optional | `DashboardStats` | none | Dashboard statistics |
| `isStatsLoading` | Optional | `boolean` | `false` | Stats loading state |
| `alerts` | Optional | `Alert[]` | `[]` | Recent alerts list |
| `onNavigationClick` | Required | `function` | none | Navigation handler |
| `onViewAllAlerts` | Required | `function` | none | View all alerts handler |

#### `DashboardStats` type

```tsx
type StatItem = {
  label: string
  value: number
  type?: 'ERROR' | 'SUCCESS'
  secondaryValue?: string
}

type DashboardStats = {
  items: StatItem[]
}
```

#### Basic usage

```tsx
<PoolManagerDashboard
  stats={{
    items: [
      { label: 'Active Pools', value: 3, type: 'SUCCESS' },
      { label: 'Total Miners', value: 150 },
      { label: 'Offline', value: 2, type: 'ERROR' },
    ],
  }}
  alerts={recentAlerts}
  onNavigationClick={(url) => router.push(url)}
  onViewAllAlerts={() => router.push('/alerts')}
/>
```

#### Loading state

```tsx
<PoolManagerDashboard
  isStatsLoading
  alerts={[]}
  onNavigationClick={handleNav}
  onViewAllAlerts={handleViewAlerts}
/>
```

#### Dashboard sections

The component displays:
1. **Stats blocks**: Key metrics with optional success/error indicators
2. **Navigation blocks**: Quick links to pool management features
3. **Recent alerts**: Latest alerts with severity indicators

#### Styling

- `.mdk-pm-dashboard`: Root element
- `.mdk-pm-dashboard__stat-blocks`: Stats container
- `.mdk-pm-dashboard__stat-block`: Individual stat block
- `.mdk-pm-dashboard__stat-header`: Stat header with label and status
- `.mdk-pm-dashboard__stat-label`: Stat label text
- `.mdk-pm-dashboard__stat-status`: Status indicator
- `.mdk-pm-dashboard__stat-value-row`: Value container
- `.mdk-pm-dashboard__stat-value`: Primary value
- `.mdk-pm-dashboard__stat-secondary`: Secondary value
- `.mdk-pm-dashboard__nav-blocks`: Navigation blocks container
- `.mdk-pm-dashboard__nav-block`: Individual navigation block
- `.mdk-pm-dashboard__nav-header`: Block header with icon
- `.mdk-pm-dashboard__nav-icon`: Block icon
- `.mdk-pm-dashboard__nav-title`: Block title
- `.mdk-pm-dashboard__nav-description`: Block description
- `.mdk-pm-dashboard__nav-action`: Action button
- `.mdk-pm-dashboard__alerts-wrapper`: Alerts section container
- `.mdk-pm-dashboard__alerts-title`: Alerts heading
- `.mdk-pm-dashboard__alerts`: Alerts list
- `.mdk-pm-dashboard__alert-row`: Individual alert row
- `.mdk-pm-dashboard__alert-text`: Alert text content
- `.mdk-pm-dashboard__alert-status`: Alert severity indicator
- `.mdk-pm-dashboard__alerts-empty`: Empty alerts message

## Related hooks

| Hook | Purpose |
|------|---------|
| [`usePoolStats`](/v0-4-0/reference/app-toolkit/hooks/data#usepoolstats) | Aggregate per-pool stats into site-level totals surfaced by the stats strip |
| [`useActiveIncidents`](/v0-4-0/reference/app-toolkit/hooks/data#useactiveincidents) | Fetch the live list of currently-firing alerts for the alerts panel |
| [`useSitesOverviewData`](/v0-4-0/reference/app-toolkit/hooks/data#usesitesoverviewdata) | Project container rows and pool stats into the sites overview view-model |

## Next steps

- For pool summary widgets that surface on the top-level operations dashboard, see
[`PoolDetailsCard`](/v0-4-0/ui/react/foundation/dashboard/stats/generic-widgets#pooldetailscard) and [`PoolDetailsPopover`](/v0-4-0/ui/react/foundation/dashboard/stats/generic-widgets#pooldetailspopover).

# Miner explorer (/v0-4-0/ui/react/foundation/pool-manager/miner-explorer)

Surface for browsing miners across the fleet and assigning them to pool configurations. `PoolManagerMinerExplorer` is the plug-and-play feature composite that the demo ships with.

For pool configuration authoring, see [Pools](/v0-4-0/ui/react/foundation/pool-manager/pools). For the Pool Manager landing surface, see [Dashboard](/v0-4-0/ui/react/foundation/pool-manager/dashboard).

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- `<MdkProvider>` from `@tetherto/mdk-react-adapter` with `useActions()` wired up (used by `PoolManagerMinerExplorer` to queue `SETUP_POOLS` actions)
- Pool configuration data (`PoolConfigData[]`) and miner `Device[]` arrays. The `Device` shape is documented in [reference types](/v0-4-0/reference/app-toolkit/ui-kit/types)

## Components

<ComponentTable category="Pool Manager" subcategory="Miner explorer" pkg="foundation" />

### `PoolManagerMinerExplorer`

<ComponentDescription name="PoolManagerMinerExplorer" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `miners` | Required | `Device[]` | none | Fleet miners to browse |
| `poolConfig` | Required | `PoolConfigData[]` | none | Pool configurations used for the pool filter and assign flow |
| `backButtonClick` | Required | `function` | none | Fires when the operator clicks the "Pool Manager" back link |

#### Behaviour

- Renders a page header with title, a back link, and an **Assign Pool** button gated by the `ACTION_TYPES.SETUP_POOLS` permission
- Lists miners across the fleet with filter, search, and multi-select
- On submit, queues a submission through `useActions()` with the selected miners and the chosen pool, then resets the selection
- The Assign Pool button is hidden when the `ASSIGN_POOL_POPUP_ENABLED` feature flag is off

#### Basic usage

```tsx
<PoolManagerMinerExplorer
  miners={fleetMiners}
  poolConfig={poolConfigs}
  backButtonClick={() => router.push('/pool-manager')}
/>
```

#### Styling

- `.mdk-pm-miner-explorer-page`: Root element
- `.mdk-pm-miner-explorer-page__header`: Title and action row
- `.mdk-pm-miner-explorer-page__title`: Page title
- `.mdk-pm-miner-explorer-page__subtitle`: Back-link wrapper
- `.mdk-pm-miner-explorer-page__back-link`: Back link button

# Pools (/v0-4-0/ui/react/foundation/pool-manager/pools)

Authoring UI for mining pools. `PoolManagerPools` is the plug-and-play feature composite used by the demo; it opens an internal pool-authoring dialog on demand when the **Add Pool** button is clicked.

For miner-to-pool assignment via the explorer, see [Miner explorer](/v0-4-0/ui/react/foundation/pool-manager/miner-explorer). For the landing surface, see [Dashboard](/v0-4-0/ui/react/foundation/pool-manager/dashboard).

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- `<MdkProvider>` from `@tetherto/mdk-react-adapter` with `useActions()` wired up. `PoolManagerPools` queues pool-config actions through the adapter store
- Pool configuration data (`PoolConfigData[]`) sourced from your API layer
- Feature flags that gate optional controls: `ADD_POOL_ENABLED`, `SHOW_CREDENTIAL_TEMPLATE`, `SHOW_POOL_VALIDATION` (re-exported from `@tetherto/mdk-react-devkit/foundation`)

## Components

<ComponentTable category="Pool Manager" subcategory="Pools" pkg="foundation" />

### `PoolManagerPools`

<ComponentDescription name="PoolManagerPools" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `poolConfig` | Required | `PoolConfigData[]` | none | Pool configuration source; hydrated by `usePoolConfigs` |
| `backButtonClick` | Required | `function` | none | Fires when the operator clicks the "Pool Manager" back link |

#### Behaviour

- Renders a page header (title + back link) with an **Add Pool** button gated by the `ADD_POOL_ENABLED` flag
- Shows a loader while `usePoolConfigs` is loading, or a `CoreAlert` on error
- Lists every pool in a collapsible accordion: each row shows the pool name, description, unit/miner counts, and a validation badge; expanding a row reveals the endpoints list, credentials template, and a Test Configuration action
- Opens an internal pool-authoring dialog when the **Add Pool** button is clicked

#### Basic usage

```tsx
<PoolManagerPools
  poolConfig={poolConfigs}
  backButtonClick={() => router.push('/pool-manager')}
/>
```

#### Styling

- `.mdk-pm-pools`: Root element
- `.mdk-pm-pools__header`: Title and action row
- `.mdk-pm-pools__header-title`: Page title
- `.mdk-pm-pools__back-link`: Back link button
- `.mdk-pm-pools__accordion`: Accordion root
- `.mdk-pm-pools__accordion-item`, `.mdk-pm-pools__accordion-trigger`, `.mdk-pm-pools__accordion-content`: Accordion parts

## Related hooks

| Hook | Purpose |
|------|---------|
| [`usePoolRows`](/v0-4-0/reference/app-toolkit/hooks/data#usepoolrows) | Fetch per-pool stats and transform them into `PoolRow[]` objects for the pool manager table |
| [`usePoolStats`](/v0-4-0/reference/app-toolkit/hooks/data#usepoolstats) | Aggregate per-pool stats into site-level totals (workers, mismatch count, hashrate) |
| [`usePoolConfigs`](/v0-4-0/reference/app-toolkit/hooks/components#usepoolconfigs) | Map caller-provided query data into `PoolSummary[]` for pool summary display |

# Site overview details (/v0-4-0/ui/react/foundation/pool-manager/site-overview-details)

<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

`PoolManagerSiteOverviewDetails` is the detail view for a single container site in the Pool Manager. It renders a rack/PDU miner grid, a status legend, and a pool-assignment panel. Operators can multi-select miners by dragging a rectangle over socket cells, then assign pool configurations from the sidebar or modal.

For the site card grid that navigates here see [Sites overview](/v0-4-0/ui/react/foundation/pool-manager/sites-overview). For the miner-level assign dialog see [Assign pool](/v0-4-0/ui/react/foundation/pool-manager/assign-pool).

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- `<MdkProvider>` from `@tetherto/mdk-react-adapter` with `useActions()` wired up
- Site data as a `Device` row from your API layer
- Pool configuration data (`PoolConfigData[]`) from your API layer
- Optionally, `useSiteOverviewDetailsData` from `@tetherto/mdk-react-adapter` to supply `dataOptions`

## Components

<ComponentTable category="Pool Manager" subcategory="Site overview details" pkg="foundation" />

### `PoolManagerSiteOverviewDetails`

<ComponentDescription name="PoolManagerSiteOverviewDetails" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `unit` | Required | `Device` | none | The site (container unit) to render details for |
| `unitName` | Required | `string` | none | Display name shown in the breadcrumb (`Site Overview / <unitName>`) |
| `poolConfig` | Required | `PoolConfigData[]` | none | Pool configurations powering the per-pool detail rows |
| `dataOptions` | Optional | `SiteOverviewDetailsDataOptions` | none | Data-fetch knobs forwarded to `useSiteOverviewDetailsData` |
| `isLoading` | Optional | `boolean` | `false` | Show a centered loader instead of the detail container |
| `backButtonClick` | Required | `VoidFunction` | none | Called when the operator clicks the "Site Overview" back link |

#### Basic usage

```tsx

  <PoolManagerSiteOverviewDetails
    unit={site}
    unitName={site.name}
    poolConfig={poolConfig}
    isLoading={isLoading}
    backButtonClick={() => router.push('/pool-manager/sites')}
  />
)
```

#### More examples

<Accordions>
  <Accordion title="Loading state">

```tsx
<PoolManagerSiteOverviewDetails
  unit={site}
  unitName={site.name}
  poolConfig={[]}
  isLoading={true}
  backButtonClick={handleBack}
/>
```

  </Accordion>
  <Accordion title="With data options">

```tsx

const { dataOptions } = useSiteOverviewDetailsData({ unit: site })

<PoolManagerSiteOverviewDetails
  unit={site}
  unitName={site.name}
  poolConfig={poolConfig}
  dataOptions={dataOptions}
  backButtonClick={handleBack}
/>
```

  </Accordion>
</Accordions>

#### Behaviour

The component renders a breadcrumb header (`Site Overview / <unitName>`) with a back link, then delegates the body to the internal `SiteOverviewDetailsContainer`.

The container renders:

- A rack/PDU miner grid (`GridUnit`) with each socket showing hashrate, status color, and socket ID
- A status legend
- A pool-assignment panel when miners are selected:
  - **Desktop**: sticky sidebar column with `SetPoolConfiguration`
  - **Tablet**: a fixed FAB button that opens `SetPoolConfigurationModal`

On submission the selected miners are queued as a `SETUP_POOLS` action through `useActions().setAddPendingSubmissionAction`, and the selection is cleared. Only miners in `mining` or `not_mining` status are eligible for pool assignment.

A `Loader` is shown while `isLoading` is true.

## Miner selection

Miners are selected using [`react-selecto`](https://github.com/daybrush/selecto). Selection is always available once the grid has loaded — no prop is required to enable it.

### Interaction reference

| Interaction | Result |
|-------------|--------|
| Drag a rectangle | Selects all miner socket cells intersected by the rectangle |
| Click a socket | Selects that single miner |
| Shift + click or drag | Adds to the current selection |
| Click a rack bar | Selects all miners in that rack |
| Control / Command + click a rack bar | Deselects all miners in that rack |
| **Select All** (header button) | Selects every miner in the grid |
| **Deselect All** (header button) | Clears the selection |

Empty sockets (no connected miner) are not selectable.

## Styling

### `PoolManagerSiteOverviewDetails` (feature wrapper)

- `.mdk-pm-sod`: Root element
- `.mdk-pm-sod__header`: Breadcrumb header row
- `.mdk-pm-sod__title`: "Site Overview" label
- `.mdk-pm-sod__subtitle`: Back-link wrapper
- `.mdk-pm-sod__back-link`: "Site Overview" back button

### `SiteOverviewDetailsContainer` (inner container)

- `.mdk-sodc`: Flex row wrapping rack column and sticky column
- `.mdk-sodc__racks-col`: Rack grid column; narrows when a sticky sidebar is visible
- `.mdk-sodc__racks-col--narrow`: Applied when miners are selected and the sidebar is shown (desktop)
- `.mdk-sodc__sticky-col`: Sticky pool-assignment sidebar column (desktop)
- `.mdk-sodc__tablet-btn`: Fixed FAB button (tablet, when miners are selected)

### `GridUnit` (rack grid)

- `.mdk-grid-unit`: Root of each rack/PDU grid section
- `.mdk-grid-unit__row-label`: Rack row label (`Rack {pdu}`)
- `.mdk-grid-unit__socket-container`: Selectable miner socket cell (drag-select target)
- `.mdk-grid-unit__socket-container--disabled`: Socket cell not eligible for pool assignment
- `.mdk-grid-unit__miner-box--selected`: Applied to a miner cell when selected

## Related hooks

| Hook | Supplies |
|------|---------|
| [`useSiteOverviewDetailsData`](/v0-4-0/reference/app-toolkit/hooks/components#usesiteoverviewdetailsdata) | Data options forwarded to the container via `dataOptions` prop |

# Sites overview (/v0-4-0/ui/react/foundation/pool-manager/sites-overview)

<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

`PoolManagerSitesOverview` is the multi-site landing screen for pool management. It renders every site as a status card showing pool assignment, online miner count, hashrate, and active incidents. Selecting one or more cards exposes a pool-assignment panel (sidebar on desktop, modal on tablet) backed by `useActions()`.

For the rack-level miner grid shown after selecting a site card see [Site overview details](/v0-4-0/ui/react/foundation/pool-manager/site-overview-details). For the miner-level pool assignment dialog see [Assign pool](/v0-4-0/ui/react/foundation/pool-manager/assign-pool). For the pool manager landing page see [Pool Manager overview](/v0-4-0/ui/react/foundation/pool-manager).

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- `<MdkProvider>` from `@tetherto/mdk-react-adapter` with `useActions()` and `useDeviceResolution()` wired up
- Site data normalised through `useSitesOverviewData` from `@tetherto/mdk-react-adapter`
- Pool configuration data (`PoolConfigData[]`) from your API layer

## Components

<ComponentTable category="Pool Manager" subcategory="Sites overview" pkg="foundation" />

### `PoolManagerSitesOverview`

<ComponentDescription name="PoolManagerSitesOverview" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `units` | Required | `ProcessedContainerUnit[]` | none | Sites to render, normalised through `useSitesOverviewData` |
| `poolConfig` | Required | `PoolConfigData[]` | none | Pool configurations used to resolve pool names on each card |
| `isLoading` | Optional | `boolean` | none | Show a skeleton loader while site data is fetching |
| `error` | Optional | `unknown` | none | Surface a "Failed to load data" error alert when defined |
| `backButtonClick` | Required | `function` | none | Called when the operator clicks the "Pool Manager" back link |
| `onCardClick` | Required | `function` | none | Called with the clicked unit id — typically navigates to `/sites/:id` |

#### `ProcessedContainerUnit` type

Produced by `useSitesOverviewData` from `@tetherto/mdk-react-adapter`. Pass the result directly to `units`.

```tsx
type ContainerUnit = {
  id?: string
  type?: string
  info?: {
    container?: string
    nominalMinerCapacity?: string
    poolConfig?: string        // pool config id currently assigned
  }
  miners?: {
    total: number
    disconnected: number
    actualMiners: number
  }
  last?: {
    snap?: {
      stats?: { status?: string; [key: string]: unknown }
    }
  }
}

type ProcessedContainerUnit = ContainerUnit & {
  hashrateMhs: number          // per-container hashrate in MH/s
  status: 'mining' | 'offline'
  poolStats?: ContainerPoolStat
}
```

#### Basic usage

```tsx

  rawUnits,
  poolStats,
  tailLog,
  poolConfig,
}: {
  rawUnits: ContainerUnit[]
  poolStats: ContainerPoolStat[]
  tailLog: SitesOverviewTailLogItem
  poolConfig: PoolConfigData[]
}) => {
  const { units } = useSitesOverviewData({ units: rawUnits, poolStats, tailLog })

  return (
    <PoolManagerSitesOverview
      units={units}
      poolConfig={poolConfig}
      backButtonClick={() => router.push('/pool-manager')}
      onCardClick={(id) => router.push(`/pool-manager/sites/${id}`)}
    />
  )
}
```

#### More examples

<Accordions>
  <Accordion title="Loading state">

```tsx
<PoolManagerSitesOverview
  units={[]}
  poolConfig={[]}
  isLoading={true}
  backButtonClick={handleBack}
  onCardClick={handleCardClick}
/>
```

  </Accordion>
  <Accordion title="Error state">

```tsx
<PoolManagerSitesOverview
  units={[]}
  poolConfig={poolConfig}
  error={new Error('Failed to fetch sites')}
  backButtonClick={handleBack}
  onCardClick={handleCardClick}
/>
```

  </Accordion>
</Accordions>

#### Behaviour

The component renders a page header ("Site Overview" title + "Pool Manager" back link) and delegates the card grid to the internal `SitesOverviewStatusCardList`.

Each site card shows:
- Container / unit name
- Assigned pool name (resolved from `poolConfig` via `usePoolConfigs`)
- Formatted hashrate
- Online miner count
- Pool override count
- Status indicator (`mining` / `offline`)

Selecting one or more cards activates a pool-assignment panel:
- **Desktop**: a sticky sidebar column with `SetPoolConfiguration`
- **Tablet**: a FAB button that opens `SetPoolConfigurationModal`

On submission the selected units are queued as a `SETUP_POOLS` action through `useActions().setAddPendingSubmissionAction`, and the selection is cleared.

A `Loader` is shown while `units` or pool configs are loading; a `CoreAlert` error is shown if either fails.

#### Styling

- `.mdk-pm-sites-overview`: Root element
- `.mdk-pm-sites-overview__header`: Title and back-link row
- `.mdk-pm-sites-overview__title`: "Site Overview" heading text
- `.mdk-pm-sites-overview__subtitle`: Back-link wrapper
- `.mdk-pm-sites-overview__back-link`: "Pool Manager" back button
- `.mdk-pm-sites-list`: Card grid container (rendered by internal list component)
- `.mdk-pm-sites-list__row`: Flex row containing card grid and optional sticky sidebar
- `.mdk-pm-sites-list__unit-col`: Card grid column
- `.mdk-pm-sites-list__unit-grid`: CSS grid of status cards
- `.mdk-pm-sites-list__sticky-col`: Sticky sidebar column (desktop, when units are selected)
- `.mdk-pm-sites-list__fab`: Floating action button (tablet, when units are selected)

## Related hooks

The following hooks supply live data to `<PoolManagerSitesOverview />`.

| Hook | Supplies |
|------|---------|
| [`useSitesOverviewData`](/v0-4-0/reference/app-toolkit/hooks/data#usesitesoverviewdata) | Projects raw container rows and pool stats into the shape accepted by `units` and `isLoading` props |
| [`usePoolStats`](/v0-4-0/reference/app-toolkit/hooks/data#usepoolstats) | Aggregate pool worker counts and hashrate passed as the `poolStats` option to `useSitesOverviewData` |

# Repairs (/v0-4-0/ui/react/foundation/repairs)

<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Repairs components display spare-part changes recorded in device repair batch actions. Pair with the [Operations centre](/v0-4-0/ui/react/foundation/operations) for full device-management context.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)

## Components

<ComponentTable category="Repairs" pkg="foundation" />

### `RepairLogChangesSubRow`

<ComponentDescription name="RepairLogChangesSubRow" />

Expandable sub-row that lists the spare-part changes recorded in a repair batch action. Each non-miner repair action is resolved against its device to show the part type, serial number, MAC address, and whether the part was added or removed. Renders as a `DataTable`; shows a `Spinner` when `isLoading` is `true`.

The component does no data fetching — the parent fetches the batch action and its associated devices (for example via the things API) and passes them as props.

#### Import

```tsx
  RepairLogChangesSubRowProps,
  RepairBatchAction,
  RepairDevice,
} from '@tetherto/mdk-react-devkit/foundation'
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `batchAction` | Required | `RepairBatchAction` | none | The repair batch action whose part changes are displayed |
| `devices` | Required | `RepairDevice[]` | none | Devices referenced by the batch action, pre-fetched by the parent |
| `isLoading` | Optional | `boolean` | `false` | When `true`, renders a spinner instead of the table |

#### Supporting types

```tsx
type RepairBatchAction = Partial<{
  params: RepairAction[]
}>

type RepairAction = Partial<{
  params: RepairActionParam[]
}>

type RepairActionParam = Partial<{
  comment: string
  id: string
  rackId: string
  info: { parentDeviceId: string | null }
}>

type RepairDevice = Partial<{
  id: string
  rack: string
  info: Partial<{
    serialNum: string
    macAddress: string
  }>
}>
```

#### Basic usage

```tsx

const batchAction: RepairBatchAction = {
  params: [
    {
      params: [
        {
          id: 'inventory-miner_part-hashboard-001',
          rackId: 'inventory-miner_part-hashboard',
          info: { parentDeviceId: null },
        },
      ],
    },
  ],
}

const devices: RepairDevice[] = [
  {
    id: 'inventory-miner_part-hashboard-001',
    rack: 'inventory-miner_part-hashboard',
    info: { serialNum: 'HB-SN-0001', macAddress: 'AA:BB:CC:00:00:01' },
  },
]

<RepairLogChangesSubRow batchAction={batchAction} devices={devices} />
```

#### Loading state

```tsx
<RepairLogChangesSubRow
  batchAction={batchAction}
  devices={[]}
  isLoading
/>
```

#### Behavior notes

- Miner rack actions and actions with a `comment` field are filtered out — only spare-part changes appear in the table
- A part is marked **Removed** when `parentDeviceId` is `null`; **Added** when `parentDeviceId` references a parent device
- Part type labels come from `MINER_TYPE_NAME_MAP` and `SparePartNames`; unrecognised rack IDs display as `Unknown`
- Pagination is disabled by default

## Next steps

- For device-level details, see the [Operations centre](/v0-4-0/ui/react/foundation/operations) pages

# Reporting tool (/v0-4-0/ui/react/foundation/reporting)

<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Reporting-tool components cover financial and operational analytics. Host apps supply API or tail-log data; components handle layout, charts, and tab navigation. Every report shares one date-range model through `TimeframeControls`, `ReportTimeFrameSelector`, and `useFinancialDateRange`.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)

## Component groups

- [Financial reporting](./financial): revenue, cost, EBITDA, energy balance, hash balance, and subsidy fee reports
- [Operational reporting](./operational): operational dashboard, energy consumption, and hashrate reports
- [Operational efficiency](./operations-efficiency): three-tab efficiency dashboard (`OperationsEfficiency`)
- [Multi-site reporting](./multi-site): portfolio-level `SiteReports` composite

## Reporting components

<FoundationComponentTablesByCategory categories={["Reporting tool"]} />

### `TimeframeControls`

<ComponentDescription name="TimeframeControls" />

Date-range picker for financial reporting pages. Supports year, month, and optional week granularity via connected selects. Drives the date range for the cost, EBITDA, energy balance, and other financial reports.

#### Import

```tsx
```

#### Props

All props are optional.

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `dateRange` | Optional | `{ start, end }` | none | Controlled date range in ms epoch; when omitted the control manages its own state |
| `onRangeChange` | Optional | `function` | none | Called with `([start, end], { year, month, period })` when the user changes the selection |
| `isMonthSelectVisible` | Optional | `boolean` | `true` | Show or hide the month select |
| `isWeekSelectVisible` | Optional | `boolean` | `true` | Show or hide the week select |
| `showResetButton` | Optional | `boolean` | `false` | Show a Reset button at the end of the toolbar |
| `onReset` | Optional | `function` | none | Called when the user clicks Reset |
| `layout` | Optional | `'horizontal' \| 'stacked'` | `'horizontal'` | Selects row layout |
| `hint` | Optional | `string` | none | Hint text shown above the toolbar in a banded strip |
| `timeframeType` | Optional | `TimeframeTypeValue \| null` | none | Controlled timeframe type (year / month / week) |
| `onTimeframeTypeChange` | Optional | `function` | none | Called when the resolved timeframe type changes |

#### Basic usage

```tsx

function ReportPage() {
  const [dateRange, setDateRange] = useState<FinancialDateRange>()

  return (
    <TimeframeControls
      isMonthSelectVisible
      isWeekSelectVisible={false}
      showResetButton
      dateRange={dateRange}
      onRangeChange={(range, opts) =>
        setDateRange({ start: range[0].getTime(), end: range[1].getTime(), period: opts.period })
      }
      onReset={() => setDateRange(undefined)}
    />
  )
}
```

For the backing state machine see [`useTimeframeControls`](/v0-4-0/reference/app-toolkit/hooks/components#usetimeframecontrols).

### `ReportTimeFrameSelector`

<ComponentDescription name="ReportTimeFrameSelector" />

Compact preset-window picker (1D / 7D / 30D / custom range) used inside the Miner Type and Mining Unit bar views of the operational efficiency and hashrate reports. Pair it with [`useReportTimeFrameSelectorState`](/v0-4-0/reference/app-toolkit/hooks/components#usereporttimeframeselectorstate) for the full controlled state.

#### Import

```tsx
```

#### Props

Props come directly from the `useReportTimeFrameSelectorState` return value; spread them onto the component:

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `presetTimeFrame` | Required | `number \| null` | none | **Required.** Active preset window in days (`1`, `7`, `30`) or `null` when the custom range is active |
| `dateRange` | Required | `{ start, end } \| null` | none | **Required.** Controlled date range; `null` while a preset is active |
| `setPresetTimeFrame` | Required | `function` | none | **Required.** Setter called when the user selects a preset |
| `setDateRange` | Required | `function` | none | **Required.** Setter called when the user picks a custom date range |

#### Basic usage

```tsx
  ReportTimeFrameSelector,
  useReportTimeFrameSelectorState,
} from '@tetherto/mdk-react-devkit/foundation'

function EfficiencyBarPanel() {
  const { presetTimeFrame, dateRange, setPresetTimeFrame, setDateRange } =
    useReportTimeFrameSelectorState()

  return (
    <ReportTimeFrameSelector
      presetTimeFrame={presetTimeFrame}
      dateRange={dateRange}
      setPresetTimeFrame={setPresetTimeFrame}
      setDateRange={setDateRange}
    />
  )
}
```

#### Styling

- `.mdk-report-time-frame-selector`: Root element

### `MetricCard`

<ComponentDescription name="MetricCard" />

Compact card displaying a labelled metric value with an optional unit, highlight state, and transparency. Used across financial and operational reporting composites for cost, EBITDA, hash, and energy metric tiles.

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `label` | Required | `string` | none | **Required.** Metric label text |
| `value` | Required | `number \| string \| null` | none | **Required.** Metric value; `null` renders a fallback dash |
| `unit` | Required | `string` | none | **Required.** Unit suffix appended after the value when value is not `null` |
| `bgColor` | Optional | `string` | `rgba(0,0,0,0.05)` | Background color CSS value |
| `isHighlighted` | Optional | `boolean` | `false` | Apply highlight text color to the value |
| `isTransparentColor` | Optional | `boolean` | `false` | Render the value text in a muted / transparent color |
| `isValueMedium` | Optional | `boolean` | `false` | Render the value at medium font weight |
| `showDashForZero` | Optional | `boolean` | `false` | When `true`, render a dash instead of `0` |
| `noMinWidth` | Optional | `boolean` | `false` | Remove the default minimum card width |
| `className` | Optional | `string` | none | Root class name from the host app |

#### Basic usage

```tsx

<MetricCard label="Total Cost" value={42500} unit="USD" />
```

#### Styling

- `.mdk-metric-card`: Root element
- `.mdk-metric-card--no-min-width`: Modifier removing the min-width constraint
- `.mdk-metric-card__label`: Label text
- `.mdk-metric-card__value`: Value text
- `.mdk-metric-card__value--medium`: Medium-weight value modifier

# Financial reporting (/v0-4-0/ui/react/foundation/reporting/financial)

<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Financial reporting components cover cost summary, EBITDA, energy balance, hash balance, subsidy fee, and revenue views. Most are self-contained reporting page composites — supply your API data and a `<TimeframeControls>` period selector; the component handles layout, charts, and metric tiles. `RevenueChart` is a standalone stacked-bar chart you can drop into a multi-site financial view.

For operational reporting see [Operational](/v0-4-0/ui/react/foundation/reporting/operational). For shared controls see the [Reporting tool overview](/v0-4-0/ui/react/foundation/reporting).

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- `<MdkProvider>` from `@tetherto/mdk-react-adapter`

## Components

<ComponentTable category="Reporting tool" subcategory="Financial" pkg="foundation" />

### `TimeframeControls`

<ComponentDescription name="TimeframeControls" />

The shared period selector for the reporting composites on this page — pass it into each composite's controls slot (`controls`, `datePicker`, or `timeframeControls` depending on the component). It drives year, month, and week granularity and is also used by the operational surfaces.

#### Import

```tsx
```

#### Props

All props are optional.

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `dateRange` | Optional | `TimeframeControlsDateRange` | — | Currently selected date range driving the connected selects |
| `timeframeType` | Optional | `TimeframeTypeValue \| null` | `null` | Active granularity (year / month / week) |
| `onRangeChange` | Optional | `TimeframeControlsOnRangeChange` | — | Fires when the selected date range changes |
| `onTimeframeTypeChange` | Optional | `(type: TimeframeTypeValue) => void` | — | Fires when the granularity changes |
| `isMonthSelectVisible` | Optional | `boolean` | — | Toggles visibility of the month select |
| `isWeekSelectVisible` | Optional | `boolean` | — | Toggles visibility of the week select |
| `layout` | Optional | `'horizontal' \| 'stacked'` | — | Arrangement of the connected selects |
| `showResetButton` | Optional | `boolean` | — | Renders a reset button |
| `onReset` | Optional | `VoidFunction` | — | Fires when the reset button is clicked |
| `hint` | Optional | `string` | — | Helper text shown alongside the selector |

#### Basic usage

Pair it with the [`useTimeframeControls`](/v0-4-0/reference/app-toolkit/hooks/components#usetimeframecontrols) state machine, then pass it into a composite's controls slot:

```tsx

const { dateRange, timeframeType, onRangeChange, onTimeframeTypeChange } = useTimeframeControls({ /* … */ })

<Cost
  controls={
    <TimeframeControls
      dateRange={dateRange}
      timeframeType={timeframeType}
      onRangeChange={onRangeChange}
      onTimeframeTypeChange={onTimeframeTypeChange}
    />
  }
/>
```

#### Behavior

Renders connected year / month / week selects. Hide the month or week select via `isMonthSelectVisible` / `isWeekSelectVisible`, switch between inline and stacked arrangement with `layout`, and optionally surface a reset button (`showResetButton` + `onReset`). The selection state itself is owned by [`useTimeframeControls`](/v0-4-0/reference/app-toolkit/hooks/components#usetimeframecontrols).

### `Cost`

<ComponentDescription name="Cost" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `metrics` | Required | `CostSummaryDisplayMetrics \| null` | none | Derived metric tiles from `buildCostSummaryViewModel` |
| `costLog` | Required | `ReadonlyArray<CostTimeSeriesEntry>` | none | Monthly cost time-series for the bar charts |
| `btcPriceLog` | Required | `ReadonlyArray<BtcPriceTimeSeriesEntry>` | none | BTC price overlay series |
| `totals` | Required | `CostSummaryMonetaryTotals \| null` | none | Aggregate totals for the summary tile |
| `dateRange` | Required | `FinancialDateRange \| null` | none | Active period — drives chart x-axis |
| `avgAllInCostData` | Optional | `ReadonlyArray<AvgAllInCostDataPoint>` | none | Revenue vs cost series for the Avg All-in Cost panel |
| `controls` | Required | `ReactElement` | none | Period selector slot — pass `<TimeframeControls>` |
| `setCostAction` | Optional | `ReactElement` | none | Optional header action slot (e.g. a "Set Monthly Cost" link) |
| `isLoading` | Optional | `boolean` | `false` | Show a loading state |
| `error` | Optional | `unknown` | none | Show an error state when defined |

#### Data preparation

Use `buildCostSummaryViewModel` to transform a raw API response into the props the component expects:

```tsx

const viewModel = buildCostSummaryViewModel({ data: apiResponse })
// → { metrics, costLog, btcPriceLog, totals }
```

#### `FinancialDateRange` type

```tsx
type FinancialDateRange = {
  start: number   // epoch ms
  end: number     // epoch ms
  period?: 'daily' | 'weekly' | 'monthly' | 'yearly'
}
```

#### Basic usage

```tsx
  Cost,
  buildCostSummaryViewModel,
  TimeframeControls,
  PERIOD,
} from '@tetherto/mdk-react-devkit/foundation'

  const [dateRange, setDateRange] = useState<FinancialDateRange>({
    start: startOfMonth(new Date()).getTime(),
    end: endOfMonth(new Date()).getTime(),
    period: PERIOD.MONTHLY,
  })

  const viewModel = useMemo(
    () => buildCostSummaryViewModel({ data: apiResponse }),
    [apiResponse],
  )

  return (
    <Cost
      {...viewModel}
      dateRange={dateRange}
      controls={
        <TimeframeControls
          isMonthSelectVisible
          isWeekSelectVisible={false}
          dateRange={{ start: dateRange.start, end: dateRange.end }}
          onRangeChange={(range, opts) =>
            setDateRange({ start: range[0].getTime(), end: range[1].getTime(), period: opts.period })
          }
        />
      }
    />
  )
}
```

#### Behavior

Renders a "Cost Summary" page header, the `controls` slot (period selector), and the `CostContent` 2×2 grid of charts and metric tiles (production cost chart, operations energy chart, avg all-in cost chart, and cost metric cards).

#### Styling

- `.mdk-cost`: Root element
- `.mdk-cost__header`: Title and optional action row
- `.mdk-cost__page-title`: "Cost Summary" heading
- `.mdk-cost__controls`: Controls slot wrapper

### `Ebitda`

<ComponentDescription name="Ebitda" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `metrics` | Required | `CostSummaryDisplayMetrics \| null` | none | Derived metric tiles from `buildCostSummaryViewModel` |
| `ebitdaChartInput` | Required | `ToBarChartDataInput \| null` | none | EBITDA bar chart series |
| `btcProducedChartInput` | Required | `ToBarChartDataInput \| null` | none | BTC produced bar chart series |
| `hasBtcProducedAllZeros` | Required | `boolean` | none | Hides the BTC produced series when all values are zero |
| `showEbitdaBarChart` | Required | `boolean` | none | Toggle the EBITDA bar chart panel |
| `currentBTCPrice` | Required | `number` | none | Live BTC price used in metric calculations |
| `datePicker` | Required | `ReactElement` | none | Period selector slot |
| `hasDateSelection` | Required | `boolean` | none | When `false` shows a "select a period" hint instead of empty data |
| `isLoading` | Optional | `boolean` | `false` | Show a loading state |
| `errors` | Optional | `string[]` | `[]` | Error messages displayed as an alert |
| `setCostHref` | Optional | `string` | none | When provided, shows a "Set Monthly Cost" gear link in the header |

#### Basic usage

```tsx

<Ebitda
  metrics={ebitdaMetrics}
  ebitdaChartInput={ebitdaChartData}
  btcProducedChartInput={btcProducedData}
  hasBtcProducedAllZeros={false}
  showEbitdaBarChart={true}
  currentBTCPrice={65000}
  datePicker={<TimeframeControls {...timeframeProps} />}
  hasDateSelection={true}
/>
```

#### Behavior

Renders an "EBITDA" page header, period selector slot, and (when `hasDateSelection` is true and data is available) the `EbitdaMetrics` card grid and `EbitdaCharts` panels. Shows a "please select a period" hint when `hasDateSelection` is false. A full-page spinner appears while `isLoading` is true.

#### Styling

- `.mdk-ebitda`: Root element
- `.mdk-ebitda__header`: Title and optional action row
- `.mdk-ebitda__page-title`: "EBITDA" heading
- `.mdk-ebitda__header-actions`: "Set Monthly Cost" link wrapper
- `.mdk-ebitda__controls`: Date picker slot
- `.mdk-ebitda__loading`: Spinner overlay
- `.mdk-ebitda__error`: Error / empty state container

### `EnergyBalance`

<ComponentDescription name="EnergyBalance" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `viewModel` | Required | `EnergyBalanceViewModel` | none | Full view-model from `useEnergyBalance` hook |
| `onTabChange` | Required | `function` | none | Called when the Revenue/Cost tab changes |
| `onRevenueDisplayModeChange` | Required | `function` | none | Called when the revenue chart display mode toggles |
| `onCostDisplayModeChange` | Required | `function` | none | Called when the cost chart display mode toggles |
| `timeframeControls` | Optional | `ReactNode` | none | Period selector slot rendered above the tabs |
| `setCostHref` | Optional | `string` | none | When provided, shows a "Set Monthly Cost" gear link in the header |
| `isDemoMode` | Optional | `boolean` | `false` | Suppresses error alerts (for demo/storybook use) |

#### `EnergyBalanceTab` type

```tsx
type EnergyBalanceTab = 'revenue' | 'cost'
```

#### Basic usage

```tsx

  const viewModel = useEnergyBalance({ data: apiData, dateRange })

  return (
    <EnergyBalance
      viewModel={viewModel}
      onTabChange={(tab) => setActiveTab(tab)}
      onRevenueDisplayModeChange={(mode) => setRevenueMode(mode)}
      onCostDisplayModeChange={(mode) => setCostMode(mode)}
      timeframeControls={<TimeframeControls {...timeframeProps} />}
    />
  )
}
```

#### Behavior

Renders an "Energy Balance" page header, optional timeframe controls, and a two-tab layout:
- **Energy Revenue** — revenue metrics cards, revenue bar chart, [`AverageDowntimeChart`](/v0-4-0/ui/react/core/components/charts#averagedowntimechart) (curtailment vs operational issues), power chart
- **Energy Cost** — cost metrics cards, cost bar chart, power chart

Empty and no-selection states are handled internally. A loading spinner overlays the view while data fetches.

#### Styling

- `.mdk-energy-balance`: Root element
- `.mdk-energy-balance__header`: Title and optional action row
- `.mdk-energy-balance__controls`: Timeframe controls slot
- `.mdk-energy-balance__tabs`: Tab container
- `.mdk-energy-balance__tabs-list`: Tab switcher row
- `.mdk-energy-balance__tabs-trigger`: Individual tab button
- `.mdk-energy-balance__tabs-content`: Tab panel content area
- `.mdk-energy-balance__loading`: Spinner overlay
- `.mdk-energy-balance__error`: Error / empty state

### `HashBalance`

<ComponentDescription name="HashBalance" />

#### Import

```tsx
```

#### Props

All props are optional (the component renders an empty/loading state when omitted).

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Optional | `HashRevenueResponse \| null` | — | Hash revenue API response |
| `isLoading` | Optional | `boolean` | `false` | Show a loading state |
| `isError` | Optional | `boolean` | `false` | Show an error alert |
| `errorMessage` | Optional | `string` | `'Error loading hash balance data…'` | Override error message text |
| `initialDateRange` | Optional | `FinancialDateRange` | current month | Starting period for the integrated timeframe controls |
| `onDateRangeChange` | Optional | `function` | — | Fires when the user changes the period |
| `className` | Optional | `string` | — | Root class override |
| `tabsClassName` | Optional | `string` | — | Class on the tab container |
| `tabsListClassName` | Optional | `string` | — | Class on the tab list |

#### Basic usage

```tsx

<HashBalance
  data={hashRevenueData}
  isLoading={isLoading}
  onDateRangeChange={(range, query) => fetchHashBalance(query)}
/>
```

#### Behavior

Renders integrated `TimeframeControls` (year/month/week) with a reset button, and a two-tab layout:
- **Hash Revenue** — site hash revenue, network hashrate, and network hashprice charts with metric cards; includes a currency toggle (USD / BTC)
- **Hash Cost** — hash cost vs revenue comparison charts

Date range is managed internally unless `initialDateRange` is supplied; `onDateRangeChange` fires on every period change.

#### Styling

- `.mdk-hash-balance`: Root element
- `.mdk-hash-balance__loading`: Spinner overlay
- `.mdk-hash-balance__error`: Error alert
- `.mdk-hash-balance__tabs`: Tab container
- `.mdk-hash-balance__tabs-list`: Tab row
- `.mdk-hash-balance__tabs-trigger`: Individual tab button
- `.mdk-hash-balance__tabs-content`: Tab panel

### `SubsidyFee`

<ComponentDescription name="SubsidyFee" />

#### Import

```tsx
```

#### Props

All props are optional.

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Optional | `HashRevenueResponse \| null` | — | Hash revenue API response |
| `log` | Optional | `SubsidyFeesLogEntry[]` | — | Per-block log entries (alternative to `data`) |
| `isLoading` | Optional | `boolean` | `false` | Show a loading state |
| `isError` | Optional | `boolean` | `false` | Show an error alert |
| `errorMessage` | Optional | `string` | `'Error loading hash balance data…'` | Override error message text |
| `showSummaryCards` | Optional | `boolean` | `false` | Show Total Subsidy, Total Fees, and Average Fees stat cards above the charts |
| `onDateRangeChange` | Optional | `function` | — | Fires when the user changes the period |

#### Basic usage

```tsx

<SubsidyFee
  data={subsidyFeesData}
  isLoading={isLoading}
  showSummaryCards={true}
  onDateRangeChange={(range, query) => fetchSubsidyFees(query)}
/>
```

#### Behavior

Renders integrated `TimeframeControls` (year/month/week) and two chart panels side by side:
- **Subsidy/Fees** — stacked bar chart of mining reward breakdown (subsidy vs transaction fees) with a % fee ratio overlay on the right y-axis
- **Average Fees** — average fees in sats/vbyte bar chart

When `showSummaryCards` is true, three `SingleStatCard` tiles (Total Subsidy, Total Fees, Average Fees) appear above the charts.

#### Styling

- `.mdk-subsidy-fee__panel`: Each chart panel wrapper
- `.mdk-subsidy-fee__panel--primary`: Primary variant

### `RevenueChart`

<ComponentDescription name="RevenueChart" />

Stacked bar chart showing monthly Bitcoin revenue across multiple mining sites. Values display in BTC when the per-label average exceeds 1 BTC, otherwise they are converted to Sats.

#### Import

```tsx
```

#### Props

All props are optional (the component renders an empty/loading state when omitted).

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `data` | Optional | `RevenueDataItem[]` | `[]` | Pre-fetched revenue records — each entry is one time period with a `timeKey`, `period`, `timestamp`, and one numeric value per site ID |
| `isLoading` | Optional | `boolean` | `false` | Shows a loading spinner while data is being fetched |
| `siteList` | Optional | `(string \| SiteItem)[]` | `[]` | Resolves `data` site IDs to display names in the legend; pass `string[]` of IDs or `{ id, name? }[]` objects |
| `legendPosition` | Optional | `'top' \| 'right' \| 'bottom' \| 'left'` | `'bottom'` | Chart legend position |
| `legendAlign` | Optional | `'start' \| 'center' \| 'end'` | `'start'` | Chart legend alignment |

#### Data shape

Each array element is one time bucket; site IDs are dynamic keys holding that site's revenue in BTC:

```ts
{
  timeKey: 'Jan 2024',   // X-axis label
  period: 'monthly',
  timestamp: 1704067200000,
  'site-a': 0.0042,      // Revenue in BTC for site-a
  'site-b': 0.0031,      // Revenue in BTC for site-b
}
```

#### Basic usage

```tsx

const siteList = [
  { id: 'site-a', name: 'Paraguay' },
  { id: 'site-b', name: 'Uruguay' },
]

<RevenueChart data={revenueData} siteList={siteList} isLoading={isLoadingRevenue} />
```

#### Behavior

Renders a stacked column chart with one series per site inside a `ChartContainer`. Currency is auto-detected: if any per-label daily average is greater than 1 BTC the chart displays BTC (₿); otherwise values are multiplied by 1,000,000 and shown in Sats. While `isLoading` is true a spinner replaces the chart. No internal data fetching — pass data from your query layer.

#### Styling

- `.mdk-revenue-chart`: Root element
- `.mdk-revenue-chart__header`: Title and currency-unit row
- `.mdk-revenue-chart__title`: "Revenue" heading
- `.mdk-revenue-chart__unit`: Currency unit label

## Related hooks

| Hook | Supplies |
|------|---------|
| [`useFinancialDateRange`](/v0-4-0/reference/app-toolkit/hooks/components#usefinancialdaterange) | Resolves the active financial date range consumed by reporting-section queries |
| [`useEbitda`](/v0-4-0/reference/app-toolkit/hooks/components#useebitda) | Transforms raw EBITDA API response into the view-model shape for `<Ebitda />` |
| [`useEnergyBalanceViewModel`](/v0-4-0/reference/app-toolkit/hooks/components#useenergybalanceviewmodel) | Computes the full energy-balance view model (tab and display-mode state) for `<EnergyBalance />` |
| [`useHashBalance`](/v0-4-0/reference/app-toolkit/hooks/utilities#usehashbalance) | Derives the hash-balance view model for `<HashBalance />` |
| [`useSubsidyFees`](/v0-4-0/reference/app-toolkit/hooks/utilities#usesubsidyfees) | Derives subsidy-fee series for `<SubsidyFee />` |

# Multi-site reporting (/v0-4-0/ui/react/foundation/reporting/multi-site)

<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

`SiteReports` is the multi-site report index — a duration toggle (weekly / monthly / yearly) and a sortable table of published report windows. Each row has a "View Report" action that the caller handles, so the component stays routing-agnostic.

For single-site financial reporting see [Financial](/v0-4-0/ui/react/foundation/reporting/financial).

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)

## Components

<ComponentTable category="Reporting tool" subcategory="Multi-site" pkg="foundation" />

### `SiteReports`

<ComponentDescription name="SiteReports" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `pageTitle` | Optional | `string` | `'Reports'` | Heading displayed above the toolbar |
| `siteName` | Optional | `string` | — | Optional site label shown below the heading |
| `duration` | Optional | `ReportDuration` | — | Controlled duration (`'weekly'`, `'monthly'`, `'yearly'`) |
| `defaultDuration` | Optional | `ReportDuration` | `'weekly'` | Initial duration when uncontrolled |
| `onDurationChange` | Optional | `function` | — | Fires when the user switches duration |
| `reports` | Optional | `SiteReportRecord[]` | auto-generated | Explicit report windows — when omitted, windows are auto-generated from the current date |
| `referenceDate` | Optional | `Date` | `new Date()` | Reference point for auto-generating report windows |
| `onViewReport` | Optional | `function` | — | Called when the user clicks "View Report" on a row; button is disabled when omitted |
| `className` | Optional | `string` | — | Optional root class override |

#### `SiteReportRecord` type

```tsx
type SiteReportRecord = {
  from: Date        // report window start
  to: Date          // report window end
  publishedAt: Date // when this report was published
}
```

#### `ReportDuration` type

```tsx
type ReportDuration = 'weekly' | 'monthly' | 'yearly'
```

#### `SiteReportViewContext` type

Passed as the second argument to `onViewReport`:

```tsx
type SiteReportViewContext = {
  duration: ReportDuration
  siteName?: string
}
```

#### Basic usage

```tsx

<SiteReports
  pageTitle="Reports"
  siteName="EU Site 01"
  onViewReport={(record, context) =>
    router.push(`/reports/${context.duration}/${record.from.toISOString()}`)
  }
/>
```

#### More examples

<Accordions>
  <Accordion title="Controlled duration">

```tsx
const [duration, setDuration] = useState<ReportDuration>('monthly')

<SiteReports
  pageTitle="Reports"
  duration={duration}
  onDurationChange={setDuration}
  onViewReport={handleViewReport}
/>
```

  </Accordion>
  <Accordion title="Providing explicit report windows">

When your API returns pre-computed report records, pass them directly to bypass auto-generation:

```tsx
<SiteReports
  pageTitle="Reports"
  reports={[
    {
      from: new Date('2026-01-01'),
      to: new Date('2026-01-07'),
      publishedAt: new Date('2026-01-08'),
    },
    {
      from: new Date('2026-01-08'),
      to: new Date('2026-01-14'),
      publishedAt: new Date('2026-01-15'),
    },
  ]}
  onViewReport={handleViewReport}
/>
```

  </Accordion>
</Accordions>

#### Behaviour

Renders a two-part surface:

1. **Toolbar** — page heading, optional site name, and a segmented duration toggle (Weekly / Monthly / Yearly) implemented as side-variant `Tabs`.
2. **Table** — `DataTable` with three columns: Date (the formatted report window), Date of Publish (formatted `publishedAt`), and Actions (a "View Report" button). Both date columns are sortable. The "View Report" button is disabled when `onViewReport` is not provided.

When `reports` is omitted, the component auto-generates windows backwards from `referenceDate` based on the active duration.

Duration is uncontrolled by default; pass `duration` + `onDurationChange` to make it controlled.

#### Styling

- `.mdk-site-reports`: Root element
- `.mdk-site-reports__toolbar`: Heading + duration toggle row
- `.mdk-site-reports__heading`: Page title text
- `.mdk-site-reports__site-name`: Site name label
- `.mdk-site-reports__duration`: Duration tab container
- `.mdk-site-reports__table`: Data table wrapper
- `.mdk-site-reports__view-button`: "View Report" action button

## Related hooks

| Hook | Supplies |
|------|---------|
| [`useHashrate`](/v0-4-0/reference/app-toolkit/hooks/components#usehashrate) | Normalises grouped-hashrate query results into the shape consumed by `<SiteReports />` |
| [`useEnergyReportSite`](/v0-4-0/reference/app-toolkit/hooks/components#useenergyreportsite) | Per-site energy data for the multi-site energy tab |

# Operational reporting (/v0-4-0/ui/react/foundation/reporting/operational)

<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

Operational reporting components cover a consolidated operations dashboard (`OperationalDashboard`), energy consumption (`EnergyReport`), and hashrate (`Hashrate`) views. `EnergyReport` and `Hashrate` are multi-tab reporting surfaces with site, miner-type, and mining-unit breakdown charts; `OperationalDashboard` is a 2×2 grid composite of the four core site-operations charts. Each is self-contained — supply your API data and the component handles layout, charts, and tab navigation.

For financial reporting see [Financial](/v0-4-0/ui/react/foundation/reporting/financial). For operational efficiency see [Operational efficiency](/v0-4-0/ui/react/foundation/reporting/operations-efficiency).

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- `<MdkProvider>` from `@tetherto/mdk-react-adapter`

## Components

<ComponentTable category="Reporting tool" subcategory="Operational" pkg="foundation" />

### `OperationalDashboard`

<ComponentDescription name="OperationalDashboard" />

#### Import

```tsx
```

#### Props

All props are optional.

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `hashrate` | Optional | `{ data, isLoading }` | none | Pre-shaped hashrate trend data from `useOperationsDashboard` |
| `consumption` | Optional | `{ data, isLoading }` | none | Pre-shaped power-consumption trend data |
| `efficiency` | Optional | `{ data, isLoading }` | none | Pre-shaped site-efficiency trend data |
| `miners` | Optional | `{ data, isLoading }` | none | Pre-shaped miners-status stacked-bar data |
| `controls` | Optional | `ReactElement` | none | Optional slot rendered above the grid (for example a date-range picker) |

#### Basic usage

```tsx

function OperationsDashboardPage({ hashrateLog, consumptionLog, efficiencyLog, minersLog }) {
  const vm = useOperationsDashboard({
    hashrate: { log: hashrateLog, isLoading },
    consumption: { log: consumptionLog, isLoading },
    efficiency: { log: efficiencyLog, isLoading },
    miners: { log: minersLog, isLoading },
  })

  return (
    <OperationalDashboard
      hashrate={vm.hashrate}
      consumption={vm.consumption}
      efficiency={vm.efficiency}
      miners={vm.miners}
    />
  )
}
```

#### Behavior

Renders a 2×2 grid of four `LineChartCard`-based chart cards: hashrate trend, power consumption trend, site efficiency trend, and miners status (stacked bar). Each card has an expand toggle that widens it to full grid width; expand state persists across remounts. Assumes a single `OperationalDashboard` instance is mounted at a time.

#### Styling

- `.mdk-operational-dashboard`: Root element
- `.mdk-operational-dashboard__controls`: Controls slot wrapper (only rendered when `controls` is supplied)
- `.mdk-operational-dashboard__grid`: 2×2 chart grid
- `.mdk-operational-dashboard__cell`: Individual chart cell
- `.mdk-operational-dashboard__cell--expanded`: Modifier applied when a card is expanded to full width

### `EnergyReport`

<ComponentDescription name="EnergyReport" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `defaultTab` | Optional | `EnergyReportTabValue` | `'site-view'` | Tab shown on first render — defaults to `'site-view'` |
| `siteView` | Optional | `HashrateSiteViewProps` | none | Props for the Site View tab |
| `minerTypeView` | Optional | `HashrateMinerTypeViewProps` | none | Props for the Miner Type View tab |
| `minerUnitView` | Optional | `EnergyReportMinerUnitViewProps` | none | Props for the Mining Unit View tab |
| `className` | Optional | `string` | none | Optional root class override |

#### `EnergyReportTabValue` type

```tsx
type EnergyReportTabValue = 'site-view' | 'miner-type-view' | 'miner-unit-view'
```

#### `EnergyReportSiteViewProps` key fields

```tsx
type EnergyReportSiteViewProps = {
  consumptionLog?: MetricsConsumptionLogEntry[]  // power/consumption time-series
  nominalPowerAvailabilityMw?: number | null      // site nominal capacity
  containers?: EnergyReportContainer[]            // container inventory
  tailLog?: EnergyReportTailLogItem[][]           // snapshot tail-log for power-mode table
  dateRange?: EnergyReportDateRange               // controlled date range
  onDateRangeChange?: (range: EnergyReportDateRange) => void
}
```

#### Basic usage

```tsx

  <EnergyReport
    siteView={{
      consumptionLog: consumptionData,
      nominalPowerAvailabilityMw: 500,
      containers,
      tailLog,
    }}
    minerTypeView={{ groupedConsumption, containers }}
    minerUnitView={{ groupedConsumption: unitGroupedData, containers }}
  />
)
```

#### More examples

<Accordions>
  <Accordion title="Minimal — site view only">

```tsx

<EnergyReport
  siteView={{
    consumptionLog: [
      { ts: Date.now() - 86_400_000, powerW: 180_000_000, consumptionMWh: 4.3 },
      { ts: Date.now(), powerW: 220_000_000, consumptionMWh: 5.1 },
    ],
    nominalPowerAvailabilityMw: 500,
  }}
/>
```

  </Accordion>
  <Accordion title="Starting on a specific tab">

```tsx
<EnergyReport
  defaultTab="miner-type-view"
  minerTypeView={{ groupedConsumption, containers }}
/>
```

  </Accordion>
</Accordions>

#### Behavior

Renders a three-tab layout:

- **Site View** — power consumption trend chart against nominal capacity, power-mode breakdown table per container. A `DateRangePicker` and Reset button appear above this tab only. Date range is managed internally unless `siteView.dateRange` is provided.
- **Miner Type View** — consumption grouped by miner type as a bar chart with per-miner-type breakdown.
- **Mining Unit View** — consumption grouped by mining unit as a bar chart.

Each tab fetches independently via its own prop bag.

#### Styling

- `.mdk-energy-report`: Root element
- `.mdk-energy-report__date-controls`: Date picker + Reset row (site view only)

### `Hashrate`

<ComponentDescription name="Hashrate" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `defaultTab` | Optional | `HashrateTabValue` | `'site-view'` | Tab shown on first render |
| `siteView` | Optional | `HashrateSiteViewProps` | none | Props for the Site View tab |
| `minerTypeView` | Optional | `HashrateMinerTypeViewProps` | none | Props for the Miner Type View tab |
| `miningUnitView` | Optional | `HashrateMiningUnitViewProps` | none | Props for the Mining Unit View tab |

#### `HashrateTabValue` type

```tsx
type HashrateTabValue = 'site-view' | 'miner-type-view' | 'mining-unit-view'
```

#### Site view tab props

`HashrateSiteView` renders an aggregate hashrate trend line across the whole site, with an optional miner-type filter that scopes the sum to a subset. A date-range picker and optional reset button appear in the control row.

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `log` | Optional | `HashrateGroupedLog` | `[]` | Hashrate log grouped by miner type |
| `isLoading` | Optional | `boolean` | `false` | Show a loading state on the chart |
| `dateRange` | Optional | `HashrateDateRange` | none | Controlled date range `{ start, end }` in ms epoch; drives the upstream query |
| `onDateRangeChange` | Optional | `function` | none | Fires when the user selects a new range from the date picker |
| `onReset` | Optional | `function` | none | When provided, renders a Reset button next to the date picker |

#### Miner type and mining unit tab props

Both `minerTypeView` and `miningUnitView` render bar charts. Charts share axis scaling when both tabs are visible so relative comparisons remain accurate.

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `chartInput` | Optional | `ToBarChartDataInput` | none | Declarative bar data (`{ labels, series }`) |
| `isEmpty` | Optional | `boolean` | `false` | When `true`, shows the chart empty state |
| `isLoading` | Optional | `boolean` | `false` | Show a loading state on the bar chart |

#### Basic usage

```tsx

<Hashrate
  siteView={{
    log: groupedHashrateLog,
    isLoading,
    dateRange,
    onDateRangeChange: setDateRange,
  }}
  minerTypeView={{ chartInput: minerTypeInput, isLoading }}
  miningUnitView={{ chartInput: miningUnitInput, isLoading }}
/>
```

#### More examples

<Accordions>
  <Accordion title="Empty / no-data state">

The component renders an empty shell when no props are supplied — safe to render while data is loading.

```tsx
<Hashrate />
```

  </Accordion>
  <Accordion title="Starting on miner type view">

```tsx
<Hashrate
  defaultTab="miner-type-view"
  minerTypeView={{ chartInput: minerTypeInput }}
/>
```

  </Accordion>
</Accordions>

#### Behavior

Renders a three-tab layout using a shared `Tabs` shell. Each tab fetches independently via its own prop bag:

- **Site View** — aggregate hashrate trend line for the whole site with optional miner-type filter; date-range picker and Reset button in the control row
- **Miner Type View** — hashrate broken down by miner model/type as a bar chart with a toggle legend
- **Mining Unit View** — hashrate broken down per mining unit (container) as a bar chart with a toggle legend

Miner type and mining unit bar charts share Y-axis scaling so relative comparisons across tabs remain accurate. Active tab is managed internally.

#### Styling

- `.mdk-hashrate`: Root element

### `OperationalDashboard`

<ComponentDescription name="OperationalDashboard" />

A 2×2 grid of the four site-operations charts — **Hashrate**, **Power Consumption**, **Site Efficiency** (line trends with an optional nominal reference line), and **Miners Status** (stacked daily breakdown). Each card can expand to full width, and the expand state persists across remounts.

The composite is pure glue: it renders pre-shaped data. Use the `useOperationsDashboard` hook to turn raw metric logs into the chart-ready payloads, then spread them in — the hook never fetches, so wire your own data layer.

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `hashrate` | Optional | `{ data?, isLoading? }` | none | Shaped hashrate trend (`LineChartCardData`) |
| `consumption` | Optional | `{ data?, isLoading? }` | none | Shaped power-consumption trend |
| `efficiency` | Optional | `{ data?, isLoading? }` | none | Shaped site-efficiency trend |
| `miners` | Optional | `{ data?, isLoading? }` | none | Shaped stacked miners-status data |
| `controls` | Optional | `ReactElement` | none | Controls slot (e.g. a date-range picker) rendered above the grid |

#### `useOperationsDashboard(input)`

`input` accepts one entry per chart. Trend inputs take `{ log, nominalValue?, isLoading?, error? }` where `log` is `{ ts, value }[]` in base units (hashrate MH/s, power W, efficiency W/TH/s). The miners input takes per-day `{ ts, online, error, offline, sleep, maintenance }` counts.

#### Basic usage

```tsx

const Dashboard = ({ queries }) => {
  const viewModel = useOperationsDashboard({
    hashrate: { log: queries.hashrate.log, nominalValue: queries.nominalHashrateMhs },
    consumption: { log: queries.consumption.log, nominalValue: queries.nominalPowerW },
    efficiency: { log: queries.efficiency.log, nominalValue: queries.nominalEfficiency },
    miners: { log: queries.miners.log },
  })

  return <OperationalDashboard {...viewModel} controls={<DateRangePicker />} />
}
```

#### Behavior

Renders the four charts in a 2×2 grid; any card expands to full width and the expand state persists across remounts. Hashrate is displayed in TH/s, power in MW, and efficiency in W/TH/s. A `nominalValue` renders a flat reference line. The individual chart components (`OperationalHashrateChart`, …) and `ChartExpandAction` are exported as `advanced` building blocks for custom layouts.

## Related hooks

| Hook | Supplies |
|------|---------|
| [`useOperationsDashboard`](/v0-4-0/reference/app-toolkit/hooks/components#useoperationsdashboard) | Shapes raw operational metric logs into chart-ready payloads for the four `OperationalDashboard` cards |
| [`useHashrate`](/v0-4-0/reference/app-toolkit/hooks/components#usehashrate) | Normalises a grouped-hashrate query result into the shape consumed by `<Hashrate />` |
| [`useEnergyReportSite`](/v0-4-0/reference/app-toolkit/hooks/components#useenergyreportsite) | Merges consumption metrics with tail-log and container data for `<EnergyReport />` |
| [`useReportTimeFrameSelectorState`](/v0-4-0/reference/app-toolkit/hooks/components#usereporttimeframeselectorstate) | Active time-frame window and setters for the time-frame selector |
| [`useTimeframeControls`](/v0-4-0/reference/app-toolkit/hooks/components#usetimeframecontrols) | Year/month/week state machine for `TimeframeControls` |

# Operational efficiency (/v0-4-0/ui/react/foundation/reporting/operations-efficiency)

<PackageBadge>@tetherto/mdk-react-devkit/foundation</PackageBadge>

`OperationsEfficiency` is a three-tab reporting surface:

- Site View
- Miner Type View
- Mining Unit View

Your app loads metrics or tail-log data, passes it through the `siteView`, `minerTypeView`, and `minerUnitView` prop bags, and the 
shell renders charts and time-frame controls.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)
- [`@tetherto/mdk-react-devkit/core`](/v0-4-0/ui/react/core) (tabs and charts are composed from core primitives)

### Import

```tsx
  OperationsEfficiency,
  toOperationsEfficiencyMinerType,
  toOperationsEfficiencyMinerUnit,
  TAIL_LOG_MINER_TYPE_KEY,
  TAIL_LOG_CONTAINER_KEY,
} from '@tetherto/mdk-react-devkit/foundation'
  EfficiencyDateRange,
  MetricsEfficiencyLogEntry,
} from '@tetherto/mdk-react-devkit/foundation'
```

## `OperationsEfficiency`

Tab shell built on [`Tabs`](/v0-4-0/ui/react/core/components/navigation#tabs) from `@tetherto/mdk-react-devkit/core`. Each tab receives an 
optional props object; omit a tab’s props to render it with empty defaults.

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `defaultTab` | Optional | `'site-view' \| 'miner-type-view' \| 'mining-unit-view'` | `'site-view'` | Initial active tab key |
| `siteView` | Optional | `EfficiencySiteViewProps` | none | Data and callbacks for the Site View line chart |
| `minerTypeView` | Optional | `EfficiencyMinerTypeViewProps` | none | Bar chart input for [Miner Type View](#miner-type-and-mining-unit-tabs) |
| `minerUnitView` | Optional | `EfficiencyMinerUnitViewProps` | none | Bar chart input for Mining Unit View (same shape as miner type) |

### Site view tab

Wire the Site View to **`/auth/metrics/efficiency`** (v2). Map the response log array to 
[`MetricsEfficiencyLogEntry`](/v0-4-0/reference/app-toolkit/ui-kit/types#metricsefficiencylogentry) and pass it on `siteView.log`.

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `log` | Optional | `MetricsEfficiencyLogEntry[]` | `[]` | Time series for the line chart (`ts`, `efficiencyWThs`) |
| `avgEfficiency` | Optional | `number \| null` | `null` | Average efficiency for the selected range; shown in the chart header |
| `nominalValue` | Optional | `number \| null` | `null` | Nominal efficiency target; when set, adds a nominal series to the chart |
| `isLoading` | Optional | `boolean` | `false` | When `true`, shows loading state on the chart container |
| `dateRange` | Optional | `EfficiencyDateRange` | none | `{ start, end }` millisecond range for the date picker |
| `onDateRangeChange` | Optional | `function` | none | Called when the user selects a new range from the chart brush or date picker |
| `onReset` | Optional | `function` | none | Called when the user activates **Reset** on the site chart header |

### Miner type and mining unit tabs

Miner Type View and Mining Unit View use bar charts fed by **`ToBarChartDataInput`** (`{ labels, series }`). Until 
the metrics API supports `groupBy=miner`, build that input from **tail-log** (`stat-5m` / `t-miner`) using the pure helpers below. 
A backend follow-up will allow swapping the data source in foundation without changing this props contract.

| Helper | Input | Output |
|--------|--------|--------|
| `toOperationsEfficiencyMinerType` | `{ tailLog }` | `{ chartInput, isEmpty }` — reads `TAIL_LOG_MINER_TYPE_KEY` from tail-log |
| `toOperationsEfficiencyMinerUnit` | `{ tailLog, containers? }` | `{ chartInput, isEmpty }` — reads `TAIL_LOG_CONTAINER_KEY`; optional `containers` for labels |

Pass the result on `minerTypeView` / `minerUnitView`:

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `chartInput` | Optional | `ToBarChartDataInput` | none | Declarative bar data; convert with [`buildBarChartData`](/v0-4-0/ui/react/core/components/charts/composition#chart-utilities) if you need Chart.js `data` elsewhere |
| `isEmpty` | Optional | `boolean` | `false` | When `true`, shows the chart empty state |
| `isLoading` | Optional | `boolean` | `false` | When `true`, shows loading on the bar chart container |
| `onTimeFrameChange` | Optional | `function` | none | Called with `(start: Date, end: Date)` when the preset or custom range changes |

Each bar view mounts a single-series toggle legend via `ChartContainer`'s `legendData` and `onToggleDataset` props. Clicking the legend item hides or shows the bar series. MDK tooltips on the bars format values with the efficiency unit (`W/TH/s`), consistent with the rest of the reporting surface.

### Example

```tsx
  OperationsEfficiency,
  toOperationsEfficiencyMinerType,
  toOperationsEfficiencyMinerUnit,
} from '@tetherto/mdk-react-devkit/foundation'

function OperationalEfficiencyPanel({
  efficiencyLog,
  minerTypeTailLog,
  containerTailLog,
}: {
  efficiencyLog: MetricsEfficiencyLogEntry[]
  minerTypeTailLog: Record<string, unknown>
  containerTailLog: Record<string, unknown>
}) {
  const [dateRange, setDateRange] = useState<EfficiencyDateRange | undefined>()

  const minerType = useMemo(
    () => toOperationsEfficiencyMinerType({ tailLog: minerTypeTailLog }),
    [minerTypeTailLog],
  )
  const minerUnit = useMemo(
    () => toOperationsEfficiencyMinerUnit({ tailLog: containerTailLog }),
    [containerTailLog],
  )

  return (
    <OperationsEfficiency
      siteView={{
        log: efficiencyLog,
        dateRange,
        onDateRangeChange: setDateRange,
      }}
      minerTypeView={{
        chartInput: minerType.chartInput,
        isEmpty: minerType.isEmpty,
      }}
      minerUnitView={{
        chartInput: minerUnit.chartInput,
        isEmpty: minerUnit.isEmpty,
      }}
    />
  )
}
```

## Next steps

Consider related reference materials:

- [Metrics efficiency types](/v0-4-0/reference/app-toolkit/ui-kit/types#metrics-efficiency-types): `MetricsEfficiencyLogEntry` and 
response wrapper for the site tab
- [Chart composition](/v0-4-0/ui/react/core/components/charts/composition): `buildBarChartData` for bar chart `data` shapes

# Settings components (/v0-4-0/ui/react/foundation/settings)

The [`@tetherto/mdk-react-devkit/foundation`](/v0-4-0/ui/react/foundation) package provides pre-built settings UI for common administrative tasks.

Either use:

1. **All-in-one dashboard** ([`SettingsDashboard`](#settings-dashboard)): renders all settings sections in collapsible accordions.
2. **Individual components**: use [`FeatureFlagsSettings`](/v0-4-0/ui/react/foundation/settings/feature-flags),
   [`HeaderControlsSettings`](/v0-4-0/ui/react/foundation/settings/header-controls), etc. directly for custom layouts.

## Settings dashboard

<ComponentDescription name="SettingsDashboard" />

Each section renders if you provide its props, giving you control over the sections you need.

```tsx
```

## Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `showFeatureFlags` | Optional | `boolean` | `false` | Show the feature flags section (requires `featureFlagsProps`) |
| `headerControlsProps` | Optional | [`HeaderControlsSettingsProps`](/v0-4-0/ui/react/foundation/settings/header-controls) | none | Props for header controls section |
| `rbacControlProps` | Optional | [`RBACControlSettingsProps`](/v0-4-0/ui/react/foundation/settings/access-control) | none | Props for RBAC/user management section |
| `importExportProps` | Optional | [`ImportExportSettingsProps`](/v0-4-0/ui/react/foundation/settings/import-export) | none | Props for import/export section |
| `featureFlagsProps` | Optional | [`FeatureFlagsSettingsProps`](/v0-4-0/ui/react/foundation/settings/feature-flags) | none | Props for feature flags section |
| `dangerActions` | Optional | `ActionButtonProps[]` | none | Entries forwarded to [`ActionButton`](/v0-4-0/ui/react/core/components/forms#actionbutton) (`@tetherto/mdk-react-devkit/core`). See [advanced usage for danger actions with confirmation](#advanced-usage-for-danger-actions-with-confirmation). |
| `className` | Optional | `string` | none | Additional CSS class |

## Basic usage

```tsx

function SettingsPage() {
  return (
    <SettingsDashboard
      headerControlsProps={{
        preferences: headerPrefs,
        onToggle: handleToggle,
        onReset: handleReset,
      }}
      rbacControlProps={{
        users,
        roles,
        rolePermissions,
        permissionLabels,
        canWrite: true,
        onCreateUser,
        onUpdateUser,
        onDeleteUser,
      }}
      importExportProps={{
        onExport: handleExport,
        onImport: handleImport,
        onParseFile: parseSettingsFile,
      }}
      dangerActions={[
        {
          label: 'Reboot system',
          variant: 'danger',
          confirmation: {
            title: 'Reboot system',
            description: 'This restarts device workers. Ensure no pending actions remain.',
            onConfirm: handleReboot,
          },
        },
      ]}
    />
  )
}
```

## Advanced usage for danger actions with confirmation

Use **`dangerActions`** when you need high-impact controls below the **Settings** heading and above the accordion sections. Each item follows **[`ActionButton`](/v0-4-0/ui/react/core/components/forms#actionbutton)** props: at minimum `label`, `variant`, and a **`confirmation`** object. Set **`mode: 'dialog'`** for modal confirmations (for example two side-by-side danger actions).

```tsx
<SettingsDashboard
  headerControlsProps={/* ... */}
  rbacControlProps={/* ... */}
  importExportProps={/* ... */}
  dangerActions={[
    {
      label: 'Restart orchestration',
      variant: 'danger',
      mode: 'dialog',
      confirmation: {
        title: 'Restart orchestration',
        description: (
          <p>
            Restarts orchestration workers. Ensure maintenance windows are respected.
          </p>
        ),
        onConfirm: () => {
          void restartOrchestration()
        },
      },
    },
    {
      label: 'Disable automation policy',
      variant: 'danger',
      mode: 'dialog',
      confirmation: {
        title: 'Disable automation policy',
        description:
          'Automated mitigations will stop until you re-enable the policy.',
        onConfirm: () => {
          void disableAutomationPolicy()
        },
      },
    },
  ]}
/>
```

Full **`ActionButton`** semantics (**`confirmation`** fields, **`mode`**, **`loading`**, etc.) live under **[Form components: ActionButton](/v0-4-0/ui/react/core/components/forms#actionbutton)**.

## Accordion sections

The dashboard renders each settings section inside an [`Accordion`](/v0-4-0/ui/react/core/components/data-display#accordion) component
from [`@tetherto/mdk-react-devkit/core`](/v0-4-0/ui/react/core):

- **[Header Controls](/v0-4-0/ui/react/foundation/settings/header-controls)**: toggle visibility of header metrics
- **[Access control](/v0-4-0/ui/react/foundation/settings/access-control)**: manage users and role-based permissions
- **[Import / Export](/v0-4-0/ui/react/foundation/settings/import-export)**: backup and restore configuration
- **[Feature Flags](/v0-4-0/ui/react/foundation/settings/feature-flags)**: toggle feature flags (requires `showFeatureFlags={true}`)

## Styling

The component uses the `.mdk-settings-dashboard` CSS class. Key selectors:

- `.mdk-settings-dashboard__title`: main heading
- `.mdk-settings-dashboard__danger-actions`: container for danger action buttons
- `.mdk-settings-dashboard__accordions`: container for accordion sections

## Individual components

Use these components directly when you need a custom layout or only one settings section:

<ComponentTable category="Settings" pkg="foundation" />

# Access control settings (/v0-4-0/ui/react/foundation/settings/access-control)

<ComponentDescription name="RBACControlSettings" />

It manages:

1. **Access control** (`canWrite`): if `false`, hides edit/delete actions.
2. **User data** (`users`): array of users to display.
3. **Role configuration** (`roles`, `rolePermissions`, `permissionLabels`): define available roles and permissions.
4. **Callbacks** (`onCreateUser`, `onUpdateUser`, `onDeleteUser`): handle CRUD operations.
5. **State** (`isLoading`): loading indicator.
6. **Styling** (`className`): optional CSS class.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)

## Import

```tsx
```

## Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `canWrite` | Required | `boolean` | none | If `false`, hides edit/delete actions |
| `users` | Required | `SettingsUser[]` | none | Array of users to display |
| `roles` | Required | `RoleOption[]` | none | Available roles for assignment |
| `rolePermissions` | Required | `Record<string, Record<string, PermLevel>>` | none | Permissions matrix by role |
| `permissionLabels` | Required | `Record<string, string>` | none | Human-readable permission names |
| `onCreateUser` | Required | `function` | none | Create user callback |
| `onUpdateUser` | Required | `function` | none | Update user callback |
| `onDeleteUser` | Required | `function` | none | Delete user callback |
| `isLoading` | Optional | `boolean` | `false` | Loading state |
| `className` | Optional | `string` | none | Additional CSS class |

## Data structure

```tsx
  SettingsUser,
  RoleOption,
  PermLevel,
} from '@tetherto/mdk-react-devkit/foundation'

type SettingsUser = {
  id: string
  name?: string
  email: string
  role: string
  lastActive?: string
  last_login?: string
}

type RoleOption = {
  value: string
  label: string
}

type PermLevel = 'r' | 'rw' | false  // read, read-write, none

type CreateUserData = {
  name: string
  email: string
  role: string
}

type UpdateUserData = CreateUserData & { id: string }
```

## Callbacks

### `onCreateUser`

Called when end user submits the add user form. Receives `CreateUserData`:

```tsx
const handleCreateUser = async (data: CreateUserData) => {
  const newUser = await api.createUser(data)
  setUsers(prev => [...prev, newUser])
}
```

### `onUpdateUser`

Called when end user saves changes in the manage modal. Receives `UpdateUserData`:

```tsx
const handleUpdateUser = async (data: UpdateUserData) => {
  await api.updateUser(data)
  setUsers(prev => prev.map(u => u.id === data.id ? { ...u, ...data } : u))
}
```

### `onDeleteUser`

Called after end user confirms deletion. Receives the user ID:

```tsx
const handleDeleteUser = async (userId: string) => {
  await api.deleteUser(userId)
  setUsers(prev => prev.filter(u => u.id !== userId))
}
```

## Basic usage

```tsx

function UserManagementSection() {
  const [users, setUsers] = useState<SettingsUser[]>([])
  const [isLoading, setIsLoading] = useState(true)

  useEffect(() => {
    api.getUsers().then(data => {
      setUsers(data)
      setIsLoading(false)
    })
  }, [])

  const handleCreateUser = async (data) => {
    const newUser = await api.createUser(data)
    setUsers(prev => [...prev, newUser])
  }

  const handleUpdateUser = async (data) => {
    await api.updateUser(data)
    setUsers(prev =>
      prev.map(u => u.id === data.id ? { ...u, ...data } : u)
    )
  }

  const handleDeleteUser = async (userId) => {
    await api.deleteUser(userId)
    setUsers(prev => prev.filter(u => u.id !== userId))
  }

  return (
    <RBACControlSettings
      canWrite={true}
      users={users}
      roles={[
        { value: 'admin', label: 'Admin' },
        { value: 'operator', label: 'Operator' },
        { value: 'viewer', label: 'Viewer' },
      ]}
      rolePermissions={{
        admin: { miners: 'rw', settings: 'rw', users: 'rw' },
        operator: { miners: 'rw', settings: 'r', users: false },
        viewer: { miners: 'r', settings: 'r', users: false },
      }}
      permissionLabels={{
        miners: 'Miner Management',
        settings: 'Settings',
        users: 'User Management',
      }}
      isLoading={isLoading}
      onCreateUser={handleCreateUser}
      onUpdateUser={handleUpdateUser}
      onDeleteUser={handleDeleteUser}
    />
  )
}
```

## Features

### User table

Displays users in a table with columns:

- **User**: name with tooltip
- **Email**: email address with tooltip
- **Assigned Roles**: role badge with color coding
- **Last Active**: formatted timestamp
- **Manage**: opens edit modal
- **Delete**: deletes with confirmation

### Search

Filter users by name, email, or role using the search input.

### Role badges

Roles are displayed as colored badges. Colors are determined by [`getRoleBadgeColors()`](/v0-4-0/reference/app-toolkit/ui-kit/constants#getrolebadgecolors)
from the foundation constants.

### Integrated modals

The component includes three modal dialogs:

- `AddUserModal`: create new users
- `ManageUserModal`: edit user details and view permissions
- `ChangeConfirmationModal`: confirm destructive actions

## Permission check

When `canWrite` is `false`:

- **Add User** button is hidden
- **Manage** and **Delete** buttons are hidden
- User list is read-only

```tsx
<RBACControlSettings
  canWrite={false}  // Read-only mode
  // ...other props
/>
```

## Styling

The component uses the `.mdk-settings-rbac` CSS class. Key selectors:

- `.mdk-settings-rbac__description`: header text
- `.mdk-settings-rbac__actions-row`: search and add button row
- `.mdk-settings-rbac__table`: user table container
- `.mdk-settings-rbac__role-badge`: role badge styling
- `.mdk-settings-rbac__manage-btn`: manage button
- `.mdk-settings-rbac__delete-btn`: delete button

# Feature flags settings (/v0-4-0/ui/react/foundation/settings/feature-flags)

<ComponentDescription name="FeatureFlagsSettings" />

It manages:

1. **User access control** (`isEditingEnabled`): if `false`, shows empty state and user is not provided with toggles.
2. **What data?** (`featureFlags`): the flags to display as toggles.
3. **What happens on save?** (`onSave`): callback receiving updated flags.
4. **State indicators** (`isLoading`, `isSaving`): optional user feedback on loading/saving states.
5. **Styling** (`className`): optional CSS class.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)

## Import

```tsx
```

## Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `isEditingEnabled` | Required | `boolean` | none | If `false`, shows empty state and other props have no effect |
| `featureFlags` | Required | `Record<string, boolean>` | none | Object mapping flag names to enabled/disabled state |
| `onSave` | Required | `function` | none | Callback when save is clicked |
| `isLoading` | Optional | `boolean` | `false` | Loading state |
| `isSaving` | Optional | `boolean` | `false` | Saving state |
| `className` | Optional | `string` | none | Additional CSS class |

## Data structure

The `featureFlags` prop accepts an object where:
- **Keys** are flag names (arbitrary strings defined by your application)
- **Values** are booleans (`true` = enabled, `false` = disabled)

```tsx
{
  darkMode: true,
  betaFeatures: false,
  advancedMetrics: true,
}
```

This component renders toggles for whatever flags you pass in. Your application or backend determines the available flags.

## Basic usage

Changes are held locally until the user clicks Save. The `onSave` callback receives the updated flags object:

```tsx
const handleSave = async (updatedFlags: Record<string, boolean>) => {
  await api.saveFeatureFlags(updatedFlags)  // persist to backend
  setFlags(updatedFlags)                     // update local state
}
```

<Callout type="warning">
The component does not persist data, you must implement the save logic.
</Callout>

### Example

```tsx

function FeatureFlagsPage() {
  // Flags reflecting your API or app config
  const [flags, setFlags] = useState<Record<string, boolean>>({
    darkMode: true,
    betaFeatures: false,
    advancedMetrics: true,
  })
  const [isSaving, setIsSaving] = useState(false)

  const handleSave = async (updatedFlags: Record<string, boolean>) => {
    setIsSaving(true)
    await api.saveFeatureFlags(updatedFlags)
    setFlags(updatedFlags)
    setIsSaving(false)
  }

  return (
    <FeatureFlagsSettings
      featureFlags={flags}
      isEditingEnabled={true}
      isSaving={isSaving}
      onSave={handleSave}
    />
  )
}
```

## Features

### Add flags

Developers add new feature flags through the UI by entering comma-separated names. Flag names are arbitrary strings; the component does not validate
against a predefined list.

### Toggle flags

Each flag displays a toggle switch to enable/disable it. Changes are held locally until the end user clicks **Save**.

### Delete flags

End user can click the trash icon next to any flag to remove it from the list.

## Permission check

When `isEditingEnabled` is `false`, the component renders an empty state:

```tsx
<FeatureFlagsSettings
  featureFlags={flags}
  isEditingEnabled={false}  // Shows "Update feature flags not enabled"
  onSave={handleSave}
/>
```

## Integration

Feature flags are typically fetched from your backend API. This component provides the UI for viewing and editing them, it does not manage persistence.
Use the `onSave` callback to sync changes back to your server.

## Styling

The component uses the `.mdk-settings-feature-flags` CSS class. Key selectors:

{/* todo when on public (verify link) The component uses the [`.mdk-settings-feature-flags`](https://github.com/tetherto/mdk/blob/main/ui/packages/react-devkit/src/foundation/components/settings/feature-flags/USAGE.md) CSS class. Key selectors: */}

- `.mdk-settings-feature-flags__add-row`: Input and add button container
- `.mdk-settings-feature-flags__toggles`: Grid of flag toggles
- `.mdk-settings-feature-flags__flag-item`: Individual flag row
- `.mdk-settings-feature-flags__save`: Save button container

## Related hooks

| Hook | Supplies |
|------|---------|
| [`useIsFeatureEditingEnabled`](/v0-4-0/reference/app-toolkit/hooks/utilities#useisfeatureeditingenabled) | Permission guard that returns `true` when the current user can toggle feature flags |

# Header controls settings (/v0-4-0/ui/react/foundation/settings/header-controls)

<ComponentDescription name="HeaderControlsSettings" />

It provides:

1. **Data** (`preferences`): current visibility state for header items.
2. **Callbacks** (`onToggle`, `onReset`): handle changes and reset to defaults.
3. **State** (`isLoading`): optional loading feedback.
4. **Styling** (`className`): optional CSS class.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)

## Import

```tsx
```

## Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `preferences` | Required | [`HeaderPreferences`](/v0-4-0/reference/app-toolkit/ui-kit/constants#headerpreferences-type) | none | Current header visibility preferences |
| `onToggle` | Required | `function` | none | Callback when a toggle changes |
| `onReset` | Required | `function` | none | Callback to reset to defaults |
| `isLoading` | Optional | `boolean` | `false` | Loading state |
| `className` | Optional | `string` | none | Additional CSS class |

## Data structure

The `HeaderPreferences` type maps header item keys to visibility booleans. See [constants](/v0-4-0/reference/app-toolkit/ui-kit/constants#header-controls) for the full list.

```tsx

type HeaderPreferences = {
  poolMiners: boolean
  miners: boolean
  poolHashrate: boolean
  hashrate: boolean
  consumption: boolean
  efficiency: boolean
}
```

## Basic usage

Changes apply instantly via `onToggle`. The callback receives the preference key and new value:

```tsx
const handleToggle = (key: keyof HeaderPreferences, value: boolean) => {
  setPreferences(prev => ({ ...prev, [key]: value }))
  api.updateHeaderPreference(key, value)  // persist immediately
}
```

<Callout type="info">
There is no **Save** button, each toggle immediately persists via your callback.
</Callout>

### Example

```tsx

function HeaderSettingsSection() {
  const [preferences, setPreferences] = useState<HeaderPreferences>({
    poolMiners: true,
    miners: true,
    poolHashrate: true,
    hashrate: false,
    consumption: true,
    efficiency: true,
  })

  const handleToggle = (key: keyof HeaderPreferences, value: boolean) => {
    setPreferences(prev => ({ ...prev, [key]: value }))
    api.updateHeaderPreference(key, value)
  }

  const handleReset = () => {
    setPreferences(DEFAULT_HEADER_PREFERENCES)
    api.resetHeaderPreferences()
  }

  return (
    <HeaderControlsSettings
      preferences={preferences}
      onToggle={handleToggle}
      onReset={handleReset}
    />
  )
}
```

## Features

### Table layout

The component renders a table with:
- **Header Item** column: label for the metric
- **Visibility Toggle** column: switch to show/hide

Items are defined by [`HEADER_ITEMS`](/v0-4-0/reference/app-toolkit/ui-kit/constants#header_items). Labels for the in-app rows (`miners`, `hashrate`) are composed from [`WEBAPP_SHORT_NAME`](/v0-4-0/reference/app-toolkit/ui-kit/constants#webapp_short_name), so they reflect the brand string the foundation kit ships with.

### Reset to default

The **Reset to Default** button calls your `onReset` callback. You define what "default" means:

```tsx

const handleReset = () => {
  setPreferences(DEFAULT_HEADER_PREFERENCES)
  api.resetHeaderPreferences()
}
```

## Loading state

When `isLoading` is `true` and no preferences are available, a spinner is shown:

```tsx
<HeaderControlsSettings
  preferences={null}
  isLoading={true}
  onToggle={handleToggle}
  onReset={handleReset}
/>
```

## Styling

The component uses the `.mdk-settings-header-controls` CSS class. Key selectors:

- `.mdk-settings-header-controls__description`: Explanatory text
- `.mdk-settings-header-controls__table`: Table container
- `.mdk-settings-header-controls__table-header`: Header row
- `.mdk-settings-header-controls__table-row`: Data rows
- `.mdk-settings-header-controls__actions`: Reset button container

## Related hooks

| Hook | Supplies |
|------|---------|
| [`useHeaderControls`](/v0-4-0/reference/app-toolkit/hooks/components#useheadercontrols) | Read/write access to the global header-controls store — toggle visibility, set sticky state, and read current values |

# Header items (/v0-4-0/ui/react/foundation/settings/header-controls/header-items)

The header bar components that render live stats and controls in the persistent app header. Visibility for each item is governed by [`HeaderControlsSettings`](/v0-4-0/ui/react/foundation/settings/header-controls) and the `HeaderPreferences` object.

For vendor-agnostic dashboard body cards see [Dashboard › Stats › Generic widgets](/v0-4-0/ui/react/foundation/dashboard/stats/generic-widgets).

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)

## Components

<ComponentTable category="Settings" subcategory="Header controls" pkg="foundation" />

---

### `HeaderStatsBar`

<ComponentDescription name="HeaderStatsBar" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `children` | Required | `ReactNode` | none | Stat box components to render left-to-right |
| `className` | Optional | `string` | none | Optional root class override |

#### Basic usage

`HeaderStatsBar` is a slot-based container. Pass the stat boxes you want to show as children — the bar inserts an angled chevron divider between each pair automatically.

```tsx
  HeaderStatsBar,
  HeaderMinersBox,
  HeaderHashrateBox,
  HeaderConsumptionBox,
  HeaderEfficiencyBox,
} from '@tetherto/mdk-react-devkit/foundation'

<HeaderStatsBar>
  <HeaderMinersBox
    total={2188}
    online={158}
    error={1}
    offline={57}
    mosTotal={216}
    poolTotal={205}
    poolOnline={201}
    poolMismatch={4}
  />
  <HeaderHashrateBox mosPhs={63.262} poolPhs={52.687} />
  <HeaderConsumptionBox valueMw={1.663} />
  <HeaderEfficiencyBox valueWthS={26.29} />
</HeaderStatsBar>
```

#### Styling

- `.mdk-header-stats-bar`: Root flex strip
- `.mdk-header-stats-bar__divider`: Angled chevron icon between children

---

### `HeaderMinersBox`

<ComponentDescription name="HeaderMinersBox" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `total` | Optional | `number` | none | Total miners on site (denominator) |
| `online` | Optional | `number` | none | Online miner count (green, also the numerator in the ratio) |
| `error` | Optional | `number` | none | Miners in warning state (amber) |
| `offline` | Optional | `number` | none | Offline miners (red) |
| `mosTotal` | Optional | `number` | none | Total miners reporting to MOS |
| `poolTotal` | Optional | `number` | none | Total miners as reported by upstream pools |
| `poolOnline` | Optional | `number` | none | Pool-side online count (green) |
| `poolMismatch` | Optional | `number` | none | Pool-side mismatch count (red) |
| `icon` | Optional | `ReactNode` | none | Override the default miner icon |
| `className` | Optional | `string` | none | Optional root class override |

All numeric values fall back to `—` when undefined.

#### Basic usage

```tsx
<HeaderMinersBox
  total={2188}
  online={158}
  error={1}
  offline={57}
  mosTotal={216}
  poolTotal={205}
  poolOnline={201}
  poolMismatch={4}
/>
```

#### Styling

- `.mdk-header-stat-box`: Shared stat cell root
- `.mdk-header-stat-box__icon`: Icon slot
- `.mdk-header-stat-box__body`: Text content area
- `.mdk-header-stat-box__row`: Each label/value row
- `.mdk-header-stat-box__success`: Green value (online)
- `.mdk-header-stat-box__warning`: Amber value (error)
- `.mdk-header-stat-box__danger`: Red value (offline / mismatch)
- `.mdk-header-stat-box__accent`: Highlighted online ratio
- `.mdk-header-stat-box__muted`: Subdued label text

---

### `HeaderHashrateBox`

<ComponentDescription name="HeaderHashrateBox" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `mosPhs` | Optional | `number` | none | MOS-side aggregate hashrate in PH/s |
| `poolPhs` | Optional | `number` | none | Pool-side aggregate hashrate in PH/s |
| `unit` | Optional | `string` | varies | Unit label — see individual component for default |
| `icon` | Optional | `ReactNode` | none | Override the default miner icon |
| `className` | Optional | `string` | none | Optional root class override |

#### Basic usage

```tsx
<HeaderHashrateBox mosPhs={63.262} poolPhs={52.687} />
```

---

### `HeaderConsumptionBox`

<ComponentDescription name="HeaderConsumptionBox" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `valueMw` | Optional | `number` | none | Current site-level power consumption in megawatts |
| `unit` | Optional | `string` | varies | Unit label — see individual component for default |
| `icon` | Optional | `ReactNode` | none | Override the default miner icon |
| `className` | Optional | `string` | none | Optional root class override |

The value is formatted to 3 decimal places and rendered in the accent (orange) colour token.

#### Basic usage

```tsx
<HeaderConsumptionBox valueMw={1.663} />
```

---

### `HeaderEfficiencyBox`

<ComponentDescription name="HeaderEfficiencyBox" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `valueWthS` | Optional | `number` | none | Efficiency in W/TH/s (`power_w / hashrate_th`) |
| `unit` | Optional | `string` | varies | Unit label — see individual component for default |
| `icon` | Optional | `ReactNode` | none | Override the default miner icon |
| `className` | Optional | `string` | none | Optional root class override |

The value is formatted to 2 decimal places.

#### Basic usage

```tsx
<HeaderEfficiencyBox valueWthS={26.29} />
```

---

### `DashboardDateRangePicker`

<ComponentDescription name="DashboardDateRangePicker" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `value` | Required | `DashboardDateRange` | none | Current range as `{ start, end }` epoch-millisecond timestamps |
| `onChange` | Required | `function` | none | Fires with the next `{ start, end }` window when the user applies a range |
| `dateFormat` | Optional | `string` | `'dd/MM/yyyy'` | Display format — defaults to `dd/MM/yyyy` |
| `disabled` | Optional | `boolean` | `false` | Disable the trigger |
| `className` | Optional | `string` | none | Optional root class override |

#### `DashboardDateRange` type

```tsx
type DashboardDateRange = {
  start: number  // epoch ms
  end: number    // epoch ms
}
```

#### Basic usage

Designed to slot directly into `useDashboardDateRange` from `@tetherto/mdk-react-adapter`:

```tsx

  const { start, end, setRange } = useDashboardDateRange()

  return (
    <DashboardDateRangePicker
      value={{ start, end }}
      onChange={({ start, end }) => setRange(start, end)}
    />
  )
}
```

#### Styling

- `.mdk-dashboard-date-range-picker__trigger`: Trigger button
- `.mdk-dashboard-date-range-picker__modal`: Calendar modal

---

### `ExportButton`

<ComponentDescription name="ExportButton" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `onExport` | Required | `function` | none | Fires with `'csv'` or `'json'` when the user picks a format |
| `formats` | Optional | `readonly ExportFormat[]` | `['csv', 'json']` | Formats to offer in the dropdown — defaults to `['csv', 'json']` |
| `label` | Optional | `string` | `'Profile menu'` | Accessible label for the trigger button — defaults to `'Profile menu'` |
| `disabled` | Optional | `boolean` | `false` | Disable the trigger |
| `className` | Optional | `string` | none | Optional root class override |

#### `ExportFormat` type

```tsx
type ExportFormat = 'csv' | 'json'
```

#### Basic usage

```tsx

<ExportButton
  onExport={(format) => {
    if (format === 'csv') downloadCsv(data)
    else downloadJson(data)
  }}
/>
```

#### Styling

- `.mdk-export-button`: Trigger button wrapper
- `.mdk-export-button__label`: Label text span
- `.mdk-export-button__chevron`: Dropdown chevron icon
- `.mdk-export-button__menu`: Dropdown menu container

---

### `ProfileMenu`

<ComponentDescription name="ProfileMenu" />

#### Import

```tsx
```

#### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `items` | Required | `ProfileMenuItem[]` | none | Items rendered in the dropdown, top-to-bottom. Defaults to a single "Sign out" item. |
| `user` | Optional | `ReactNode` | none | Optional user label rendered at the top of the dropdown (e.g. an email) |
| `icon` | Optional | `ReactNode` | none | Override the default miner icon |
| `label` | Optional | `string` | `'Profile menu'` | Accessible label for the trigger button — defaults to `'Profile menu'` |
| `className` | Optional | `string` | none | Optional root class override |

#### `ProfileMenuItem` type

```tsx
type ProfileMenuItem = {
  label: string
  onSelect: () => void
  danger?: boolean       // render as a destructive action (red)
  icon?: ReactNode       // optional leading icon
  description?: ReactNode // optional secondary line (e.g. "Current: Europe/Podgorica")
  disabled?: boolean
}
```

#### Basic usage

```tsx

<ProfileMenu
  user="admin@example.com"
  items={[
    {
      label: 'Timezone',
      description: 'Current: Europe/Podgorica',
      onSelect: () => openTimezoneDialog(),
    },
    {
      label: 'Sign out',
      danger: true,
      onSelect: () => auth.signOut(),
    },
  ]}
/>
```

#### Styling

- `.mdk-profile-menu__trigger`: Trigger button
- `.mdk-profile-menu__user`: User label at top of dropdown
- `.mdk-profile-menu__item`: Menu item row
- `.mdk-profile-menu__item--two-line`: Modifier when `description` is present
- `.mdk-profile-menu__item--danger`: Modifier for destructive items
- `.mdk-profile-menu__item-icon`: Leading icon slot
- `.mdk-profile-menu__item-body`: Label + description wrapper
- `.mdk-profile-menu__item-label`: Primary label text
- `.mdk-profile-menu__item-description`: Secondary description text

# Import/export settings (/v0-4-0/ui/react/foundation/settings/import-export)

<ComponentDescription name="ImportExportSettings" />

It manages:

1. **Export** (`onExport`): callback to generate and download settings JSON.
2. **Import** (`onImport`): callback when end user confirms import.
3. **File parsing** (`onParseFile`): validate and parse uploaded files.
4. **State indicators** (`isExporting`, `isImporting`): show loading feedback.
5. **Styling** (`className`): optional CSS class.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)

## Import

```tsx
```

## Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `onExport` | Required | `function` | none | Callback to trigger settings export |
| `onImport` | Required | `function` | none | Callback when import is confirmed |
| `onParseFile` | Optional | `function` | none | Parse and validate uploaded file |
| `isExporting` | Optional | `boolean` | `false` | Export in progress |
| `isImporting` | Optional | `boolean` | `false` | Import in progress |
| `className` | Optional | `string` | none | Additional CSS class |

## Data structure

The [`SettingsExportData`](/v0-4-0/reference/app-toolkit/ui-kit/types#settingsexportdata) type defines the structure for exported settings:

```tsx

type SettingsExportData = {
  headerControls?: HeaderPreferences
  featureFlags?: Record<string, boolean>
  timestamp?: string
}
```

## Callbacks

### `onExport`

Called when end user clicks **Export JSON**. Generate settings and trigger download:

```tsx
const handleExport = async () => {
  const data = await api.getSettingsExport()
  downloadJson(data, 'settings-backup.json')
}
```

### `onImport`

Called after end user confirms import. Receives parsed `SettingsExportData`:

```tsx
const handleImport = async (data: SettingsExportData) => {
  await api.importSettings(data)
}
```

### `onParseFile`

Validates and parses the uploaded file. Must return a `Promise<SettingsExportData>`:

```tsx
const parseFile = async (file: File): Promise<SettingsExportData> => {
  const text = await file.text()
  return JSON.parse(text)
}
```

## Basic usage

```tsx

function ImportExportSection() {
  const [isExporting, setIsExporting] = useState(false)
  const [isImporting, setIsImporting] = useState(false)

  const handleExport = async () => {
    setIsExporting(true)
    const data = await api.getSettingsExport()
    downloadJson(data, 'settings-backup.json')
    setIsExporting(false)
  }

  const handleImport = async (data: SettingsExportData) => {
    setIsImporting(true)
    await api.importSettings(data)
    setIsImporting(false)
  }

  const parseFile = async (file: File): Promise<SettingsExportData> => {
    const text = await file.text()
    return JSON.parse(text)
  }

  return (
    <ImportExportSettings
      onExport={handleExport}
      onImport={handleImport}
      onParseFile={parseFile}
      isExporting={isExporting}
      isImporting={isImporting}
    />
  )
}
```

## Features

### Export

Clicking **Export JSON** triggers the `onExport` callback. The developer is responsible for:

- Fetching current settings
- Converting to JSON
- Triggering file download

### Import flow

1. End user clicks **Import JSON**.
2. Modal opens with file upload area.
3. End user selects a `.json` or `.csv` file.
4. `onParseFile` validates and parses the file.
5. Confirmation modal shows what will be imported.
6. End user confirms and `onImport` is called with parsed data.

### Confirmation modal

Before importing, end users see a confirmation dialog listing:

- Header Controls (if present)
- Feature Flags (if present)
- Export timestamp (if available)

## Accepted file formats

The file input accepts:

- `.json`: JSON format
- `.csv`: CSV format (requires custom `onParseFile` handling)

## Warning message

The component displays a warning:

<Callout type="warning">
Importing settings will overwrite your current configuration. Make sure to export your current settings before importing.
</Callout>

## Styling

The component uses the `.mdk-settings-import-export` CSS class. Key selectors:

- `.mdk-settings-import-export__description`: explanatory text
- `.mdk-settings-import-export__actions`: button container
- `.mdk-settings-import-export__warning`: warning message
- `.mdk-settings-import-export__upload-area`: file drop zone in modal

# User management modals (/v0-4-0/ui/react/foundation/settings/user-management)

These modal components handle user creation, editing, and confirmation dialogs.

While they are designed to be used within
[`RBACControlSettings`](/v0-4-0/ui/react/foundation/settings/access-control) the three modal components can stand alone:

1. **[`AddUserModal`](#addusermodal)**: create new users with form validation.
2. **[`ManageUserModal`](#manageusermodal)**: edit users and view effective permissions.
3. **[`ChangeConfirmationModal`](#changeconfirmationmodal)**: confirm destructive or important actions.

## Prerequisites

- Complete the [@tetherto/mdk-react-devkit installation and add the dependency](/v0-4-0/ui/react/foundation#prerequisites)

## `AddUserModal`

<ComponentDescription name="AddUserModal" />

## Import

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `open` | Required | `boolean` | none | Whether the modal is visible |
| `roles` | Required | `array` | none | Available roles for selection |
| `onSubmit` | Required | `function` | none | Called with form data on submit |
| `onClose` | Required | `function` | none | Called when modal should close |
| `isSubmitting` | Optional | `boolean` | `false` | Shows loading state on submit button |

### `RoleOption` structure

Full type: [`RoleOption` in Types reference](/v0-4-0/reference/app-toolkit/ui-kit/types#roleoption).

| Property | Type | Description |
|----------|------|-------------|
| `value` | `string` | Role identifier |
| `label` | `string` | Display name |

### Callbacks

The `onSubmit` callback receives the validated form data:

```tsx
const handleSubmit = async (data) => {
  // data: { name: string, email: string, role: string }
  await api.createUser(data)  // persist to backend
  refetchUsers()               // refresh list
}
```

The modal resets its form after successful submission.

#### Example

```tsx
function UserActions() {
  const [isOpen, setIsOpen] = useState(false)

  const handleSubmit = async (data) => {
    await api.createUser(data)
    setIsOpen(false)
  }

  return (
    <>
      <Button onClick={() => setIsOpen(true)}>Add User</Button>
      <AddUserModal
        open={isOpen}
        onClose={() => setIsOpen(false)}
        roles={[
          { value: 'admin', label: 'Admin' },
          { value: 'operator', label: 'Operator' },
        ]}
        onSubmit={handleSubmit}
      />
    </>
  )
}
```

### Form fields

- **Name**: required, trimmed
- **Email**: required, must be valid email
- **Role**: required, select from available roles

Validation uses Zod schema with `react-hook-form`. The modal displays **Add User** and **Cancel** buttons.

## `ManageUserModal`

<ComponentDescription name="ManageUserModal" />

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `open` | Required | `boolean` | none | Whether the modal is visible |
| `user` | Required | [`SettingsUser`](/v0-4-0/reference/app-toolkit/ui-kit/types#settingsuser) | none | User being edited |
| `roles` | Required | `array` | none | Available roles |
| `rolePermissions` | Required | `object` | none | Permissions by role |
| `permissionLabels` | Required | `object` | none | Permission display names |
| `onSubmit` | Required | `function` | none | Called with updated data on submit |
| `onClose` | Required | `function` | none | Called when modal should close |
| `isSubmitting` | Optional | `boolean` | `false` | Shows loading state on submit button |

### Data structures

The `rolePermissions` object maps role IDs to their permissions:

```tsx
const rolePermissions = {
  admin: { users: 'rw', settings: 'rw', reports: 'r' },
  operator: { users: 'r', settings: 'none', reports: 'r' }
}
```

The `permissionLabels` object provides display names:

```tsx
const permissionLabels = {
  users: 'User Management',
  settings: 'System Settings',
  reports: 'Reports'
}
```

### Callbacks

The `onSubmit` callback receives the updated user data:

```tsx
const handleSubmit = async (data) => {
  // data: { id: string, name: string, email: string, role: string }
  await api.updateUser(data)  // persist to backend
  refetchUsers()               // refresh list
}
```

### Example

```tsx
function UserRow({ user }) {
  const [isEditing, setIsEditing] = useState(false)

  const handleSubmit = async (data) => {
    await api.updateUser(data)
    setIsEditing(false)
  }

  return (
    <>
      <Button onClick={() => setIsEditing(true)}>Manage</Button>
      <ManageUserModal
        open={isEditing}
        onClose={() => setIsEditing(false)}
        user={user}
        roles={roles}
        rolePermissions={rolePermissions}
        permissionLabels={permissionLabels}
        onSubmit={handleSubmit}
      />
    </>
  )
}
```

### Sections

The modal is divided into three sections:

1. **User Information**: edit name and email.
2. **Assigned Role**: select role from dropdown.
3. **Effective Permissions**: read-only table showing what the selected role can do.

### Permission icons

Permissions display icons based on level:

- `rw`: Read/Write (full access)
- `r`: Read (view only)
- `none`: No access

The modal displays **Save Changes** and **Cancel** buttons.

## `ChangeConfirmationModal`

<ComponentDescription name="ChangeConfirmationModal" />

```tsx
```

### Props

| Prop | Status | Type | Default | Description |
|------|--------|------|---------|-------------|
| `open` | Required | `boolean` | none | Whether the modal is visible |
| `title` | Required | `string` | none | Modal title |
| `children` | Required | `ReactNode` | none | Modal body content |
| `onConfirm` | Required | `function` | none | Called when end user confirms |
| `onClose` | Required | `function` | none | Called when end user cancels or closes |
| `confirmText` | Optional | `string` | `'Confirm'` | Text for confirm button |
| `destructive` | Optional | `boolean` | `false` | Use danger styling (red button) |

### Example

```tsx
function DeleteButton({ user }) {
  const [showConfirm, setShowConfirm] = useState(false)

  const handleDelete = async () => {
    await api.deleteUser(user.id)
    setShowConfirm(false)
  }

  return (
    <>
      <Button onClick={() => setShowConfirm(true)}>Delete</Button>
      <ChangeConfirmationModal
        open={showConfirm}
        title={`Delete ${user.email}?`}
        onClose={() => setShowConfirm(false)}
        onConfirm={handleDelete}
        confirmText="Delete"
        destructive
      >
        Are you sure you want to delete this user? This action is permanent
        and cannot be undone.
      </ChangeConfirmationModal>
    </>
  )
}
```

### Variants

#### Standard confirmation

```tsx
<ChangeConfirmationModal
  title="Save Changes?"
  confirmText="Save"
  destructive={false}
>
  Your changes will be applied immediately.
</ChangeConfirmationModal>
```

#### Destructive confirmation

```tsx
<ChangeConfirmationModal
  title="Delete Item?"
  confirmText="Delete"
  destructive
>
  This action cannot be undone.
</ChangeConfirmationModal>
```

The modal displays a **Cancel** button alongside the configurable confirm button.

## Dependencies

All user management modals depend on `@tetherto/mdk-react-devkit/core` components:

- `Dialog`, `DialogContent`, `DialogFooter`
- `Button`
- `Form`, `FormInput`, `FormSelect`

They also use:

- `react-hook-form` for form state
- `zod` for validation
- `@hookform/resolvers` for Zod integration

# Get started with React (/v0-4-0/ui/react/get-started)

## TL;DR 

- **One provider, zero Redux**: install and wrap your app in `<MdkProvider>` so connected foundation components and adapter hooks share the 
same stores and API client
- Presentational [`/core`](/v0-4-0/ui/react/core) and [`/foundation`](/v0-4-0/ui/react/foundation) imports can work without the provider
- Anything that reads app state needs the provider

## Choose your path

<Cards>
  <Card
    icon={<Rocket />}
    title={<span style={cardTitle}>Quickstart</span>}
    href="/v0-4-0/ui/react/quickstart"
    description={
      <>
        <span style={cardProse}>
          Install all three packages, wrap in MdkProvider, wire stores and theming
        </span>
        <span style={cardTime}>⏱️ &lt;3 min</span>
      </>
    }
  />
  <Card
    icon={<Compass />}
    title={<span style={cardTitle}>Explore the demo</span>}
    href="/v0-4-0/ui/react/explore-the-demo"
    description={
      <>
        <span style={cardProse}>
          Single copy/paste to clone the monorepo and browse the full demo app in your browser
        </span>
        <span style={cardTime}>⏱️ &lt;1 min</span>
      </>
    }
  />
  <Card
    icon={<BookOpen />}
    title={<span style={cardTitle}>Tutorial</span>}
    href="/v0-4-0/tutorials/ui/react/tutorial"
    description={
      <>
        <span style={cardProse}>
          Step-by-step app scaffold with MdkProvider, adapter hooks, and foundation components
        </span>
        <span style={cardTime}>⏱️ &lt;5 min</span>
      </>
    }
  />
  <Card
    icon={<Bot />}
    title={<span style={cardTitle}>Build dashboards with your AI agent</span>}
    href="/v0-4-0/agents"
    description={
      <span style={cardProse}>
        Wire Cursor or Claude to MDK with the UI CLI, then build from plain-language prompts
      </span>
    }
  />
</Cards>

## Prerequisites

- **Node.js** >=24
- **npm** >=11
- **React** 19+ and **react-dom** 19+

{/* npm packages are not yet published to the registry; clone the MDK UI monorepo and use workspace dependencies until publish. */}

## About the React stack

The React UI Kit is published as **three workspace packages** in the [MDK monorepo](https://github.com/tetherto/mdk): 

- **[`@tetherto/mdk-ui-core`](/v0-4-0/reference/app-toolkit/ui-core)** — headless state (Zustand vanilla stores), a TanStack `QueryClient` factory, telemetry primitives, and the command state machine. No React.
- **`@tetherto/mdk-react-adapter`** — React bindings: `<MdkProvider>`, store hooks (`useAuth`, `useDevices`, `useNotifications`, `useTimezone`, `useActions`) and re-exports of `useQuery` / `useMutation`.
- **`@tetherto/mdk-react-devkit`** — the React UI library: `./core` primitives and `./foundation` mining-domain components, hooks, and styles.

`@tetherto/mdk-ui-core` is framework-agnostic; the adapter and devkit are React-specific.
Add all three to your app’s `package.json`, then wrap the tree once in **`<MdkProvider>`** from `@tetherto/mdk-react-adapter`. 

<Accordions>
  <Accordion title="Explain it like I'm 12">

**`@tetherto/mdk-ui-core`** is the brain in the back room. It remembers who is logged in, which miners are selected, and what timezone the operator uses — but it has no buttons and no React. Other code (including non-React utilities) can read and update it with `getState()` / `setState()`.

**`@tetherto/mdk-react-adapter`** plugs that brain into React. `<MdkProvider>` turns it on for your whole app. Hooks like `useAuth` and `useDevices` let components listen to one slice of state and re-render when that slice changes.

**[`/core`](/v0-4-0/ui/react/core)** (`import … from '@tetherto/mdk-react-devkit/core'`) is the generic UI toolkit. Buttons, inputs, dialogs, tabs, charts, sidebars, toasts, tables. Nothing in it knows what a miner is. It owns colours, fonts, spacing, and the shared styling recipes.

**[`/foundation`](/v0-4-0/ui/react/foundation)** (`import … from '@tetherto/mdk-react-devkit/foundation'`) is built on top of core. It knows about miners, containers, pools, hashrate, alarms, operators, permissions, and settings dashboards. Domain hooks and connected components expect `<MdkProvider>` to be in place.

  </Accordion>
</Accordions>

## Next steps

- Explore the [Core primitives](/v0-4-0/ui/react/core) in `@tetherto/mdk-react-devkit/core`: buttons, forms, charts, and themes
- Explore the [Foundation components](/v0-4-0/ui/react/foundation) in `@tetherto/mdk-react-devkit/foundation`: mining-domain components and hooks
- See the [UI Kit overview](/v0-4-0/ui) for how the headless layer fits other frameworks
- Browse the [Hooks](/v0-4-0/reference/app-toolkit/hooks) reference — [state](/v0-4-0/reference/app-toolkit/hooks/state), [component](/v0-4-0/reference/app-toolkit/hooks/components), and [utility](/v0-4-0/reference/app-toolkit/hooks/utilities) hooks

# Quickstart (/v0-4-0/ui/react/quickstart)

This page walks through the minimum integration of the MDK UI toolkit into a React application.

## Prerequisites

- **Node.js** >=24
- **npm** >=11
- **React** 19+ and **react-dom** 19+

{/* npm packages are not yet published to the registry; clone the MDK UI monorepo and use workspace dependencies until publish. */}

## Install

<Tabs>
  <Tab value="workspace" label="Monorepo workspace" default>

```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-core": "*"
  }
}
```

<Callout type="info">
[Wrap your app](/v0-4-0/ui/react/quickstart) in `<MdkProvider>` from `@tetherto/mdk-react-adapter` when using connected foundation components 
or adapter store hooks.
</Callout>

  </Tab>
  <Tab value="npm" label="🚧 Registry install">

> **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-core
```

  </Tab>
</Tabs>

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(
  <MdkProvider apiBaseUrl="https://app-node.example.com">
    <App />
  </MdkProvider>,
)
```

## Use the adapter hooks inside React

Each hook subscribes the component to the relevant Zustand store and re-renders only when the selected slice changes.

```tsx

const Toolbar = () => {
  const { permissions } = useAuth()
  const { selectedDevices } = useDevices()
  const { setAddPendingSubmissionAction } = useActions()
  // ...
}
```

## Or read / write stores directly outside React

The vanilla stores expose `getState()` / `setState()` so utility code, side-effect handlers, and tests can interact with the same source of truth.

```tsx

// Outside React (utilities, sagas, etc.) you can read/write directly:
devicesStore.getState().setSelectedDevices([])
actionsStore.getState().setAddPendingSubmissionAction({ /* … */ })
```

## Theme via design tokens and @layer mdk

The compiled stylesheet declares `@layer base`, `mdk`, `app` — so unlayered or `@layer app` styles in your application always win against devkit 
component styles. MDK ships with `--mdk-color-primary: #f7931a`; override tokens in `:root` only when reskinning.

```css
/* app.css — imported AFTER @tetherto/mdk-react-devkit/styles.css */
:root {
  --mdk-color-primary: #f7931a;
  --mdk-radius: 6px;
}

@layer app {
  .mdk-button--variant-primary { letter-spacing: 0.04em; }
}
```

See [Theme](/v0-4-0/ui/react/core/theme) for design tokens and `@layer` override rules.

## Next steps

- [Tutorial](/v0-4-0/tutorials/ui/react/tutorial) — full integration walkthrough
- [Hooks](/v0-4-0/reference/app-toolkit/hooks) — [state hooks](/v0-4-0/reference/app-toolkit/hooks/state), [component hooks](/v0-4-0/reference/app-toolkit/hooks/components), and [utility hooks](/v0-4-0/reference/app-toolkit/hooks/utilities)
- [Explore the demo](/v0-4-0/ui/react/explore-the-demo) — run the demo browser without adding MDK to your own app yet
- [Core](/v0-4-0/ui/react/core) — primitives reference (`@tetherto/mdk-react-devkit/core`)
- [Foundation](/v0-4-0/ui/react/foundation) — mining-domain components (`@tetherto/mdk-react-devkit/foundation`)
