Files
minerops/docs/tasks/003-return-to-service.md
T

21 KiB

Task 003: Return to Service

Status

Ready for implementation.

Objective

Complete the MinerOps repair lifecycle by adding an explicit Return to Service operation that closes an active repair, records an immutable completion event, and transitions the miner from in_repair to an eligible post-repair lifecycle state.

The operation must be atomic, permission-controlled, auditable, safe under concurrent requests, and compatible with the existing Begin Repair and Repair Timeline implementations.

Product Intent

A repair should not remain open indefinitely after work is complete.

An authorized operator must be able to:

  1. identify the active repair,
  2. confirm the miner is ready to leave repair,
  3. select or confirm the resulting lifecycle state,
  4. optionally record a concise completion summary,
  5. close the repair,
  6. transition the miner out of in_repair,
  7. preserve immutable operational history.

This is an operational command, not generic repair editing.

Background

Task 001 introduced the Begin Repair workflow.

Task 002 introduced repair detail and immutable repair timeline read APIs.

The current repair model supports:

  • creating an active repair,
  • moving a miner into in_repair,
  • preserving the miner's pre-repair lifecycle in the repair-start event payload,
  • enforcing one active repair per miner,
  • reading repair details,
  • reading ordered repair events.

Task 003 completes that lifecycle.

The implementation must inspect the repository and reuse current names, models, exception conventions, migration patterns, event sequencing, and transaction infrastructure rather than inventing parallel abstractions.

Scope

This task includes:

  • a new repairs.complete permission,
  • a new explicit Return to Service API,
  • request and response models,
  • service-layer completion workflow,
  • focused repository methods,
  • repair completion persistence,
  • miner lifecycle transition,
  • immutable completion event,
  • concurrency protection,
  • migration and permission assignment,
  • full authorization, domain, migration, routing, and regression tests,
  • release-document update after implementation passes.

Explicitly Out of Scope

Do not implement:

  • generic repair update or patch endpoints,
  • reopening a completed repair,
  • cancelling a repair,
  • abandoning a repair,
  • repair diagnosis fields,
  • repair photos,
  • repair parts consumption,
  • technician assignment,
  • work orders,
  • control board replacement,
  • MAC transfer,
  • automatic monitoring confirmation,
  • automated burn-in or test workflows,
  • bulk Return to Service,
  • notifications,
  • UI changes unless the repository already requires generated API client updates,
  • changes to unrelated legacy repair routes.

Those belong in later tasks.

User and Operator Workflow

A typical flow is:

  1. A technician opens the active repair.
  2. The technician completes physical work and any required validation.
  3. The technician invokes Return to Service.
  4. The technician chooses the miner's destination lifecycle:
    • the original pre-repair lifecycle, when eligible,
    • or another explicitly allowed lifecycle.
  5. The technician may enter a completion summary.
  6. MinerOps atomically:
    • verifies the repair is active,
    • verifies the miner is still in repair,
    • validates the destination lifecycle,
    • closes the repair,
    • transitions the miner,
    • appends a completion event.
  7. The repair becomes read-only historical data.

API

Add one explicit action route.

POST /api/repairs/{repair_id}/complete

Use complete unless existing route naming conventions make return-to-service materially more consistent. Do not add both aliases.

Authentication

Required.

Permission

repairs.complete

Path Parameter

  • repair_id: UUID

Request Body

The request should contain:

{
  "target_lifecycle": "active",
  "completion_summary": "Replaced failed fan and completed load test."
}

Use repository-established lifecycle values and naming.

target_lifecycle

Required unless the service can unambiguously restore the original lifecycle under existing product rules.

Preferred design: require an explicit target lifecycle in the request so the operator confirms the resulting operational state.

Allowed target values must be restricted to valid non-repair lifecycle states.

At minimum:

  • active
  • inactive

Also allow other existing non-terminal lifecycle states only if they are already legitimate post-repair states in the current domain model.

Never allow:

  • in_repair
  • unknown values
  • deprecated values
  • terminal or destructive states that are not valid Return to Service destinations

Do not invent new lifecycle states.

completion_summary

Optional.

Requirements:

  • trim leading and trailing whitespace,
  • empty after trimming becomes null,
  • enforce a sensible maximum length using current project conventions,
  • preserve text exactly after normalization,
  • do not interpret it as structured diagnosis or parts data.

Successful Response

Return the completed repair using the existing repair-detail response model where practical.

The response must allow the client to determine:

  • repair UUID,
  • miner UUID,
  • completed status,
  • completion timestamp,
  • completing actor where exposed by current conventions,
  • resulting miner lifecycle,
  • timeline or event count only if already part of the established response.

