From 548051899ed2ed1d8bfed7912a70c41f2c398d05 Mon Sep 17 00:00:00 2001 From: JC Date: Mon, 10 Aug 2026 17:41:17 +0000 Subject: [PATCH] docs: add Mine Map v1 design --- docs/design/MINE_MAP.md | 792 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 792 insertions(+) create mode 100644 docs/design/MINE_MAP.md diff --git a/docs/design/MINE_MAP.md b/docs/design/MINE_MAP.md new file mode 100644 index 0000000..2e97f90 --- /dev/null +++ b/docs/design/MINE_MAP.md @@ -0,0 +1,792 @@ +# MinerOps Mine Map v1 + +## Status + +This document is the implementation-ready design baseline for Mine Map v1. It +defines the smallest useful spatial operations view that can be built from the +current MinerOps location, inventory, telemetry, and repair foundations. It +does not claim that the described v1 interface is implemented. + +The current application exposes a legacy Location Map at `/locations`, a Mine +Map placeholder at `/monitoring/map`, and a bulk location response at +`GET /api/location/map`. Mine Map v1 replaces the placeholder with a read-only, +permission-aware operational view. Existing location-management workflows +remain available separately and are not redesigned by this specification. + +## Purpose + +Mine Map v1 answers one primary question: + +> What is happening physically in the mine right now, and where do I need to +> look? + +It must let a technician or manager scan a physical room, identify explicit +runtime and work states, locate affected miners, understand empty capacity, and +inspect one miner without losing spatial context. It is an operational tool, +not a geographic GIS map and not a decorative dashboard. + +## Current Data Model + +### Physical Hierarchy + +The implemented hierarchy is: + +```text +Site (UUID) + -> Room (UUID, belongs to Site) + -> Rack (text label on a location position) + -> Shelf (text label and optional numeric index) + -> Position (UUID and numeric index) + -> Miner (UUID, optional occupant) +``` + +`sites` and `rooms` are normalized entities. A room belongs to one site. +`location_positions` stores `room_id`, denormalized `room_name`, `rack_name`, +`shelf_name`, optional `shelf_index`, `position_index`, and optional +`position_label`. Rack and shelf are not independent entities and do not have +durable UUIDs. Mine Map must therefore treat rack and shelf values as ordered +labels within a room, not as globally addressable assets. + +A position may reference one miner. `miner_location_states` separately stores +the miner's current location state and optional position reference. The miner's +durable identity remains `miners.id`; neither its position, IP address, nor MAC +address is asset identity. + +### Position Vocabulary + +The current code uses these position kinds: + +- `miner_slot` +- `switch` +- `blocked` + +The current code uses these position statuses: + +- `available` +- `occupied` +- `switch` +- `blocked` +- `conflict` + +These values are used by the active implementation but are not constrained by +the database. V1 must render unknown values as **Unknown position state** and +must not silently treat them as empty or occupied. A later schema task should +constrain the vocabulary after legacy-data preflight. + +The current miner location-state vocabulary is: + +- `shelved` +- `waiting_placement` +- `storage` +- `repair` +- `decommissioned` + +These values are also not database-constrained. They describe location or +placement state, not miner lifecycle and not authoritative repair status. + +### Lifecycle And Repair State + +Miner lifecycle is independently constrained to: + +- `active` +- `spare` +- `retired` +- `in_repair` + +The modular Repairs domain uses `repairs` and immutable `repair_events` as the +authoritative repair workflow. Active repair statuses are `OPEN`, +`IN_PROGRESS`, `WAITING_FOR_PARTS`, and `READY_FOR_TEST`. `COMPLETED` and +`CANCELLED` are not active repairs. + +The legacy location API currently exposes `miner_location_states.asset_state` +but does not expose lifecycle or the authoritative active repair. Mine Map v1 +must not equate legacy `asset_state = repair` with a modular repair record. + +## User Outcomes + +V1 must support these outcomes without requiring location mutation: + +- Select a site and room and see its known physical layout. +- Scan racks, shelves, and positions in their stored order. +- Distinguish occupied, explicitly empty, blocked, switch, conflict, and + incomplete positions. +- Identify miners explicitly reported offline or hash-unstable. +- Identify miners in the authoritative repair lifecycle or an active repair. +- Distinguish active, spare, retired, and in-repair assets. +- See latest available hashrate, power, and temperature values with timestamps. +- Inspect a miner and open its existing detail page. +- Return to the same room, rack, layer, filters, selection, and scroll context. + +## Page Composition + +Mine Map uses the current authenticated application shell at +`/monitoring/map`. The page contains four functional regions. + +### Context Bar + +The context bar contains: + +- site selector; +- room selector; +- layer selector, defaulting to **Operational Status**; +- search; +- filters; +- data freshness summary; and +- a link to the existing location-management page when the user has + `locations.manage`. + +The selected site limits the room selector. The selected room is required for +the primary rack layout. An optional all-room summary may show room counts, but +v1 must not render every tile from every room by default at large sites. + +### Spatial Layout + +The main region renders the selected room as ordered rack groups. Each rack +contains shelf rows, and each row contains positions ordered by +`position_index`. The layout uses ordinary semantic DOM elements and CSS grid. +Canvas, WebGL, pan physics, and a geographic basemap are not required. + +The visual order follows stored room `sort_order`, shelf `shelf_index` where +present, and position index. Rack ordering is label-based because no rack sort +field exists. The UI must not infer missing racks, shelves, or positions from +gaps in names or numbers. + +### Legend And Summary + +The legend is always visible for the active layer and pairs color with text and +an icon or pattern. A compact summary reports only counts derivable from the +loaded response, such as occupied positions, empty positions, conflicts, +explicit offline miners, in-repair miners, and unknown-status miners. + +Counts must identify their scope and freshness. They must not claim site-wide +coverage when only one room is loaded or when data is partial. + +### Miner Inspector + +Selecting an occupied miner opens a side panel on desktop. On narrow screens it +opens a full-height sheet with an obvious close control. The map remains the +underlying context and selection is visible on the selected tile. + +Closing the inspector restores focus to the tile that opened it. Opening the +full miner detail view uses the existing durable route: + +```text +/miners/{miner_id}/detail +``` + +## Position And Miner Rendering + +### Occupied Positions + +An occupied miner tile shows, in priority order: + +- position label or `Position {position_index}`; +- Asset ID when available, otherwise current MAC, otherwise **Unidentified + miner**; +- operational condition text and icon; +- the active layer's value; and +- observation age or **Never observed**. + +The position remains the visible frame; filtering and status styling must not +make its physical address ambiguous. + +### Empty And Non-Miner Positions + +Empty positions are operationally meaningful and remain visible. + +| Condition | V1 presentation | +| --- | --- | +| `miner_slot`, no miner, no expected MAC, `available` | **Empty position** | +| `miner_slot`, expected MAC, no miner | **Expected miner not resolved**; not empty | +| `blocked` kind or status | **Blocked position** with reason when available | +| `switch` kind or status | **Switch/equipment position** with equipment label | +| `conflict` or `conflict_reason` | **Location conflict**; never shown as empty | +| Unknown kind or status | **Unknown position state** | +| Filtered occupied miner | Tile remains in place but is visually deemphasized | +| Missing response data | **Unavailable**, distinct from an empty position | + +An absent position record is not an empty position. V1 must not synthesize a +tile merely because a numeric position is missing from a shelf sequence. + +### Unracked And Operationally Removed Miners + +Miners returned in `virtual_miners` appear outside the rack canvas in an +**Unplaced and operational areas** section grouped by the stored location +state. V1 maps the stored values to plain labels without inventing workflow: + +- `waiting_placement`: Waiting for placement +- `storage`: Storage +- `repair`: Legacy repair location state +- `decommissioned`: Decommissioned location state + +Lifecycle is shown separately. A spare miner is not automatically in storage, +and a retired miner is not automatically decommissioned. Where lifecycle and +location state disagree, both are displayed and the discrepancy is not hidden. + +### Repair Miners + +A miner with `lifecycle_status = in_repair` or an authoritative active repair is +visually unmistakable through text, icon, and a repair treatment independent of +the selected data layer. + +V1 distinguishes: + +- in repair while still assigned to a physical position; +- in repair with no rack position; +- repair complete with lifecycle `active` or `spare`, awaiting placement; and +- legacy `asset_state = repair` without an authoritative active repair. + +The last case is labelled **Legacy repair location state** rather than being +presented as a confirmed active repair. + +## Operational Status Model + +Operational Status is the default layer. It composes current facts with this +display precedence: + +1. Location conflict, blocked position, switch, or empty position. +2. Lifecycle `retired`. +3. Active repair or lifecycle `in_repair`. +4. Lifecycle `spare`. +5. Runtime status explicitly equal to `offline`. +6. `hash_stable = false`, labelled **Hash unstable**. +7. Runtime status explicitly equal to `online`. +8. Unknown or unrecognized runtime status. + +Lifecycle and repair treatments do not erase runtime facts. For example, an +in-repair miner may also show that its last runtime observation was online. + +`miners.status` is free text and not database-constrained. V1 must not infer +that arbitrary values containing `error`, `warn`, or `fault` mean a particular +severity. Unknown values may be displayed as secondary raw context after safe +formatting. + +### Freshness + +`last_seen_at` and latest telemetry `observed_at` are available, but MinerOps has +no single authoritative staleness policy. Existing legacy screens use +inconsistent thresholds, including 10 and 30 minutes. Therefore: + +- v1 always displays observation time or age; +- missing timestamps are **Never observed** or **Freshness unknown**; +- explicit runtime `offline` may be shown as Offline; +- v1 does not label a miner Stale based on age until a centralized policy is + approved and supplied by the backend; and +- late or failed refreshes keep the last successful values visible with a + prominent **Update failed** or **Data may be stale** state. + +### Conditions Requiring Policy + +The following labels require an approved backend policy and are not inferred by +v1: + +- stale based on observation age; +- down based on missing hashrate or elapsed time; +- hot or overheating based on temperature; +- degraded based on hashrate, rejection rate, power, or temperature; and +- low hashrate relative to model expectation. + +The legacy Location Map and dashboard endpoints contain hard-coded thresholds, +but they are not a consistent policy contract. V1 may display raw measurements +and the explicit `hash_stable` boolean without adopting those thresholds. + +## Layer Model + +The layer selector is extensible, but the first implementation is not required +to enable every layer. + +| Layer | V1 status | Current data | Required behavior | +| --- | --- | --- | --- | +| Operational Status | Required, default | Position kind/status, runtime status, lifecycle after API addition, `hash_stable`, timestamps | Uses the factual precedence above; no invented thresholds | +| Repair State | Required presentation, optional standalone layer | Lifecycle and modular repair after API addition; legacy location state exists now | Repair treatment remains visible on every layer | +| Temperature | Optional v1 value layer | Latest chip maximum and average are already in the map response; environment temperature exists in snapshots but is omitted | Show numeric value and timestamp; no hot classification without policy | +| Hashrate | Optional v1 value layer | Latest average hashrate in TH/s is already in the map response | Show value and missing-data state; no low-hashrate threshold | +| Power | Optional v1 value layer | Latest power in watts is already in the map response | Show value and missing-data state; no density or efficiency inference | +| Thermal heat map | Future | Point temperatures exist for some miners | Requires approved thresholds, aggregation semantics, coverage disclosure, and legend | + +The frontend implements layers through a shared tile contract rather than +separate maps. Adding a future layer changes tile content and legend, not the +physical hierarchy or selection model. + +## Filtering And Search + +V1 filters are allowlisted and based on returned facts: + +- explicit runtime status; +- lifecycle; +- model or miner type; +- authoritative active repair presence; +- location state; +- position kind/status; and +- presence or absence of current telemetry. + +Search supports Asset ID, miner serial, current MAC, current IP, model, room, +rack, shelf, and position label where those fields are available. Asset ID and +lifecycle require the map response addition described below. + +Filtering preserves physical context. Positions remain in the layout; +nonmatching occupied tiles are deemphasized and marked as filtered. A separate +**Show only matches** density option may collapse nonmatching rack groups, but +it is off by default and clearly indicates that context is hidden. Empty, +blocked, switch, and conflict positions remain visible unless explicitly +filtered by position type. + +No-results differs from load failure and from an empty room: + +- **No positions configured**: the selected room has no position records. +- **No miners match these filters**: layout remains visible, with miners + deemphasized. +- **Map unavailable**: the request failed or returned an invalid response. + +## Miner Inspector Fields + +Where authorized and available, the inspector shows: + +- Asset ID; +- model or miner type; +- miner serial number; +- current MAC and IP address; +- full site, room, rack, shelf, and position path; +- runtime status and last seen time; +- lifecycle; +- active repair status, reason, and repair UUID; +- latest hashrate in TH/s; +- latest power in watts; +- latest chip maximum and average temperature; +- telemetry observation time; and +- a link to full miner detail. + +Null values render as **Unavailable**. Numeric zero is displayed as zero, not as +missing. The panel must not fetch one API resource per visible tile. A single +detail request for the selected miner is acceptable when it supplies fields not +needed by the map response. + +## Context Preservation + +Shareable state is URL-backed. The initial query contract is: + +```text +/monitoring/map?site_id={uuid}&room_id={uuid}&rack={label}&layer=status&miner_id={uuid} +``` + +Optional repeated or encoded parameters hold allowlisted filters and search. +Rack is a label because racks do not have UUIDs. Unknown, unauthorized, or +malformed parameters are ignored with a nonblocking notice; they never broaden +data access. + +The URL preserves: + +- selected site and room; +- optional rack focus; +- layer; +- filters and search; and +- selected miner. + +History state or session state preserves room scroll position, focused tile, +and any future map viewport that is too transient for a URL. Selecting or +closing a miner updates history so browser Back and Forward restore predictable +selection. Refresh and bookmarked links reconstruct the same shareable view. + +When full miner detail opens in the same tab, the return URL contains the map +query state. Returning restores the prior scroll position when it remains +valid. A new tab may be offered as a normal browser link, not forced. + +## Responsive Behavior + +### Desktop + +- Context controls remain visible above the map. +- The room layout occupies the main content width. +- The inspector uses a fixed-width side panel without resizing rack tiles. +- Horizontal scrolling is contained within an individual wide rack or map + viewport, not the whole page. + +### Tablet And Phone + +- Site, room, layer, and filter controls stack without hiding the active scope. +- One rack is shown at a time when the full room cannot remain legible. +- Shelf rows retain physical position order and allow contained horizontal + scrolling. +- The inspector becomes a full-screen sheet. +- Closing the sheet restores the selected tile and scroll location. +- Touch targets meet the shared UX guide sizing rules. + +The mobile view does not turn positions into an unrelated flat miner list. Rack +and shelf context remains visible. + +## Accessibility + +Mine Map v1 must: + +- communicate every state with text and an icon or pattern, not color alone; +- provide one page heading and semantic site, room, rack, shelf, and position + labels; +- expose each rack as a labelled region and each shelf as a labelled group; +- expose miner tiles as buttons when they open the inspector and noninteractive + elements when they do not; +- include an accessible name containing position, miner identity, condition, + and freshness; +- support logical keyboard movement through positions in stored spatial order; +- preserve visible focus and return focus after closing the inspector; +- provide keyboard-operable filters, layer selection, scrolling, and zoom or + density controls; +- avoid hover-only values; +- support browser zoom without clipping labels or controls; and +- avoid required motion and respect reduced-motion preferences if motion is + introduced later. + +A screen-reader summary provides counts and a structured rack/shelf/position +list. It describes empty and blocked positions rather than omitting them. + +## Loading, Failure, And Partial Data + +The map has explicit states: + +- **Loading**: layout skeletons preserve the eventual page structure. +- **Ready**: layout and response scope are known. +- **Empty room**: valid response with no configured positions. +- **No configured sites or rooms**: directs authorized users to existing + location management without implying a load failure. +- **Denied**: permission failure without leaking counts or miner previews. +- **Error**: request failed or response shape is invalid; remains visible until + retry or navigation. +- **Partial data**: physical layout is usable, but one or more miner fields or + telemetry observations are unavailable. +- **Refresh failed**: last successful map remains visible, clearly marked with + the failure time. + +The frontend validates the response envelope and required position identifiers. +A malformed response is an error, not an empty room. + +## Performance And Refresh + +V1 is designed for hundreds or thousands of positions without prematurely +adopting canvas or WebGL. + +- Fetch a map-shaped bulk response; never issue one request per position. +- Load one selected site and room where backend filtering is available. +- Render the visible room first and defer secondary summaries. +- Build rack and shelf fragments off-DOM, then attach them in batches. +- Update changed tile content without replacing the full page or shell. +- Keep layout refresh separate from telemetry refresh because positions change + less frequently than observations. +- Do not overlap refresh requests; cancel or ignore superseded responses. +- Pause automatic telemetry refresh while the page is hidden. +- Base any polling interval on collector cadence or backend guidance; this + document does not invent an interval. +- Preserve last successful values during transient failure and expose their + observation timestamps. +- Introduce list virtualization only after measurement shows the selected-room + DOM is too large; accessibility and keyboard order must be preserved. + +The current bulk location query selects the latest snapshot per miner and is a +useful foundation. Before production use at large scale, its query plan and +indexes must be measured with representative site data. + +## Current API Assessment + +### `GET /api/location/map` + +Permission: `locations.view`. + +This is the primary existing map-shaped response and avoids N+1 tile requests. +It returns: + +- `data.positions`: position UUID, room/rack/shelf labels, shelf and position + indexes, position label/kind/status, expected MAC, equipment metadata, + conflict information, miner UUID, current MAC/IP, runtime status, hostname, + serials, model/type, firmware, last seen, legacy location state, and latest + hashrate, power, chip temperatures, hash stability, rejection percentage, and + observation time; +- `data.virtual_miners`: miners in waiting-placement, storage, legacy repair, + or decommissioned location states; and +- counts in `meta`. + +Limitations: + +- no site ID or site name; +- no room UUID in each position; +- room ordering joins positions to rooms by case-insensitive room name, which is + ambiguous when different sites use the same room name; +- no Asset ID; +- no lifecycle status; +- no authoritative active repair UUID/status/reason; +- no tags; +- no environment temperature; +- no generated-at or response freshness metadata; +- no server-side site/room filter; and +- integrated `main.py` implementation with no typed response model. + +The endpoint is already efficient in shape. V1 should preserve its path for +compatibility and implement an additive, typed map read model through the +required Router -> Service -> Repository -> PostgreSQL boundaries. It should +accept allowlisted `site_id` and `room_id` filters and add the missing identity, +lifecycle, site, and authoritative repair fields. A second competing map +endpoint is not required solely for v1. + +### `GET /api/sites` + +Permission: `locations.view`. + +Returns site UUID, name, description, and timestamps. It supplies the site +selector. It does not include room or map counts. + +### `GET /api/rooms` + +Permission: `locations.view`. + +Returns room UUID, site UUID/name, room name/description, sort order, and +timestamps. It supplies site-scoped room selection. + +### `GET /api/location/rooms-order` + +Permission: `locations.view`. + +Returns room UUID/name/order and position, miner-slot, switch, blocked, and +assigned-miner counts. Its location join is name-based and not site-scoped, so +it cannot be the sole source for a correct multi-site selector. + +### `GET /api/inventory/miners` + +Permission: `miners.view`. + +Returns durable miner UUID, Asset ID, current MAC/IP, model, lifecycle, runtime +status, site/room UUIDs, tags, and timestamps. Search covers Asset ID, MAC, IP, +model, miner serial, control-board serial, PSU serial, and notes. The collection +is bounded to 500 records per request and has no physical position or telemetry. + +It can validate the missing identity fields during development, but Mine Map +must not join paginated inventory calls in the browser for every room. The map +read model should include the small identity and lifecycle subset it needs. + +### `GET /api/inventory/miners/{miner_id}` + +Permission: `miners.view`. + +Adds hostname, miner serial, control-board serial, PSU serial, firmware, notes, +and first-seen time. One call after selection is acceptable when the inspector +needs these fields, but it is not used to populate tiles. + +### `GET /api/miners/{miner_id}/status-snapshots` + +Authentication is required by the route, but the current legacy endpoint does +not enforce `miners.view`. Mine Map must not treat authentication alone as +sufficient authorization. Any v1 use of this supplemental endpoint requires a +backend permission correction to `miners.view` first. + +Returns up to 100 recent snapshots with hashrate, power, efficiency, +environment and chip temperatures, fans, uptime, stability, rejection rate, +performance mode, timestamps, and raw JSON. It is too heavy for every tile and +must not be called per visible miner. It may support a future selected-miner +trend, which is outside v1. + +### Repair APIs + +`GET /api/repairs/{repair_id}` and its event route expose authoritative modular +repair detail when a repair UUID is already known. There is no current modular +read endpoint that returns the active repair for each map miner. + +`GET /api/repairs/queue` is a bulk legacy queue based on +`miner_location_states.asset_state = repair` and legacy maintenance events. It +does not represent the modular `repairs` table and must not be used as the +authoritative Mine Map repair source. + +The additive map read model therefore needs a set-based join to at most one +active modular repair per miner. It must not perform per-miner repair lookups. + +### Dashboard APIs + +`GET /api/dashboard/miner-table` exposes latest telemetry in a fleet table. +The hot, unstable, and stale endpoints expose useful operational lists, but +they use endpoint-specific or caller-supplied thresholds. Mine Map does not +need these endpoints because the location map already includes the relevant raw +latest measurements. V1 must not combine their classifications into a new +status policy by accident. + +## Required Map Read Model Additions + +Before the full v1 behavior is enabled, the additive `GET /api/location/map` +contract must provide: + +- site UUID and name; +- room UUID as well as room name; +- Asset ID; +- lifecycle status; +- a nullable active repair object containing repair UUID, status, and reason; +- response generation time; +- explicit scope metadata for selected site and room; +- per-miner latest telemetry observation time; and +- truthful nulls for unavailable fields. + +The query must remain set-based, parameterized, permission-controlled, and +bounded by selected scope. The repository owns SQL, the service owns response +composition and any future status policy, and the router owns HTTP validation, +authorization, and the typed response envelope. + +V1 does not require a write endpoint. Reading the map, changing layer, applying +filters, selecting a miner, or opening the inspector must never mutate location, +lifecycle, repair, or telemetry state. + +## Permissions + +- The page and location layout require `locations.view`. +- Supplemental Inventory detail requires `miners.view`. +- The existing miner detail link is shown only when the user can view miners. +- The location-management link is shown only with `locations.manage`. +- No placement or repair command is exposed by Mine Map v1. +- Frontend permission checks control presentation only; backend dependencies + remain authoritative. + +All default roles currently receive both `locations.view` and `miners.view`, +but custom role combinations are possible. Before implementing the additive map +response, decide whether users with only `locations.view` receive a reduced +miner shape or whether the enriched response requires both permissions. The API +must not expose fields merely because the standard role matrix usually grants +both. + +## Architecture Boundary + +Mine Map v1 follows: + +```text +Frontend page + -> FastAPI router + -> Location map service/read model + -> Location repository + -> PostgreSQL +``` + +The frontend contains no SQL, status policy, repair reconstruction, or location +mutation. The read repository may join location, Inventory-owned projections, +latest telemetry, and active repair state for this explicit read model. It does +not write those domains. The service maps persistence results into the typed +map contract and preserves null and unknown values. + +The existing integrated route may be migrated incrementally behind the same +path. The legacy `/locations` page remains available during rollout. Mine Map +v1 must not copy its inline HTML/JavaScript or hard-coded classification rules +into the new frontend shell. + +## V1 Implementation Boundary + +The smallest useful v1 includes: + +- real site and room selection; +- real racks, shelves, positions, and miners; +- explicit empty, blocked, switch, conflict, and unknown positions; +- unplaced and operational-area miners; +- Operational Status as the default layer; +- repair and lifecycle treatments based on authoritative data; +- raw latest telemetry values and freshness; +- status legend and scoped counts; +- basic allowlisted filters and search; +- selected-miner inspector; +- existing miner-detail navigation; +- URL-backed context and predictable browser history; +- loading, empty, denied, error, partial, and refresh-failure states; +- desktop, tablet, and phone behavior; +- keyboard, screen-reader, touch, zoom, and reduced-motion support; +- permission-aware presentation; and +- focused route, contract, rendering, permission, accessibility, and context + preservation tests. + +Location creation, movement, import, rack/shelf editing, and position-type +changes remain in existing management workflows. V1 may link to those workflows +but does not reproduce them. + +## Deferred Capabilities + +The following are explicitly outside v1: + +- advanced thermal heat maps; +- historical map playback; +- drag-and-drop or click-to-move relocation; +- map, rack, shelf, or position editing; +- predictive failure visualization; +- animated telemetry; +- power-density and electrical-capacity modelling; +- saved map views; +- multi-site geographic maps; +- multi-selection and bulk commands; +- repair, lifecycle, or component mutations; +- alerts and notifications; +- selected-miner telemetry charts; and +- generalized infrastructure modelling beyond current position records. + +## Backend And Data Gaps + +These gaps block parts of the acceptance criteria: + +1. **Multi-site location identity:** The current map response omits site and + room UUIDs and joins room ordering by name. Same-named rooms across sites + cannot be represented reliably until the query is site-scoped. +2. **Asset and lifecycle context:** Asset ID and lifecycle are not in the map + response. Without them, the map cannot truthfully distinguish spare, + retired, and in-repair lifecycle states. +3. **Authoritative repair context:** No bulk modular repair projection is joined + to map miners. The legacy repair queue is not an equivalent source. +4. **Freshness policy:** No single approved stale/down threshold exists. V1 can + show explicit offline state and observation age, but cannot classify stale + consistently until policy is centralized. +5. **Health and thermal policy:** Hashrate, rejection, power, and temperature + measurements exist, but model-aware degraded and hot thresholds do not. +6. **Location vocabularies:** Position and location-state values are not + database-constrained. Unknown legacy values require truthful fallback and a + later preflight before constraints are added. +7. **Rack and shelf identity/order:** Rack and shelf are labels, and racks lack + an explicit sort field. V1 can render them but cannot provide durable rack or + shelf links or guaranteed operator-defined rack ordering. +8. **Map contract:** The current response has no typed model or generated-at + metadata and cannot be server-filtered by site or room. + +## Acceptance Criteria + +Mine Map v1 is accepted when all of the following are true: + +- An authorized user can select a site and room and see every configured + position in stored spatial order. +- Occupied, empty, expected-but-unresolved, blocked, switch, conflict, unknown, + and unavailable states are visually and semantically distinct. +- Filtering does not silently erase physical context. +- Explicit offline, hash-unstable, spare, retired, and in-repair miners are + identifiable without inventing thresholds. +- Every telemetry value is tied to an observation time or marked unavailable. +- A user can identify whether apparent problems cluster by room, rack, or shelf + from factual tile states and measurements. +- A selected position identifies its miner or states why no miner is present. +- The inspector shows the key current identity, location, lifecycle, repair, + runtime, and telemetry details available to the user. +- The full miner detail page can be opened using the durable miner UUID. +- Closing the inspector or returning from detail restores the exact shareable + map context and practical scroll/focus context. +- Browser Back, Forward, refresh, bookmarks, and shared URLs behave + predictably. +- Permission failures expose no unauthorized counts, previews, search results, + or inspector fields. +- The page remains usable with keyboard, screen reader, browser zoom, touch, + narrow viewports, missing telemetry, partial records, and transient refresh + failures. +- The implementation performs no per-tile or per-miner request fan-out. +- The map is read-only; existing location management remains available. + +Until the freshness and health-policy gaps are resolved, the map can answer +which miners are explicitly offline, hash-unstable, in repair, or associated +with location conflicts. It cannot truthfully claim that every down, stale, +hot, or degraded miner has been identified. The interface must state that +boundary rather than implying complete health classification. + +## Unresolved Decisions + +The following decisions must be made before implementation begins: + +1. Whether the enriched map response requires both `locations.view` and + `miners.view`, or returns a reduced miner shape to location-only users. +2. Which backend-owned policy defines stale, down, hot, and degraded states, + including scope, thresholds, collector cadence, and configuration source. +3. Whether rack ordering may use a documented natural-label sort for v1 or + requires an additive persisted sort field before rollout. +4. Whether `location_positions.room_id` can be made required after legacy-data + preflight, eliminating name-only room association for production map data. + +These decisions do not require changing the durable miner identity, existing +repair workflow, or v1 read-only boundary.