feat: add return to service workflow
This commit is contained in:
1 parent
72ad19a071
commit
f2b736b08e
19 files changed
+2883
-46
No files matched your search
@@ -0,0 +1,253 @@
|
||||
"""add return to service workflow
|
||||
|
||||
Revision ID: 0012_return_to_service
|
||||
Revises: 0011_repair_timeline
|
||||
Create Date: 2026-07-29
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
|
||||
revision: str = "0012_return_to_service"
|
||||
down_revision: Union[str, None] = "0011_repair_timeline"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
REPAIR_REASONS = (
|
||||
"HARDWARE_FAILURE",
|
||||
"LOW_HASHRATE",
|
||||
"CONTROL_BOARD",
|
||||
"HASHBOARD",
|
||||
"PSU",
|
||||
"FAN",
|
||||
"OVERHEATING",
|
||||
"NETWORKING",
|
||||
"PREVENTIVE_MAINTENANCE",
|
||||
"INSPECTION",
|
||||
"OTHER",
|
||||
)
|
||||
|
||||
|
||||
def _quoted(values: Sequence[str]) -> str:
|
||||
return ", ".join(f"'{value}'" for value in values)
|
||||
|
||||
|
||||
def _create_event_constraints() -> None:
|
||||
op.create_check_constraint(
|
||||
"ck_repair_events_type",
|
||||
"repair_events",
|
||||
"event_type IN ('REPAIR_STARTED', 'INITIAL_NOTE', "
|
||||
"'REPAIR_COMPLETED')",
|
||||
)
|
||||
op.create_check_constraint(
|
||||
"ck_repair_events_payload_shape",
|
||||
"repair_events",
|
||||
"("
|
||||
"event_type = 'REPAIR_STARTED' "
|
||||
"AND payload ?& ARRAY['reason', 'from_lifecycle', 'to_lifecycle'] "
|
||||
f"AND payload ->> 'reason' IN ({_quoted(REPAIR_REASONS)}) "
|
||||
"AND payload ->> 'from_lifecycle' IN ('active', 'spare') "
|
||||
"AND payload ->> 'to_lifecycle' = 'in_repair'"
|
||||
") OR ("
|
||||
"event_type = 'INITIAL_NOTE' "
|
||||
"AND jsonb_typeof(payload -> 'note') = 'string' "
|
||||
"AND trim(payload ->> 'note') <> ''"
|
||||
") OR ("
|
||||
"event_type = 'REPAIR_COMPLETED' "
|
||||
"AND payload ?& ARRAY["
|
||||
"'previous_miner_lifecycle', 'resulting_miner_lifecycle'"
|
||||
"] "
|
||||
"AND payload ->> 'previous_miner_lifecycle' = 'in_repair' "
|
||||
"AND payload ->> 'resulting_miner_lifecycle' "
|
||||
"IN ('active', 'spare') "
|
||||
"AND (NOT payload ? 'pre_repair_lifecycle' "
|
||||
"OR payload ->> 'pre_repair_lifecycle' IN ('active', 'spare')) "
|
||||
"AND (NOT payload ? 'completion_summary' OR ("
|
||||
"jsonb_typeof(payload -> 'completion_summary') = 'string' "
|
||||
"AND trim(payload ->> 'completion_summary') <> '' "
|
||||
"AND char_length(payload ->> 'completion_summary') <= 4000"
|
||||
"))"
|
||||
")",
|
||||
)
|
||||
|
||||
|
||||
def _create_previous_event_constraints() -> None:
|
||||
op.create_check_constraint(
|
||||
"ck_repair_events_type",
|
||||
"repair_events",
|
||||
"event_type IN ('REPAIR_STARTED', 'INITIAL_NOTE')",
|
||||
)
|
||||
op.create_check_constraint(
|
||||
"ck_repair_events_payload_shape",
|
||||
"repair_events",
|
||||
"("
|
||||
"event_type = 'REPAIR_STARTED' "
|
||||
"AND payload ?& ARRAY['reason', 'from_lifecycle', 'to_lifecycle'] "
|
||||
f"AND payload ->> 'reason' IN ({_quoted(REPAIR_REASONS)}) "
|
||||
"AND payload ->> 'from_lifecycle' IN ('active', 'spare') "
|
||||
"AND payload ->> 'to_lifecycle' = 'in_repair'"
|
||||
") OR ("
|
||||
"event_type = 'INITIAL_NOTE' "
|
||||
"AND jsonb_typeof(payload -> 'note') = 'string' "
|
||||
"AND trim(payload ->> 'note') <> ''"
|
||||
")",
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
connection = op.get_bind()
|
||||
|
||||
connection.exec_driver_sql(
|
||||
"""
|
||||
INSERT INTO permissions (key, description)
|
||||
VALUES ('repairs.complete', 'Return repaired miners to service')
|
||||
ON CONFLICT (key) DO UPDATE
|
||||
SET description = EXCLUDED.description
|
||||
"""
|
||||
)
|
||||
connection.exec_driver_sql(
|
||||
"""
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT roles.id, permissions.id
|
||||
FROM roles
|
||||
JOIN permissions ON permissions.key = 'repairs.complete'
|
||||
WHERE roles.name IN ('Admin', 'Manager', 'Technician')
|
||||
ON CONFLICT DO NOTHING
|
||||
"""
|
||||
)
|
||||
|
||||
op.add_column(
|
||||
"repairs",
|
||||
sa.Column("completion_summary", sa.Text(), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"repairs",
|
||||
sa.Column("resulting_lifecycle", sa.Text(), nullable=True),
|
||||
)
|
||||
op.create_check_constraint(
|
||||
"ck_repairs_resulting_lifecycle",
|
||||
"repairs",
|
||||
"resulting_lifecycle IS NULL "
|
||||
"OR resulting_lifecycle IN ('active', 'spare')",
|
||||
)
|
||||
op.create_check_constraint(
|
||||
"ck_repairs_completion_summary",
|
||||
"repairs",
|
||||
"completion_summary IS NULL OR ("
|
||||
"trim(completion_summary) <> '' "
|
||||
"AND char_length(completion_summary) <= 4000"
|
||||
")",
|
||||
)
|
||||
# NOT VALID preserves truthful legacy COMPLETED rows whose destination was
|
||||
# never recorded, while PostgreSQL still enforces the check for every new
|
||||
# or updated row after this migration.
|
||||
connection.exec_driver_sql(
|
||||
"""
|
||||
ALTER TABLE repairs
|
||||
ADD CONSTRAINT ck_repairs_completion_metadata_state
|
||||
CHECK (
|
||||
(
|
||||
status = 'COMPLETED'
|
||||
AND resulting_lifecycle IS NOT NULL
|
||||
)
|
||||
OR (
|
||||
status <> 'COMPLETED'
|
||||
AND completion_summary IS NULL
|
||||
AND resulting_lifecycle IS NULL
|
||||
)
|
||||
)
|
||||
NOT VALID
|
||||
"""
|
||||
)
|
||||
|
||||
op.drop_constraint(
|
||||
"ck_repair_events_payload_shape",
|
||||
"repair_events",
|
||||
type_="check",
|
||||
)
|
||||
op.drop_constraint(
|
||||
"ck_repair_events_type",
|
||||
"repair_events",
|
||||
type_="check",
|
||||
)
|
||||
_create_event_constraints()
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
connection = op.get_bind()
|
||||
|
||||
connection.exec_driver_sql(
|
||||
"""
|
||||
DO $$
|
||||
DECLARE
|
||||
completion_count BIGINT;
|
||||
BEGIN
|
||||
SELECT count(*)
|
||||
INTO completion_count
|
||||
FROM repairs
|
||||
WHERE resulting_lifecycle IS NOT NULL
|
||||
OR completion_summary IS NOT NULL;
|
||||
|
||||
completion_count := completion_count + (
|
||||
SELECT count(*)
|
||||
FROM repair_events
|
||||
WHERE event_type = 'REPAIR_COMPLETED'
|
||||
);
|
||||
|
||||
IF completion_count > 0 THEN
|
||||
RAISE EXCEPTION USING
|
||||
MESSAGE = format(
|
||||
'Cannot downgrade Return to Service: %s completion records or events would lose authoritative history',
|
||||
completion_count
|
||||
),
|
||||
HINT = 'Preserve or explicitly migrate Return to Service history before downgrading; MinerOps will not invent or discard lifecycle, actor, summary, or event values.';
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
"""
|
||||
)
|
||||
|
||||
op.drop_constraint(
|
||||
"ck_repair_events_payload_shape",
|
||||
"repair_events",
|
||||
type_="check",
|
||||
)
|
||||
op.drop_constraint(
|
||||
"ck_repair_events_type",
|
||||
"repair_events",
|
||||
type_="check",
|
||||
)
|
||||
_create_previous_event_constraints()
|
||||
|
||||
op.drop_constraint(
|
||||
"ck_repairs_completion_metadata_state",
|
||||
"repairs",
|
||||
type_="check",
|
||||
)
|
||||
op.drop_constraint(
|
||||
"ck_repairs_completion_summary",
|
||||
"repairs",
|
||||
type_="check",
|
||||
)
|
||||
op.drop_constraint(
|
||||
"ck_repairs_resulting_lifecycle",
|
||||
"repairs",
|
||||
type_="check",
|
||||
)
|
||||
op.drop_column("repairs", "resulting_lifecycle")
|
||||
op.drop_column("repairs", "completion_summary")
|
||||
|
||||
connection.exec_driver_sql(
|
||||
"""
|
||||
DELETE FROM role_permissions
|
||||
WHERE permission_id = (
|
||||
SELECT id FROM permissions WHERE key = 'repairs.complete'
|
||||
)
|
||||
"""
|
||||
)
|
||||
connection.exec_driver_sql(
|
||||
"DELETE FROM permissions WHERE key = 'repairs.complete'"
|
||||
)
|
||||
Reference in new issue
Block a user