Do not expand the response with unrelated miner details.

Status Codes

200 OK

Preferred when returning the completed repair representation.

Use 201 only if existing operational-command conventions clearly require it. Do not use 204 if the existing repair response can be returned cleanly.

401 Unauthorized

Unauthenticated request.

403 Forbidden

Authenticated actor lacks repairs.complete.

404 Not Found

The repair UUID does not exist.

Do not reveal authorization-sensitive information beyond existing project behavior.

409 Conflict

Use for valid requests that conflict with current domain state, including:

  • repair is already completed,
  • repair is otherwise not active,
  • miner is no longer in in_repair,
  • repair does not match the miner's active repair,
  • target lifecycle is known but not eligible for Return to Service,
  • a concurrent request completed the repair first.

422 Unprocessable Entity

Use for malformed UUIDs, invalid JSON, missing required fields, unknown lifecycle values, excessive summary length, or other request-validation errors.

Permissions

Add:

repairs.complete

Intended Role Assignment

Follow current role conventions and migrations.

Expected behavior:

  • Administrator: allowed
  • Technician: allowed
  • Read Only / Viewer: denied
  • Auditor: denied unless current policy explicitly grants operational actions
  • Unauthenticated: denied

Do not grant the permission to every role by default.

Do not overload repairs.create.

Domain Rules

Active Repair Requirement

Only an active repair can be completed.

A completed repair cannot be completed again.

Miner Lifecycle Requirement

The associated miner must currently be in in_repair.

If it is not, return a domain conflict and do not modify anything.

Destination Lifecycle

The destination must:

  • exist in the current lifecycle domain,
  • not equal in_repair,
  • be explicitly allowed as a post-repair state,
  • be applied only after all validation succeeds.

Original Lifecycle

The Begin Repair workflow preserves the previous lifecycle in the REPAIR_STARTED event payload.

The service may use this value for:

  • validation,
  • response context,
  • defaulting only if the API contract and existing model conventions support it safely.

The implementation must not assume every miner should return to active.

If restoration of the previous lifecycle is supported, preserve exact historical meaning. Do not infer or fabricate it when missing.

Completion Is Final

Once completed:

  • the repair is historical,
  • the completion event is immutable,
  • the active-repair constraint is released,
  • a later repair must be a new repair record.

No Silent Repair Mutation

Do not edit prior events.

Do not replace the start event.

Do not overwrite historical lifecycle values.

Architecture

Preserve:

Router -> Service -> Repository -> PostgreSQL

Router Responsibilities

The router should:

  • define the action route,
  • validate the UUID and request model,
  • enforce authentication and repairs.complete,
  • call the service,
  • map established domain exceptions to HTTP responses,
  • return the established response model.

The router must not:

  • contain SQL,
  • manage commits or rollbacks,
  • choose lifecycle transitions,
  • construct event sequence values,
  • read actor UUIDs from the request body.

Service Responsibilities

The service should:

  • load and lock the repair,
  • verify it exists and is active,
  • load and lock the associated miner when required,
  • verify the miner is in repair,
  • validate target lifecycle,
  • normalize summary,
  • close the repair,
  • update miner lifecycle,
  • append the completion event,
  • return the completed domain result,
  • ensure the operation is atomic.

Repository Responsibilities

Repositories should provide focused methods for:

  • retrieving an active repair for update,
  • retrieving the associated miner for update,
  • marking a repair completed,
  • updating lifecycle,
  • obtaining or allocating the next repair event sequence,
  • appending a repair event,
  • reading the completed repair representation efficiently.

Do not introduce a broad generic repair-update method.

Database and Migration

A migration is required if the current schema does not already support all required completion data and permission assignment.

Inspect the existing repair table before adding columns.

Preferred Repair Completion Fields

If not already present, add only the fields needed to represent completion authoritatively:

  • completed_at: timezone-aware timestamp, nullable while active
  • completed_by_user_id: UUID foreign key, nullable while active
  • completion_summary: nullable text
  • resulting_lifecycle: constrained lifecycle value, nullable while active

Use existing naming conventions if equivalent columns already exist.

Status Representation

Do not add redundant mutable status data if active versus completed can be derived safely from established fields.

If the current repair schema already uses a status column, reuse and constrain it rather than introducing a second status mechanism.

Constraints

Enforce critical invariants in PostgreSQL where practical.

Examples:

  • active repair has no completion timestamp,
  • completed repair has a completion timestamp,
  • completed repair has a completing actor,
  • resulting lifecycle is null while active and present when completed,
  • resulting lifecycle cannot equal in_repair,
  • completion summary length is constrained if the project uses database length checks,
  • one active repair per miner remains enforced.

