"""Set-based PostgreSQL reads for Mine Map v1.""" from collections.abc import Callable, Iterator from contextlib import contextmanager from typing import Any from uuid import UUID import psycopg from psycopg.rows import dict_row from app.config import settings ConnectionFactory = Callable[[], psycopg.Connection] ACTIVE_REPAIR_STATUSES = ( "OPEN", "IN_PROGRESS", "WAITING_FOR_PARTS", "READY_FOR_TEST", ) def default_connection_factory() -> psycopg.Connection: return psycopg.connect( host=settings.postgres_host, port=settings.postgres_port, dbname=settings.postgres_db, user=settings.postgres_user, password=settings.postgres_password, connect_timeout=5, ) LATEST_TELEMETRY_SQL = """ LEFT JOIN LATERAL ( SELECT snapshots.observed_at, ROUND((snapshots.mhs_av / 1000000.0)::numeric, 2)::float AS hashrate_ths, ROUND(snapshots.power_w::numeric, 2)::float AS power_w, ROUND(snapshots.chip_temp_max_c::numeric, 2)::float AS chip_temp_max_c, ROUND(snapshots.chip_temp_avg_c::numeric, 2)::float AS chip_temp_avg_c, snapshots.hash_stable, snapshots.pool_rejected_percent FROM miner_status_snapshots snapshots WHERE snapshots.miner_id = miners.id ORDER BY snapshots.observed_at DESC, snapshots.id DESC LIMIT 1 ) latest ON true """ ACTIVE_REPAIR_SQL = """ LEFT JOIN LATERAL ( SELECT repairs.id, repairs.status, repairs.reason FROM repairs WHERE repairs.miner_id = miners.id AND repairs.status = ANY(%s) ORDER BY repairs.opened_at, repairs.id LIMIT 1 ) active_repair ON true """ class LocationMapRepository: """Own all persistence used by the Mine Map read model.""" def __init__( self, connection_factory: ConnectionFactory | None = None, *, connection: psycopg.Connection | None = None, ): self._connection_factory = connection_factory or default_connection_factory self._connection = connection @contextmanager def _connection_scope(self) -> Iterator[psycopg.Connection]: if self._connection is not None: yield self._connection return connection = self._connection_factory() try: yield connection finally: connection.close() def read_map( self, *, site_id: UUID | None = None, room_id: UUID | None = None, ) -> dict[str, list[dict[str, Any]]]: position_where = [] position_params: list[Any] = [list(ACTIVE_REPAIR_STATUSES)] virtual_where = [ "COALESCE(mls.asset_state, 'waiting_placement') = ANY(%s)", "NOT EXISTS (SELECT 1 FROM location_positions assigned " "WHERE assigned.miner_id = miners.id)", ] virtual_params: list[Any] = [ list(ACTIVE_REPAIR_STATUSES), ["waiting_placement", "storage", "repair", "decommissioned"], ] if site_id is not None: position_where.append("rooms.site_id = %s") position_params.append(site_id) virtual_where.append("miners.site_id = %s") virtual_params.append(site_id) if room_id is not None: position_where.append("lp.room_id = %s") position_params.append(room_id) virtual_where.append("miners.room_id = %s") virtual_params.append(room_id) position_filter = ( "WHERE " + " AND ".join(position_where) if position_where else "" ) virtual_filter = "WHERE " + " AND ".join(virtual_where) with self._connection_scope() as connection: with connection.cursor(row_factory=dict_row) as cur: cur.execute("SELECT id, name FROM sites ORDER BY lower(name), id") sites = [dict(row) for row in cur.fetchall()] cur.execute( """ SELECT rooms.id, rooms.site_id, sites.name AS site_name, rooms.name, rooms.sort_order FROM rooms JOIN sites ON sites.id = rooms.site_id ORDER BY lower(sites.name), rooms.sort_order, lower(rooms.name), rooms.id """ ) rooms = [dict(row) for row in cur.fetchall()] cur.execute( f""" SELECT lp.id AS location_position_id, sites.id AS site_id, sites.name AS site_name, rooms.id AS room_id, COALESCE(rooms.name, lp.room_name) AS room_name, lp.rack_name, lp.shelf_name, lp.shelf_index, lp.position_index, lp.position_label, lp.position_kind, lp.position_status, (miners.id IS NOT NULL) AS occupied, lp.expected_mac, lp.equipment_label, lp.notes, lp.source_file, lp.source_cell, lp.conflict_reason, miners.id AS miner_id, miners.asset_tag AS asset_id, miners.current_mac, host(miners.current_ip) AS current_ip, miners.status AS miner_status, miners.hostname, miners.miner_sn, miners.power_sn, miners.model, miners.miner_type, miners.firmware_version, miners.lifecycle_status, miners.last_seen_at, COALESCE(mls.asset_state, 'unassigned') AS asset_state, latest.observed_at, latest.hashrate_ths, latest.power_w, latest.chip_temp_max_c, latest.chip_temp_avg_c, latest.hash_stable, latest.pool_rejected_percent, active_repair.id AS active_repair_id, active_repair.status AS active_repair_status, active_repair.reason AS active_repair_reason FROM location_positions lp LEFT JOIN rooms ON rooms.id = lp.room_id LEFT JOIN sites ON sites.id = rooms.site_id LEFT JOIN miners ON miners.id = lp.miner_id LEFT JOIN miner_location_states mls ON mls.miner_id = miners.id {LATEST_TELEMETRY_SQL} {ACTIVE_REPAIR_SQL} {position_filter} ORDER BY lower(sites.name) NULLS LAST, rooms.sort_order NULLS LAST, lower(COALESCE(rooms.name, lp.room_name)), lower(lp.rack_name), lp.shelf_index NULLS LAST, lower(lp.shelf_name), lp.position_index, lp.id """, position_params, ) positions = [dict(row) for row in cur.fetchall()] cur.execute( f""" SELECT miners.id AS miner_id, miners.site_id, miners.room_id, miners.asset_tag AS asset_id, COALESCE(mls.asset_state, 'waiting_placement') AS asset_state, miners.current_mac, host(miners.current_ip) AS current_ip, miners.status AS miner_status, miners.status, miners.hostname, miners.miner_sn, miners.power_sn, miners.model, miners.miner_type, miners.firmware_version, miners.lifecycle_status, miners.last_seen_at, latest.observed_at, latest.hashrate_ths, latest.power_w, latest.chip_temp_max_c, latest.chip_temp_avg_c, latest.hash_stable, active_repair.id AS active_repair_id, active_repair.status AS active_repair_status, active_repair.reason AS active_repair_reason FROM miners LEFT JOIN miner_location_states mls ON mls.miner_id = miners.id {LATEST_TELEMETRY_SQL} {ACTIVE_REPAIR_SQL} {virtual_filter} ORDER BY asset_state, lower(miners.asset_tag), miners.id """, virtual_params, ) virtual_miners = [dict(row) for row in cur.fetchall()] return { "sites": sites, "rooms": rooms, "positions": positions, "virtual_miners": virtual_miners, } def summarize_locations(self, *, include_miner_counts: bool) -> dict[str, Any]: miner_counts = ( "count(DISTINCT lp.miner_id)::integer AS mapped_miners, " "(SELECT count(*)::integer FROM miners WHERE NOT EXISTS (" "SELECT 1 FROM location_positions assigned " "WHERE assigned.miner_id = miners.id)) AS unmapped_miners" if include_miner_counts else "NULL::integer AS mapped_miners, NULL::integer AS unmapped_miners" ) with self._connection_scope() as connection: with connection.cursor(row_factory=dict_row) as cur: cur.execute( f""" SELECT (SELECT count(*)::integer FROM sites) AS sites, (SELECT count(*)::integer FROM rooms) AS rooms, count(lp.id)::integer AS positions, count(lp.id) FILTER ( WHERE lp.miner_id IS NOT NULL )::integer AS occupied_positions, count(lp.id) FILTER ( WHERE lp.miner_id IS NULL )::integer AS empty_positions, {miner_counts} FROM location_positions AS lp """ ) return dict(cur.fetchone()) def get_miner_location(self, miner_id: UUID) -> dict[str, Any] | None: """Return the one current physical position assigned to a miner.""" with self._connection_scope() as connection: with connection.cursor(row_factory=dict_row) as cur: cur.execute( """ SELECT sites.id AS site_id, sites.name AS site_name, rooms.id AS room_id, COALESCE(rooms.name, position.room_name) AS room_name, position.id AS position_id, position.rack_name AS rack_label, position.shelf_name AS shelf_label, position.position_label, position.position_index FROM location_positions AS position LEFT JOIN rooms ON rooms.id = position.room_id LEFT JOIN sites ON sites.id = rooms.site_id WHERE position.miner_id = %s ORDER BY position.id LIMIT 1 """, (miner_id,), ) row = cur.fetchone() return dict(row) if row else None