diff --git a/docs/design/API_STYLE_GUIDE.md b/docs/design/API_STYLE_GUIDE.md new file mode 100644 index 0000000..9f55f37 --- /dev/null +++ b/docs/design/API_STYLE_GUIDE.md @@ -0,0 +1,156 @@ +# MinerOps API Style Guide + +## Purpose +Keep MinerOps APIs explicit, consistent, auditable, and aligned with the domain architecture. Implemented conventions and approved task specifications take precedence. + +## Principles +1. Prefer domain commands over generic CRUD. +2. Use nouns for resources and explicit action routes for operational commands. +3. Keep writes focused and atomic. +4. Return truthful domain representations. +5. Separate authentication, permission, validation, domain logic, and persistence. +6. Preserve static-route compatibility. +7. Do not expose database structure as the public API. + +## Current and Proposed Examples +Implemented modular repair routes: + +```http +POST /api/repairs +GET /api/repairs/{repair_id} +GET /api/repairs/{repair_id}/events +POST /api/repairs/{repair_id}/complete +``` + +Relevant legacy compatibility routes include: + +```http +GET /api/repairs/queue +GET /api/repairs/parts-summary +POST /api/miners/{miner_id}/change-mac +``` + +These integrated routes predate the modular domain conventions and are not +models for new code. + +Proposed by Task 004, not implemented: + +```http +POST /api/miners/{miner_id}/replace-control-board +``` + +Good: +```http +POST /api/repairs/{repair_id}/complete +``` + +Avoid: +```http +PATCH /api/repairs/{repair_id} +{"status":"completed"} +``` + +## Methods +- GET is read-only. +- POST creates a resource or invokes an explicit command. +- PATCH is limited to non-operational metadata when partial update semantics are + appropriate. +- Meaningful operational changes use explicit commands rather than generic PUT, + PATCH, or DELETE routes. +- Historical records are corrected with explicit append-only or void workflows, + not physical deletion. + +## Status Codes +- 200: successful read or command with response +- 201: created resource +- 204: success with no useful response +- 401: unauthenticated +- 403: lacks permission +- 404: resource missing +- 409: valid request conflicts with domain state +- 422: request validation failure + +## Identifiers +Use typed UUIDs for authoritative entity identifiers. Do not expose sequential database IDs as public identity. + +## Time and Actors +Use timezone-aware ISO 8601 timestamps. Authoritative timestamps and actor IDs are server controlled. Never accept client-supplied event actor, event timestamp, completion timestamp, or sequence. + +## Permissions +Implemented modular repair capabilities are: + +- `repairs.read` +- `repairs.create` +- `repairs.complete` + +Task 004 proposes `miners.replace_control_board`; it is not registered today. +Do not duplicate role logic in routers. + +## Request Models +Use constrained types, normalize whitespace, reject unknown values, enforce length limits, avoid persistence-only fields, and never accept arbitrary client event payloads. + +## Response Models +The modular Inventory and Repairs APIs use typed success envelopes containing +`data`, `meta`, and `error`. Expose useful domain state, not secrets or unrelated +nested data. Preserve null when historical truth is unknown. + +## Errors +The application does not yet have one uniform typed error envelope. Modular +routes currently use FastAPI validation responses and `HTTPException` detail +responses while translating domain exceptions to 404, 409, or 422 as +appropriate. New routes must follow the established behavior of their domain +and must not leak SQL or driver errors. Repositories do not raise HTTP +exceptions. Services do not import FastAPI. + +## Pagination +Potentially large collections require a bounded pagination design. The current +Inventory collection uses bounded `limit` and `offset`; do not claim cursor +pagination until a domain adopts it. The implemented repair timeline returns +all events for one repair by explicit Task 002 design. Any later change to that +contract requires compatibility review. + +## Filtering +Use explicit allowlisted filters and sort fields. Never pass arbitrary column names into queries. + +## Static Routes +Register static routes before overlapping UUID routes: +```text +/api/repairs/queue +/api/repairs/parts-summary +/api/repairs/{repair_id} +``` +Add route-precedence tests. + +## Idempotency and Concurrency +Use database invariants, row locks, affected-row checks, and narrow operation +identifiers where demonstrated need exists. Do not add a generalized +idempotency platform speculatively. + +The implemented repair workflows prevent duplicate active repairs, duplicate +completion, duplicate event sequences, and partial state. Task 004 proposes the +additional invariant that concurrent requests cannot assign one normalized MAC +to two miners. + +## Events +Clients request domain actions, not raw events. The service constructs immutable events. + +## Versioning +MinerOps does not currently expose versioned API prefixes. Any incompatible API +versioning scheme requires a separate compatibility decision; this guide does +not reserve a future path. + +## Route Specification Checklist +Every new route should define: +- method and path +- authentication and permission +- request and response +- status codes +- domain rules +- transaction boundary +- concurrency behavior +- events +- tests +- out-of-scope items + +## Review Checklist +Confirm explicit action choice, status codes, permission, actor and timestamp authority, UUID typing, limits, transaction, concurrency, event construction, route precedence, architecture boundaries, and regressions. diff --git a/docs/design/REPAIR_DOMAIN_MODEL.md b/docs/design/REPAIR_DOMAIN_MODEL.md new file mode 100644 index 0000000..7394224 --- /dev/null +++ b/docs/design/REPAIR_DOMAIN_MODEL.md @@ -0,0 +1,174 @@ +# MinerOps Repair Domain Model + +## Purpose +This design guide describes repair-domain concepts, relationships, lifecycle, invariants, and operational boundaries. Implemented repository behavior and approved task specifications remain authoritative. + +## Core Concepts + +### Miner +A durable physical mining asset whose identity is independent of IP address, MAC address, control board, hostname, location, and repair state. + +### Repair +A bounded operational episode for one miner. It has one start, zero or one completion, immutable timeline events, actors, and timestamps. + +### Repair Event +An immutable fact ordered by explicit sequence. Implemented event types are +`REPAIR_STARTED`, `INITIAL_NOTE`, and `REPAIR_COMPLETED`. + +### Network Identity History +`miner_network_identities` associates MAC, IP, and hostname observations with a +durable miner and identifies current versus historical rows. It is not a +separate asset identity. + +### Proposed Component History +Task 004 proposes preserving control-board replacement details without +redefining the miner. No dedicated immutable component-history table currently +exists; the legacy generic maintenance-event table is not the modular repair +timeline. + +### Proposed Repair Part Entry +Task 005 proposes a record of a part installed, removed, or consumed during a +repair. It is not a warehouse inventory transaction, and no `repair_parts` +table currently exists. + +## Relationships +Current relationships: + +```text +Miner -> many Repairs -> ordered Repair Events +Miner -> Network Identity History +``` + +Control-board replacement history and repair-part entries are proposed +extensions, not implemented relationships. + +## Lifecycle + +Current miner states relevant to repair: +- `active` +- `spare` +- `in_repair` +- `retired` + +Eligible Begin Repair sources: +- active +- spare + +Eligible Return to Service destinations: +- active +- spare + +Terminal: +- retired + +The repair schema defines these statuses: + +- Active: `OPEN`, `IN_PROGRESS`, `WAITING_FOR_PARTS`, `READY_FOR_TEST` +- Terminal: `COMPLETED`, `CANCELLED` + +The modular API currently creates repairs as `OPEN` and can complete any active +repair. It does not provide status-transition or cancellation routes. The +implemented modular workflow is therefore: + +```text +OPEN -> COMPLETED +``` + +The other active values remain valid schema states for compatibility and later +explicit workflows. Cancellation or abandonment requires a separate task. + +## Current Workflow +```text +Miner active/spare + -> Begin Repair + -> Miner in_repair + -> Timeline events + -> Return to Service + -> Miner active/spare + -> Repair completed +``` + +## Invariants +- replacing a board or MAC must not create a new miner +- asset-ID and network-identity history must be preserved +- no more than one active repair per miner is database-enforced +- modular repair actions require the miner and repair state to agree +- Return to Service prevents a completed repair from leaving the miner in `in_repair` +- multi-record transitions are atomic +- events are append-only and explicitly sequenced +- actor comes from authentication +- timestamps are server/database controlled +- new completions require completion time, actor, and eligible resulting lifecycle +- legacy unknown values remain unknown rather than fabricated + +## Transaction Boundaries + +### Begin Repair +Lock miner, create repair, transition miner to `in_repair`, append start event. + +### Return to Service +Lock miner, lock repair, validate active state, complete repair, transition miner to active/spare, append completion event. + +### Proposed Control Board Replacement +Task 004 proposes locking miner, active repair, and network-identity rows; +validating MAC ownership; updating current projections; and appending one event. + +### Proposed Repair Part Recording +Task 005 proposes locking the affected repair or repair-part row, adding or +voiding the entry, and appending the corresponding event atomically. + +## Lock Order +Use: +```text +Miner -> Repair -> related rows +``` + +This aligns workflows and reduces deadlock risk. + +## Commands vs CRUD +Implemented explicit commands: + +- Begin Repair +- Return to Service + +Proposed explicit commands: + +- Replace Control Board +- Record Repair Part +- Void Repair Part + +Avoid generic repair patches for meaningful operations. + +## Read Models +Implemented modular reads: + +- repair detail +- ordered repair timeline + +The integrated application also has legacy repair-queue, parts-summary, and +miner network-identity reads. Those routes are compatibility behavior, not the +proposed Task 004 or Task 005 read models. + +Proposed focused reads include repair parts and control-board replacement +history. Read models do not weaken write invariants. + +## Permissions +Implemented permissions: + +- `repairs.read` +- `repairs.create` +- `repairs.complete` + +Proposed permissions: + +- `repairs.parts.manage` +- `miners.replace_control_board` + +Task 005 reuses `repairs.read` for its proposed read route. Roles receive capabilities; +endpoints should not hard-code role names when permission checks exist. + +## Legacy Data +Preserve truth, allow nullable historical fields where necessary, enforce stronger rules for new or updated rows, use PostgreSQL `NOT VALID` constraints where appropriate, and never backfill invented actors, states, or events. + +## Future Extensions +Repair cancellation, diagnosis, work orders, technician assignment, burn-in testing, photos, warranty/RMA, component inventory, and donor-unit tracking should each arrive through explicit tasks. diff --git a/docs/tasks/004-control-board-replacement.md b/docs/tasks/004-control-board-replacement.md new file mode 100644 index 0000000..01cf4e8 --- /dev/null +++ b/docs/tasks/004-control-board-replacement.md @@ -0,0 +1,259 @@ +# 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. diff --git a/docs/tasks/005-repair-parts-tracking.md b/docs/tasks/005-repair-parts-tracking.md new file mode 100644 index 0000000..fcc6d49 --- /dev/null +++ b/docs/tasks/005-repair-parts-tracking.md @@ -0,0 +1,189 @@ +# Task 005: Repair Parts Tracking + +## Status +Design specification. Not yet implemented. + +## Objective +Allow authorized operators to record parts installed, removed, or consumed +during a repair while preserving immutable history and leaving room for a later +spare-parts inventory system. + +## Product Intent +A technician should be able to determine what parts were installed, removed, or +consumed; their quantity and identifying serial number where relevant; any +concise notes; and the authoritative actor and timestamp. + +## Scope +- proposed focused repair-part read and action APIs +- proposed capability-based permissions +- a deliberately small part type and action vocabulary +- part type, action, quantity, optional serial number, and notes +- serialized component identifiers where applicable +- explicit void/correction rather than physical deletion +- immutable repair events +- migration, constraints, tests, and documentation + +## Out of Scope +Purchasing, suppliers, pricing, invoices, warehouse transfers, reorder automation, warranties, RMA, full inventory, generalized bill of materials, bulk import, automatic stock deduction, photos, work orders, and control-board identity replacement. + +Task 004 remains authoritative for control-board identity and MAC changes. +Task 005 must not accept `control_board` as a generic part type or update +`miners.control_board_sn`, `miners.current_mac`, or +`miner_network_identities`. + +## API +Proposed routes: + +```http +GET /api/repairs/{repair_id}/parts +POST /api/repairs/{repair_id}/parts +POST /api/repairs/{repair_id}/parts/{repair_part_id}/void +``` + +Do not physically delete authoritative entries. + +## Permissions +The proposed authorization model is: + +- use implemented `repairs.read` for the read route +- introduce `repairs.parts.manage` for add and void actions + +Grant the proposed write permission to Admin, Manager, and Technician. Do not +grant it to Read Only. `repairs.parts.manage` does not exist yet and must be +introduced by the Task 005 migration. The router checks capabilities rather than +role names. + +## Add Request +```json +{ + "part_type": "fan", + "action": "installed", + "quantity": 1, + "serial_number": null, + "notes": "Replacement fan installed." +} +``` + +## Initial Vocabulary +Proposed part types: + +- `fan` +- `power_supply` +- `hashboard` +- `cable` +- `consumable` +- `other` + +Proposed actions: + +- `installed` +- `removed` +- `consumed` + +Task 005 does not model source inventory, stock movement, return destination, +condition, or disposition. Those concepts require a later inventory decision +and must not be approximated with overlapping action/value combinations here. +Use notes for concise context that is not authoritative structured inventory +data. + +## Quantity Rules +- quantity greater than zero +- integer quantities for this task +- serialized components normally quantity 1 +- consumables may exceed 1 +- voiding does not create a negative entry or reverse inventory; it marks the + original record void + +## Repair State Rules +- add entries only while the repair is active +- void entries only while the repair is active +- read entries and void state after completion +- completed repairs remain read-only +- no unrestricted editing in any repair state + +## Data Model +Proposed table: `repair_parts`. No such table currently exists. The legacy +`/api/repairs/parts-summary` route summarizes generic maintenance-event data and +is not an implementation of this model. + +Potential fields: +- UUID +- repair UUID +- part type +- action +- quantity +- serial number +- notes +- recorded actor and timestamp +- void actor, timestamp, and reason +- related event UUID + +Entries are never physically deleted. A void changes only the one-way void +metadata. It must not edit the original part type, action, quantity, serial +number, notes, recording actor, or recording timestamp. The void reason is +required; void actor and timestamp come from authentication and the server. + +## Constraints +- quantity > 0 +- void fields all-null or all-present +- void timestamp after recorded timestamp +- actor and recorded timestamp required +- valid constrained values and combinations +- serialized entries with a serial number have quantity 1 + +## Events +Proposed types: +- `REPAIR_PART_RECORDED` +- `REPAIR_PART_VOIDED` + +The events are immutable history; the table is the queryable projection. + +## Atomicity +Adding a part and appending its event occur in one transaction. Voiding and appending the void event occur in one transaction. + +Voiding must lock the repair-part row and succeed only when its void fields are +still null. Concurrent void requests must produce one success and one conflict, +with exactly one `REPAIR_PART_VOIDED` event. + +## Architecture +Preserve `Router -> Service -> Repository -> PostgreSQL`. No generic parts CRUD, router SQL, or physical deletion. + +## Tests +Cover: +- authorization +- serialized and consumable entries +- removed part +- list chronology +- read after completion +- explicit void +- invalid quantities and combinations +- control-board part rejection +- add to completed repair +- already-voided conflict +- cross-repair entry conflict +- concurrent void +- rollback on event failure +- direct constraints +- Tasks 001-004 regressions +- migration lifecycle and full backend suite + +## Migration +Create the focused table, add permissions and event constraints, add justified indexes, preserve legacy truth, and refuse destructive downgrade when authoritative Task 005 history exists. + +## Acceptance Criteria +1. Parts can be recorded against an active repair. +2. Historical entries remain readable. +3. Entries are never physically deleted. +4. Voids are explicit and audited. +5. Actor and timestamps are authoritative. +6. Writes and events are atomic. +7. Values and quantities are constrained. +8. No warehouse or purchasing system is introduced. +9. Task 004 remains authoritative for control-board identity changes. +10. No commit or push is performed. + +## Completion Report +Report files, migration, routes, permissions, schema, constraints, events, tests, migration lifecycle, architecture audit, limitations, and final status. + +Do not commit. +Do not push.