Adapt the exact checks to the existing schema.

Foreign Keys

completed_by_user_id should reference the established user table.

Use the project's existing historical-record deletion policy. Operational history should not silently disappear.

Indexes

Add only indexes justified by actual read patterns.

Likely useful:

  • existing active-repair partial uniqueness,
  • lookup by miner,
  • lookup by completion timestamp only if current repair queue/history queries need it.

Avoid speculative indexes.

Migration Data Handling

Existing active repairs must remain active.

Existing completed repairs, if legacy data exists, must be handled without inventing actor IDs, summaries, or lifecycle states.

If new non-null completion invariants cannot be truthfully applied to legacy completed rows:

  • use nullable historical fields with conditional checks,
  • preserve legacy data,
  • document the limitation,
  • do not fabricate values.

Downgrade

The downgrade must restore the previous schema safely.

Before dropping completion columns or constraints, restore miner lifecycle only if the downgrade actually reverses newly created completion data and there is a truthful historical source.

Do not map every historical completed repair to active.

Do not invent original lifecycle values.

If a fully safe downgrade is impossible once real completion records exist, use the project's established explicit-refusal pattern rather than destructive guessing.

Events and History

Append one immutable completion event.

Use the existing repair event naming conventions. Preferred semantic name:

REPAIR_COMPLETED

Reuse an existing equivalent event type if already defined.

Event Fields

The event must include:

  • event UUID,
  • repair UUID,
  • next explicit sequence number,
  • authenticated actor UUID,
  • server/database timestamp,
  • structured payload.

Payload

Include only durable, meaningful values.

Recommended payload:

{
  "previous_miner_lifecycle": "in_repair",
  "resulting_miner_lifecycle": "active",
  "pre_repair_lifecycle": "inactive",
  "completion_summary": "Replaced failed fan and completed load test."
}

pre_repair_lifecycle may be omitted if the existing event model or service cannot retrieve it reliably without ambiguity.

Do not duplicate arbitrary repair fields in the payload.

Do not store display names as authoritative actor identity.

Event Ordering

Use the established explicit sequence mechanism.

Do not calculate order from timestamps alone.

Concurrent event insertion must not produce duplicate sequence values.

Models and Validation

Create or extend focused models.

Suggested names, adapted to current conventions:

  • RepairCompleteRequest
  • RepairDetailResponse or existing equivalent
  • internal completion result model if needed

Validation Requirements

  • UUID path validation,
  • finite lifecycle validation,
  • summary trimming,
  • empty summary normalization,
  • maximum summary length,
  • no client-supplied actor,
  • no client-supplied completion timestamp,
  • no arbitrary event payload supplied by the client.

Concurrency and Atomicity

The entire operation must use one transaction.

The following must succeed or fail together:

  1. repair validation,
  2. miner validation,
  3. repair completion update,
  4. miner lifecycle transition,
  5. completion event insertion.

Concurrent Completion

Two simultaneous completion requests must not both succeed.

Expected result:

  • one request succeeds,
  • the other receives 409 Conflict,
  • one completion event exists,
  • one final lifecycle transition exists.

Use row locking, compare-and-update semantics, or another database-backed mechanism consistent with the repository.

Concurrent Miner Changes

A simultaneous lifecycle-changing operation must not leave:

  • a completed repair with miner still in in_repair,
  • an active repair with miner already returned,
  • conflicting lifecycle history.

Lock ordering must be consistent with Begin Repair and other miner workflows to reduce deadlock risk.

Document the chosen lock order in code comments only where it is not obvious.

Error and Failure Behaviour

On any failure after the transaction starts:

  • no partial completion fields persist,
  • miner lifecycle remains unchanged,
  • no completion event persists,
  • repair remains active.

Unexpected database integrity failures should be translated using established service/repository exception conventions.

Do not catch broad exceptions merely to convert them into 409.

Tests

Add focused tests following current test organization.

Authorization Tests

Verify:

  • unauthenticated request is denied,
  • user without repairs.complete is denied,
  • Read Only / Viewer is denied,
  • Technician is allowed,
  • Administrator is allowed,
  • existing repairs.read behavior is unchanged,
  • existing repairs.create behavior is unchanged.

Happy Path Tests

Verify:

  • active repair completes successfully,
  • miner transitions from in_repair to requested eligible lifecycle,
  • repair is marked completed,
  • completion timestamp is server-controlled,
  • completing actor is authenticated user,
  • optional summary is persisted,
  • blank summary normalizes to null,
  • completion event is appended,
  • event sequence follows existing events,
  • response represents the completed repair.

