260 lines
10 KiB
Markdown
260 lines
10 KiB
Markdown
# Task 004: Control Board Replacement
|
|
|
|
## Status
|
|
Design specification. Not yet implemented.
|
|
|
|
## Objective
|
|
Add an explicit, auditable workflow for replacing a miner control board while
|
|
preserving durable asset identity, network-identity history, repair context, and
|
|
immutable operational history.
|
|
|
|
## Product Intent
|
|
A miner is a durable physical asset. Its control board is replaceable. A
|
|
replacement may change the MAC address and control-board serial, but it must not
|
|
create a new miner or erase prior history.
|
|
|
|
**Current implementation context:** `miners.id` is the durable asset UUID.
|
|
`miners.current_mac` and `miners.control_board_sn` are current-state fields.
|
|
`miner_network_identities` already stores MAC, IP, hostname, source, seen
|
|
timestamps, and an `is_current` marker for a miner. The legacy
|
|
`miner_maintenance_events` table can contain MAC and component details, but
|
|
those records are editable, deletable, and not part of the immutable,
|
|
repair-scoped timeline. No dedicated control-board component-history table
|
|
exists.
|
|
|
|
The integrated application also exposes the legacy
|
|
`POST /api/miners/{miner_id}/change-mac` route, and Inventory, discovery, and
|
|
location-import code can write current network fields. These are existing
|
|
compatibility paths, not implementations of Task 004.
|
|
|
|
## Scope
|
|
- proposed route `POST /api/miners/{miner_id}/replace-control-board`
|
|
- proposed permission `miners.replace_control_board`
|
|
- require an active repair and miner lifecycle `in_repair`
|
|
- normalize and validate the new MAC address
|
|
- reuse and strengthen `miner_network_identities` for MAC history rather than
|
|
creating a competing MAC-history table
|
|
- update `miners.current_mac` and `miners.control_board_sn` as current-state
|
|
projections
|
|
- preserve the old and new control-board serials in immutable repair history
|
|
- append one immutable `CONTROL_BOARD_REPLACED` event
|
|
- execute atomically and safely under concurrency
|
|
- add migration, constraints, tests, and documentation
|
|
|
|
## Out of Scope
|
|
Automatic discovery reconciliation, generic component inventory, stock quantities, purchasing, warranties, firmware flashing, pool changes, remote commands, bulk replacement, automatic miner merge, UI redesign, and replacement of hashboards, fans, or PSUs.
|
|
|
|
## Permission
|
|
The proposed permission is:
|
|
|
|
```text
|
|
miners.replace_control_board
|
|
```
|
|
|
|
Proposed role assignment, following the implemented operational-action policy:
|
|
|
|
- Admin: allowed
|
|
- Manager: allowed
|
|
- Technician: allowed
|
|
- Read Only: denied
|
|
|
|
The permission and assignments do not exist yet. They must be introduced by the
|
|
Task 004 migration. The route must check the capability rather than hard-code
|
|
role names.
|
|
|
|
## Authentication and Response
|
|
Authentication is required. On success, return `200 OK` using a typed response
|
|
envelope. The response must identify the durable miner UUID, resolved active
|
|
repair UUID, current MAC, current control-board serial, and appended event ID and
|
|
sequence without exposing unrelated miner data.
|
|
|
|
## Request
|
|
Suggested shape:
|
|
|
|
```json
|
|
{
|
|
"new_mac_address": "AA:BB:CC:DD:EE:FF",
|
|
"new_control_board_serial": "CB-2026-001234",
|
|
"notes": "Replacement board installed and tested."
|
|
}
|
|
```
|
|
|
|
The new MAC is required. `new_control_board_serial` is a required, nullable
|
|
field so the operator explicitly records a serial or confirms that it is
|
|
unknown. A null or normalized blank value clears the old current-board serial;
|
|
it must never leave the removed board serial as current. Normalize nonblank
|
|
serials and limit them to the Inventory field maximum of 255 characters.
|
|
|
|
Notes are optional, trim like existing repair notes, treat whitespace-only as
|
|
absent, and use the established 4,000-character limit. The client must not
|
|
supply the old MAC, old serial, repair UUID, actor, timestamp, event sequence,
|
|
or event payload; the service derives those values from authoritative state.
|
|
|
|
## MAC Rules
|
|
Define and test one canonical display format, such as
|
|
`AA:BB:CC:DD:EE:FF`, while comparing ownership by a normalized 12-hex-digit
|
|
key. Existing data must be checked for normalized conflicts before a uniqueness
|
|
constraint is added.
|
|
|
|
Reject malformed, broadcast, multicast, all-zero, and currently duplicated MAC
|
|
addresses. A MAC historically associated with another asset must produce a
|
|
conflict requiring a later deliberate transfer or correction workflow rather
|
|
than automatic reassignment. Task 004 must not silently rewrite, merge, or
|
|
delete a conflicting association.
|
|
|
|
## Durable Identity Rules
|
|
- miner UUID and asset record remain unchanged
|
|
- current MAC changes
|
|
- prior MAC remains associated with the same miner in
|
|
`miner_network_identities`
|
|
- repair, location, ownership, and asset-ID history remain unchanged
|
|
- no second miner is auto-created
|
|
- no automatic asset merge occurs
|
|
|
|
## Preconditions
|
|
1. Miner exists.
|
|
2. Miner is `in_repair`.
|
|
3. Miner has exactly one active repair.
|
|
4. New MAC is valid and canonical.
|
|
5. New MAC is not current or historical on another miner.
|
|
6. When a current MAC exists, old and new MAC differ.
|
|
7. Actor and timestamp are authoritative.
|
|
|
|
## Data Model
|
|
Task 004 must build on the existing structures:
|
|
|
|
- `miners.current_mac` remains the current MAC projection.
|
|
- `miners.control_board_sn` remains the current control-board serial projection.
|
|
- `miner_network_identities` remains the MAC-history structure. The migration
|
|
may add only the constraints or indexes required to enforce one current
|
|
association and prevent cross-miner normalized-MAC ownership conflicts.
|
|
- `repair_events` remains the immutable repair timeline. Extend its constrained
|
|
event vocabulary and payload validation for `CONTROL_BOARD_REPLACED`.
|
|
|
|
Do not create a second MAC-history table. Do not treat legacy
|
|
`miner_maintenance_events` as authoritative component history for this
|
|
workflow. A separate component-installation table is outside this task unless
|
|
repository inspection during implementation identifies a concrete query or
|
|
integrity requirement that cannot be met by the current projections, network
|
|
identity history, and immutable repair event. Report that conflict before
|
|
adding such a table.
|
|
|
|
## Constraints
|
|
- no more than one current network-identity row per miner
|
|
- one miner owner per normalized MAC across current and historical identities
|
|
- valid normalized MAC for rows written by the new workflow
|
|
- current projections agree with the newly current identity after replacement
|
|
- foreign-key behavior preserves operational history
|
|
|
|
The migration must preflight existing normalized MAC duplicates and fail with
|
|
an actionable message rather than changing existing ownership silently.
|
|
|
|
## Legacy Data
|
|
Existing current MAC values may be reconciled with
|
|
`miner_network_identities` only when the association is unambiguous. Existing
|
|
identity rows and legacy maintenance events must be preserved. Unknown
|
|
installation actor, time, control-board serial, or source must remain unknown;
|
|
never fabricate a replacement event.
|
|
|
|
## Event
|
|
Proposed type: `CONTROL_BOARD_REPLACED`.
|
|
|
|
Suggested payload:
|
|
|
|
```json
|
|
{
|
|
"old_mac_address": "CC:0C:15:00:0A:49",
|
|
"new_mac_address": "CC:0C:15:00:9D:82",
|
|
"old_control_board_serial": null,
|
|
"new_control_board_serial": "CB-2026-001234",
|
|
"notes": "Replacement board installed and tested."
|
|
}
|
|
```
|
|
|
|
Include only authoritative values.
|
|
|
|
## Atomicity and Locking
|
|
Use one transaction and the established lock order:
|
|
|
|
```text
|
|
Miner -> Repair -> network-identity rows
|
|
```
|
|
|
|
The service resolves and locks the miner first, then its one active repair, then
|
|
the relevant network-identity rows. This matches Tasks 001 and 003.
|
|
|
|
The operation must atomically validate MAC ownership, mark prior network
|
|
identities non-current, create or activate the new association without
|
|
reassigning another miner's history, update the current miner fields, and append
|
|
exactly one event to the active repair.
|
|
|
|
Concurrent requests must not assign one MAC to two miners, create two current boards, duplicate event sequence values, or leave partial state.
|
|
|
|
## Errors
|
|
- `401`: unauthenticated
|
|
- `403`: authenticated user lacks `miners.replace_control_board`
|
|
- `404`: miner missing
|
|
- `409`: miner not in repair, no active repair, same current MAC, MAC conflict,
|
|
historical ownership conflict, or concurrent replacement
|
|
- `422`: malformed UUID, invalid MAC, missing required fields, excessive text,
|
|
unknown fields, or other invalid request data
|
|
|
|
## Architecture
|
|
Preserve `Router -> Service -> Repository -> PostgreSQL`.
|
|
|
|
No SQL or transaction management in routers. No FastAPI in services or repositories. Do not add generic component CRUD.
|
|
|
|
## Tests
|
|
Cover:
|
|
- role authorization
|
|
- valid replacement
|
|
- durable asset identity unchanged
|
|
- old MAC remains searchable
|
|
- malformed and prohibited MACs
|
|
- current and historical MAC conflicts
|
|
- duplicate existing normalized-MAC migration preflight
|
|
- missing active repair
|
|
- miner not in repair
|
|
- concurrent replacement
|
|
- rollback at each write step
|
|
- direct database constraints
|
|
- event payload and sequence
|
|
- Tasks 001-003 regressions
|
|
- migration upgrade, downgrade/refusal, re-upgrade, and empty database
|
|
|
|
## Decisions Required Before Implementation
|
|
Before Task 004 is marked ready for implementation, decide:
|
|
|
|
1. Whether the legacy `/api/miners/{miner_id}/change-mac` route will delegate to
|
|
the new service or be deprecated through an explicit compatibility plan. It
|
|
must not remain a competing writer that bypasses the active-repair workflow.
|
|
2. Whether an operator-recorded replacement MAC counts as a network observation
|
|
for the existing `first_seen_at` and `last_seen_at` semantics, or whether the
|
|
schema needs a distinct association timestamp. Do not label an unobserved MAC
|
|
as discovered accidentally.
|
|
3. How Inventory creation, discovery, and location-import writers will translate
|
|
normalized-MAC ownership conflicts without exposing raw database errors or
|
|
auto-merging assets.
|
|
|
|
These decisions do not permit automatic asset creation, merging, or MAC
|
|
transfer. A separate deliberate transfer workflow remains out of scope.
|
|
|
|
## Acceptance Criteria
|
|
1. One explicit route exists.
|
|
2. Durable miner identity remains unchanged.
|
|
3. Old and new MACs remain searchable.
|
|
4. Normalized MAC ownership and current-association invariants are
|
|
database-backed.
|
|
5. Active repair and `in_repair` are required.
|
|
6. Exactly one immutable event is written.
|
|
7. Operation is atomic and concurrency-safe.
|
|
8. Legacy data remains truthful.
|
|
9. Migration and regression suites pass.
|
|
10. No commit or push is performed.
|
|
|
|
## Completion Report
|
|
Report branch, starting commit, migration, files, route, permission, schema, constraints, normalization, history, transaction strategy, event, tests, validation, limitations, and final `git status --short`.
|
|
|
|
Do not commit.
|
|
Do not push.
|