Test at least:

  • return to active,
  • return to inactive,
  • restoration to a valid original lifecycle when relevant.

Domain Conflict Tests

Verify 409 for:

  • already completed repair,
  • inactive/non-active repair,
  • miner not in in_repair,
  • mismatched active repair state,
  • disallowed destination lifecycle,
  • duplicate concurrent completion.

Validation Tests

Verify 422 for:

  • malformed repair UUID,
  • missing required lifecycle,
  • unknown lifecycle,
  • in_repair as destination,
  • summary over maximum length,
  • invalid request shape.

If lifecycle defaulting is intentionally supported, replace the missing-field expectation with tests for documented default behavior.

Missing Resource Test

Verify 404 for unknown repair UUID.

Transaction Rollback Tests

Force failures at meaningful steps, such as:

  • miner update failure,
  • repair completion update failure,
  • event insertion failure.

Verify no partial state persists.

Event Tests

Verify:

  • exactly one completion event,
  • immutable event behavior remains,
  • actor is correct,
  • payload is structured,
  • ordering is explicit,
  • prior events remain unchanged.

Database Constraint Tests

Verify applicable constraints directly, including:

  • completed record requirements,
  • active/completed consistency,
  • resulting lifecycle restriction,
  • one-active-repair rule remains valid.

Migration Tests

Verify:

  • previous head upgrades,
  • new permission exists,
  • intended roles receive the permission,
  • unintended roles do not,
  • schema constraints exist,
  • downgrade works safely or refuses explicitly according to project convention,
  • re-upgrade works,
  • empty database upgrades through head.

Routing Tests

Verify:

  • static repair routes such as /api/repairs/queue and /api/repairs/parts-summary still resolve,
  • /api/repairs/{repair_id}/complete resolves correctly,
  • UUID detail and event routes remain unchanged,
  • malformed static-looking paths are not accidentally consumed by UUID routes,
  • expected route count increases only by the intended amount.

Regression Tests

Run at minimum:

  • Begin Repair tests,
  • Repair Timeline tests,
  • inventory lifecycle tests,
  • permission tests,
  • migration lifecycle suite,
  • relevant legacy repair tests,
  • complete project suite if practical.

Documentation Updates

After implementation and validation succeed:

  • mark Return to Service complete in docs/releases/v0.2-repair-workflow.md,
  • update docs/ROADMAP.md only if the repository uses it as current implementation status,
  • update API documentation if maintained separately.

Do not mark Control Board Replacement or Repair Parts Tracking complete.

Do not perform broad stale-document cleanup in this task unless a directly changed statement would become materially false.

Acceptance Criteria

The task is complete when all of the following are true:

  1. POST /api/repairs/{repair_id}/complete exists.
  2. Authentication and repairs.complete are enforced.
  3. Only an active repair can be completed.
  4. The associated miner must be in in_repair.
  5. The target lifecycle is explicit and validated.
  6. Repair completion and miner transition are atomic.
  7. Exactly one immutable completion event is appended.
  8. Concurrent completion cannot succeed twice.
  9. Existing repair static and UUID routes still resolve.
  10. Existing Begin Repair and Repair Timeline behavior remains unchanged.
  11. Migration lifecycle validation passes.
  12. Permission assignments are correct.
  13. Focused and regression tests pass.
  14. git diff --check passes.
  15. No unrelated files are changed.
  16. No commit or push is performed.

Validation Commands

Use repository-native commands where available.

At minimum run:

python -m compileall .

Run shell syntax checks for changed shell files, if any.

Run:

  • focused Return to Service tests,
  • Begin Repair tests,
  • Repair Timeline tests,
  • inventory lifecycle tests,
  • permission tests,
  • migration tests,
  • route audits,
  • architecture audits,
  • full suite when practical.

Validate migrations through:

  • previous head to new head,
  • downgrade,
  • re-upgrade,
  • empty database to head,
  • disposable development baseline.

Run:

git diff --check
git status --short

Architecture Audit

Confirm:

  • no SQL in routers,
  • no database driver imports in routers,
  • no transaction management in routers,
  • no FastAPI imports in services or repositories,
  • no generic repair update repository introduced,
  • actor identity comes from authentication,
  • timestamps are server-controlled,
  • events are append-only,
  • route ordering is safe.

Completion Report

Return:

  • branch and starting commit,
  • files created,
  • files modified,
  • migration revision and parent,
  • route added,
  • permission added and role assignments,
  • domain behavior,
  • concurrency strategy,
  • event type and payload,
  • tests added,
  • focused test results,
  • regression results,
  • migration validation results,
  • architecture audit results,
  • git diff --check,
  • known limitations,
  • final git status --short.

Do not commit. Do not push.