dev: add deterministic demo dataset

This commit is contained in:
lussierj committed 2026-08-10 21:33:50 +00:00
1 parent 60a831950c
commit d06ee7bcc6
5 files changed
+810

No files matched your search

+3
View File
@@ -142,6 +142,9 @@ def _map_content(can_manage_locations: bool = False) -> str:
<section class="mine-map-workspace" id="mapWorkspace" hidden>
<div class="map-main-column">
<div id="mapLegend" class="map-legend" aria-label="Map legend"></div>
<p id="mapPermissionNote" class="map-notice" hidden>
Miner details require inventory access. Layout and occupancy remain visible.
</p>
<div id="mapNotice" class="map-notice" role="status" hidden></div>
<div id="mapViewport" class="map-viewport-v1"
aria-label="Room racks, shelves, and positions"></div>
+26
View File
@@ -1,3 +1,4 @@
import re
import unittest
from pathlib import Path
from unittest.mock import patch
@@ -157,6 +158,31 @@ class FrontendShellTests(unittest.TestCase):
self.assertIn(value, response.text)
self.assertNotIn("Live layout and telemetry data are not", response.text)
def test_map_script_dom_contract_matches_rendered_page(self):
self.authenticate()
response = self.client.get("/monitoring/map")
javascript = (STATIC_ROOT / "mine-map.js").read_text(encoding="utf-8")
declared_ids = re.search(
r"const ids = \[(.*?)\];",
javascript,
re.DOTALL,
)
self.assertIsNotNone(declared_ids)
required = set(
re.findall(
r'"([A-Za-z][A-Za-z0-9]+)"',
declared_ids.group(1),
)
)
required.update(
re.findall(
r'document\.getElementById\("([^"]+)"\)',
javascript,
)
)
rendered = set(re.findall(r'\bid="([^"]+)"', response.text))
self.assertEqual(required - rendered, set())
def test_data_views_include_all_request_states(self):
self.authenticate()
for path in ("/operations/repairs", "/assets/miners"):
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${repo_root}"
env_file="${MINEROPS_DEMO_ENV_FILE:-.env.dev}"
project_name="${MINEROPS_DEMO_PROJECT:-minerops-dev}"
mode="${1:-seed}"
usage() {
cat <<'EOF'
Usage: scripts/seed-development-demo.sh [--reseed]
Seeds the current Docker development database with deterministic synthetic data.
--reseed reapplies the mutable demo projections without deleting immutable history.
EOF
}
case "${mode}" in
seed) [[ $# -eq 0 ]] || { usage >&2; exit 2; } ;;
--reseed) [[ $# -eq 1 ]] || { usage >&2; exit 2; } ;;
-h|--help) usage; exit 0 ;;
*) usage >&2; exit 2 ;;
esac
[[ -f "${env_file}" ]] || { echo "Missing development environment file: ${env_file}" >&2; exit 1; }
[[ -f compose.dev.yaml ]] || { echo "Missing compose.dev.yaml." >&2; exit 1; }
[[ -f sql/development-demo.sql ]] || { echo "Missing sql/development-demo.sql." >&2; exit 1; }
env_value() {
awk -F= -v wanted="$1" '
$1 == wanted {
sub(/^[^=]*=/, "")
gsub(/\r$/, "")
print
exit
}
' "${env_file}"
}
app_env="$(env_value APP_ENV)"
database_name="$(env_value POSTGRES_DB)"
database_host="$(env_value POSTGRES_HOST)"
if [[ "${app_env}" != "development" ]]; then
echo "Refusing demo seed: APP_ENV must be exactly development." >&2
exit 1
fi
if [[ ! "${database_name}" =~ (^|_)(dev|development|test)(_|$) ]]; then
echo "Refusing demo seed: POSTGRES_DB must be clearly named for development or testing." >&2
exit 1
fi
if [[ "${database_name,,}" =~ (prod|production|live) ]]; then
echo "Refusing demo seed: POSTGRES_DB resembles a production database." >&2
exit 1
fi
if [[ "${database_host}" != "postgres" ]]; then
echo "Refusing demo seed: POSTGRES_HOST must be the compose.dev.yaml postgres service." >&2
exit 1
fi
compose=(
docker compose
--project-name "${project_name}"
--env-file "${env_file}"
-f compose.dev.yaml
)
"${compose[@]}" config --quiet
if ! "${compose[@]}" ps --status running --services | grep -qx postgres; then
echo "Development PostgreSQL is not running for project ${project_name}." >&2
echo "Start it with: docker compose --project-name ${project_name} --env-file ${env_file} -f compose.dev.yaml up -d" >&2
exit 1
fi
migration_head="$(
"${compose[@]}" exec -T postgres sh -ec \
'psql -v ON_ERROR_STOP=1 -At -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "SELECT version_num FROM alembic_version"'
)"
if [[ "${migration_head}" != "0014_repair_parts_tracking" ]]; then
echo "Refusing demo seed: database Alembic head is ${migration_head:-missing}, expected 0014_repair_parts_tracking." >&2
exit 1
fi
if [[ "${mode}" == "--reseed" ]]; then
echo "Reapplying deterministic demo projections; immutable repair and MAC history will not be rewritten."
else
echo "Seeding deterministic MinerOps development demo data..."
fi
"${compose[@]}" exec -T postgres sh -ec \
'psql -v ON_ERROR_STOP=1 -v app_env=development -U "$POSTGRES_USER" -d "$POSTGRES_DB"' \
< sql/development-demo.sql
"${compose[@]}" exec -T postgres sh -ec \
'psql -v ON_ERROR_STOP=1 -At -F " | " -U "$POSTGRES_USER" -d "$POSTGRES_DB"' <<'SQL'
SELECT
'demo summary',
count(*) FILTER (WHERE asset_tag LIKE 'DEMO-MIN-%') || ' miners',
(SELECT count(*) FROM location_positions WHERE source_file = 'MinerOps demo seed') || ' positions',
(SELECT count(*) FROM location_positions WHERE source_file = 'MinerOps demo seed' AND miner_id IS NULL) || ' empty',
(SELECT count(*) FROM repairs WHERE id::text LIKE 'd4000000-0000-4000-8000-%') || ' repairs'
FROM miners;
SQL
echo "Demo data is ready at the configured development gateway."
+173
View File
@@ -0,0 +1,173 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${repo_root}"
project_name="${MINEROPS_DEMO_TEST_PROJECT:-minerops-demo-test-$$}"
dev_port="${MINEROPS_DEMO_TEST_PORT:-$((21000 + ($$ % 10000)))}"
base_url="http://127.0.0.1:${dev_port}"
tmp_prefix="/tmp/${project_name}"
admin_cookie="${tmp_prefix}-admin.cookies"
compose=(
docker compose
--project-name "${project_name}"
--env-file .env.dev
-f compose.dev.yaml
)
cleanup() {
status=$?
if (( status != 0 )); then
echo
echo "Development demo test failed. Recent service logs:"
"${compose[@]}" logs --no-color --tail 160 || true
fi
rm -f "${tmp_prefix}"-*
"${compose[@]}" down --volumes --remove-orphans >/dev/null 2>&1 || true
exit "${status}"
}
trap cleanup EXIT
status_for() {
local path="$1"
curl --silent --output /dev/null --write-out '%{http_code}' \
-b "${admin_cookie}" -c "${admin_cookie}" "${base_url}${path}"
}
printf '%s\n' 'APP_ENV=production' > "${tmp_prefix}-production.env"
if MINEROPS_DEMO_ENV_FILE="${tmp_prefix}-production.env" \
./scripts/seed-development-demo.sh >"${tmp_prefix}-safety.out" 2>&1; then
echo "Demo seed accepted a production environment." >&2
exit 1
fi
grep -q 'APP_ENV must be exactly development' "${tmp_prefix}-safety.out"
echo "Starting disposable MinerOps data services..."
MINEROPS_DEV_PORT="${dev_port}" "${compose[@]}" up --detach postgres redis
for attempt in $(seq 1 180); do
postgres_ready=false
redis_ready=false
if "${compose[@]}" exec -T postgres sh -ec \
'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atqc "SELECT 1"' \
2>/dev/null | grep -qx 1; then
postgres_ready=true
fi
if "${compose[@]}" exec -T redis redis-cli ping 2>/dev/null | grep -qx PONG; then
redis_ready=true
fi
if [[ "${postgres_ready}" == "true" && "${redis_ready}" == "true" ]]; then
break
fi
(( attempt < 180 )) || { echo "Disposable data services did not become ready." >&2; exit 1; }
sleep 1
done
echo "Building and starting the disposable backend..."
MINEROPS_DEV_PORT="${dev_port}" "${compose[@]}" up --detach --build --no-deps backend
backend_container="$("${compose[@]}" ps -q backend)"
for attempt in $(seq 1 180); do
backend_health="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "${backend_container}")"
[[ "${backend_health}" == "healthy" ]] && break
[[ "${backend_health}" != "exited" && "${backend_health}" != "dead" ]] || { echo "Disposable backend exited." >&2; exit 1; }
(( attempt < 180 )) || { echo "Disposable backend did not become ready." >&2; exit 1; }
sleep 1
done
echo "Starting the disposable gateway..."
MINEROPS_DEV_PORT="${dev_port}" "${compose[@]}" up --detach --no-deps gateway
for attempt in $(seq 1 90); do
if curl --fail --silent "${base_url}/api/health" >"${tmp_prefix}-health.json"; then
break
fi
(( attempt < 90 )) || { echo "Disposable gateway did not become ready." >&2; exit 1; }
sleep 1
done
echo "Creating disposable development administrator..."
curl --fail --silent -c "${admin_cookie}" \
-H 'Content-Type: application/json' \
--data '{"username":"admin","display_name":"Test Administrator","email":"admin@example.com","password":"MinerOpsTest!123"}' \
"${base_url}/api/auth/setup-admin" >"${tmp_prefix}-setup.json"
grep -q '"role":"Admin"' "${tmp_prefix}-setup.json"
echo "Seeding and reapplying the deterministic fixture..."
MINEROPS_DEMO_PROJECT="${project_name}" ./scripts/seed-development-demo.sh
MINEROPS_DEMO_PROJECT="${project_name}" ./scripts/seed-development-demo.sh
MINEROPS_DEMO_PROJECT="${project_name}" ./scripts/seed-development-demo.sh --reseed
counts="$(
"${compose[@]}" exec -T postgres sh -ec \
'psql -v ON_ERROR_STOP=1 -At -F "|" -U "$POSTGRES_USER" -d "$POSTGRES_DB"' <<'SQL'
SELECT
count(*) FILTER (WHERE asset_tag LIKE 'DEMO-MIN-%'),
(SELECT count(*) FROM location_positions WHERE source_file = 'MinerOps demo seed'),
(SELECT count(*) FROM location_positions WHERE source_file = 'MinerOps demo seed' AND miner_id IS NULL),
(SELECT count(*) FROM repairs WHERE id::text LIKE 'd4000000-0000-4000-8000-%'),
(SELECT count(*) FROM repairs WHERE id::text LIKE 'd4000000-0000-4000-8000-%' AND status IN ('OPEN', 'IN_PROGRESS', 'WAITING_FOR_PARTS', 'READY_FOR_TEST')),
(SELECT count(*) FROM repair_events WHERE id::text LIKE 'd5000000-0000-4000-8000-%'),
(SELECT count(*) FROM miner_status_snapshots WHERE id::text LIKE 'd3000000-0000-4000-8000-%')
FROM miners;
SQL
)"
[[ "${counts}" == "30|50|23|4|3|8|23" ]]
echo "Checking live authenticated pages and APIs..."
for path in /dashboard /assets/miners /monitoring/map /operations/repairs; do
[[ "$(status_for "${path}")" == "200" ]]
done
curl --fail --silent -b "${admin_cookie}" "${base_url}/monitoring/map" >"${tmp_prefix}-map.html"
curl --fail --silent -b "${admin_cookie}" "${base_url}/assets/minerops-mine-map.js" >"${tmp_prefix}-map.js"
curl --fail --silent -b "${admin_cookie}" "${base_url}/api/location/map" >"${tmp_prefix}-map.json"
curl --fail --silent -b "${admin_cookie}" "${base_url}/api/inventory/miners?limit=100" >"${tmp_prefix}-miners.json"
curl --fail --silent -b "${admin_cookie}" "${base_url}/api/repairs/queue" >"${tmp_prefix}-repairs.json"
for id in mapSite mapRoom mapLayer mapPermissionNote mapViewport mapInspector; do
grep -q "id=\"${id}\"" "${tmp_prefix}-map.html"
done
for layer in temperature hashrate power repair; do
grep -q "\"${layer}\"" "${tmp_prefix}-map.js"
done
! grep -q "Cannot set properties of null" "${tmp_prefix}-map.html"
python3 - "${tmp_prefix}-map.json" "${tmp_prefix}-miners.json" "${tmp_prefix}-repairs.json" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as handle:
mine_map = json.load(handle)
with open(sys.argv[2], encoding="utf-8") as handle:
miners = json.load(handle)
with open(sys.argv[3], encoding="utf-8") as handle:
repairs = json.load(handle)
assert mine_map["meta"]["miner_details_included"] is True
assert mine_map["meta"]["position_count"] == 50
assert len(mine_map["data"]["sites"]) >= 1
assert {room["name"] for room in mine_map["data"]["rooms"]} >= {"Demo Unit A", "Demo Unit B"}
positions = mine_map["data"]["positions"]
assert {position["rack_name"] for position in positions} >= {"A1", "A2", "A3", "B1", "B2"}
assert sum(position["occupied"] for position in positions) == 27
assert sum(not position["occupied"] for position in positions) == 23
assert any(position["active_repair"] for position in positions)
assert any(position["occupied"] and position["observed_at"] is None for position in positions)
assert any(position["hashrate_ths"] == 0 for position in positions)
assert len({position["model"] for position in positions if position["model"]}) == 6
assert len({position["chip_temp_max_c"] for position in positions if position["chip_temp_max_c"] is not None}) >= 5
assert len(miners["data"]) == 30
assert any(miner["asset_id"] == "DEMO-MIN-001" for miner in miners["data"])
assert repairs["data"]["summary"]["total"] == 3
assert {item["latest_event_status"] for item in repairs["data"]["items"]} == {"open", "waiting_parts", "testing"}
PY
for query in \
'query=DEMO-MIN-001' \
'query=DEMO-SERIAL-001' \
'query=02%3A00%3A00%3A00%3A00%3A01' \
'query=M60S'; do
response="$(curl --fail --silent -b "${admin_cookie}" "${base_url}/api/inventory/miners?${query}&limit=100")"
[[ "${response}" == *'"returned":'* ]]
[[ "${response}" != *'"returned":0'* ]]
done
echo "Development demo seed and live UI/API smoke checks passed."
+501
View File
@@ -0,0 +1,501 @@
\set ON_ERROR_STOP on
BEGIN;
SET CONSTRAINTS ALL DEFERRED;
SELECT set_config('minerops.seed_environment', :'app_env', true);
DO $$
BEGIN
IF current_setting('minerops.seed_environment', true) <> 'development' THEN
RAISE EXCEPTION 'MinerOps demo seed is restricted to APP_ENV=development';
END IF;
IF current_database() !~* '(^|_)(dev|development|test)(_|$)' THEN
RAISE EXCEPTION 'MinerOps demo seed requires a development or test database name; got %', current_database();
END IF;
END $$;
CREATE TEMP TABLE demo_seed_context ON COMMIT DROP AS
SELECT users.id AS actor_id
FROM users
JOIN roles ON roles.id = users.role_id
WHERE users.is_active AND roles.name = 'Admin'
ORDER BY users.created_at, users.id
LIMIT 1;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM demo_seed_context) THEN
RAISE EXCEPTION 'Create an active Admin account before seeding MinerOps demo data';
END IF;
END $$;
INSERT INTO sites (id, name, description)
VALUES (
'd0000000-0000-4000-8000-000000000001',
'Demo Mining Site',
'Synthetic development-only MinerOps fixture. Never use as production data.'
)
ON CONFLICT (id) DO UPDATE SET
name = EXCLUDED.name,
description = EXCLUDED.description,
updated_at = now();
INSERT INTO rooms (id, site_id, name, description, sort_order)
VALUES
(
'd0000000-0000-4000-8000-000000000101',
'd0000000-0000-4000-8000-000000000001',
'Demo Unit A',
'Synthetic room with racks A1 through A3.',
10
),
(
'd0000000-0000-4000-8000-000000000102',
'd0000000-0000-4000-8000-000000000001',
'Demo Unit B',
'Synthetic room with racks B1 and B2.',
20
)
ON CONFLICT (id) DO UPDATE SET
site_id = EXCLUDED.site_id,
name = EXCLUDED.name,
description = EXCLUDED.description,
sort_order = EXCLUDED.sort_order,
updated_at = now();
WITH demo_miners AS (
SELECT
i,
('d1000000-0000-4000-8000-' || lpad(i::text, 12, '0'))::uuid AS id,
CASE
WHEN i <= 18 THEN 'd0000000-0000-4000-8000-000000000101'::uuid
WHEN i <= 27 THEN 'd0000000-0000-4000-8000-000000000102'::uuid
ELSE NULL
END AS room_id,
(ARRAY['M30S', 'M30S+', 'M50', 'M50S', 'M60', 'M60S'])[((i - 1) % 6) + 1] AS model,
format('02:00:00:00:00:%s', upper(lpad(to_hex(i), 2, '0'))) AS mac,
CASE
WHEN i IN (21, 22, 23) THEN 'in_repair'
WHEN i IN (25, 26, 29) THEN 'spare'
WHEN i = 28 THEN 'retired'
ELSE 'active'
END AS lifecycle_status,
CASE
WHEN i IN (6, 12, 21, 22, 23, 24) THEN 'offline'
WHEN i IN (5, 10, 20, 25, 28, 29, 30) THEN 'unknown'
ELSE 'online'
END AS runtime_status,
CASE
WHEN i IN (5, 10, 20, 25, 28, 29, 30) THEN NULL
WHEN i = 19 THEN now() - interval '3 days'
WHEN i IN (21, 22, 23) THEN now() - make_interval(hours => i - 17)
ELSE now() - make_interval(mins => i * 3)
END AS observed_at
FROM generate_series(1, 30) AS series(i)
)
INSERT INTO miners (
id, site_id, room_id, asset_tag, current_mac, current_ip, hostname,
miner_sn, power_sn, control_board_sn, model, miner_type, api_version,
firmware_version, platform, chip, status, lifecycle_status, notes,
first_seen_at, last_seen_at, created_at, updated_at
)
SELECT
id,
'd0000000-0000-4000-8000-000000000001',
room_id,
format('DEMO-MIN-%s', lpad(i::text, 3, '0')),
mac,
('192.0.2.' || i)::inet,
format('demo-miner-%s', lpad(i::text, 3, '0')),
format('DEMO-SERIAL-%s', lpad(i::text, 3, '0')),
format('DEMO-PSU-%s', lpad(i::text, 3, '0')),
format('DEMO-CB-%s', lpad(i::text, 3, '0')),
model,
'WhatsMiner',
'3.2-demo',
format('DEMO-FW-2026.%s', ((i - 1) % 4) + 1),
'Demo Platform',
'Synthetic ASIC',
runtime_status,
lifecycle_status,
'Synthetic development-only miner. No real fleet identity or measurements.',
now() - make_interval(days => 120 - i),
observed_at,
now() - make_interval(days => 120 - i),
now()
FROM demo_miners
ON CONFLICT (id) DO UPDATE SET
site_id = EXCLUDED.site_id,
room_id = EXCLUDED.room_id,
asset_tag = EXCLUDED.asset_tag,
current_mac = EXCLUDED.current_mac,
current_ip = EXCLUDED.current_ip,
hostname = EXCLUDED.hostname,
miner_sn = EXCLUDED.miner_sn,
power_sn = EXCLUDED.power_sn,
control_board_sn = EXCLUDED.control_board_sn,
model = EXCLUDED.model,
miner_type = EXCLUDED.miner_type,
api_version = EXCLUDED.api_version,
firmware_version = EXCLUDED.firmware_version,
platform = EXCLUDED.platform,
chip = EXCLUDED.chip,
status = EXCLUDED.status,
lifecycle_status = EXCLUDED.lifecycle_status,
notes = EXCLUDED.notes,
first_seen_at = EXCLUDED.first_seen_at,
last_seen_at = EXCLUDED.last_seen_at,
updated_at = now();
WITH room_a AS (
SELECT
n AS ordinal,
'd0000000-0000-4000-8000-000000000101'::uuid AS room_id,
'Demo Unit A'::text AS room_name,
format('A%s', ((n - 1) / 10) + 1) AS rack_name,
((n - 1) % 10) / 5 + 1 AS shelf_index,
((n - 1) % 5) + 1 AS position_index,
CASE WHEN n <= 18 THEN n ELSE NULL END AS miner_number
FROM generate_series(1, 30) AS positions(n)
),
room_b AS (
SELECT
n + 30 AS ordinal,
'd0000000-0000-4000-8000-000000000102'::uuid AS room_id,
'Demo Unit B'::text AS room_name,
format('B%s', ((n - 1) / 10) + 1) AS rack_name,
((n - 1) % 10) / 5 + 1 AS shelf_index,
((n - 1) % 5) + 1 AS position_index,
CASE WHEN n <= 9 THEN n + 18 ELSE NULL END AS miner_number
FROM generate_series(1, 20) AS positions(n)
),
positions AS (
SELECT * FROM room_a
UNION ALL
SELECT * FROM room_b
)
INSERT INTO location_positions (
id, room_id, room_name, rack_name, shelf_name, shelf_index,
position_index, position_label, position_kind, position_status,
expected_mac, miner_id, notes, source_file, source_sheet, created_at, updated_at
)
SELECT
('d2000000-0000-4000-8000-' || lpad(ordinal::text, 12, '0'))::uuid,
room_id,
room_name,
rack_name,
format('Shelf %s', shelf_index),
shelf_index,
position_index,
format('%s-S%s-P%s', rack_name, shelf_index, position_index),
'miner_slot',
CASE WHEN miner_number IS NULL THEN 'available' ELSE 'occupied' END,
CASE WHEN ordinal = 50 THEN '02:00:00:00:00:FE' ELSE NULL END,
CASE WHEN miner_number IS NULL THEN NULL ELSE
('d1000000-0000-4000-8000-' || lpad(miner_number::text, 12, '0'))::uuid
END,
'Synthetic development-only position.',
'MinerOps demo seed',
'Synthetic layout',
now(),
now()
FROM positions
ON CONFLICT (id) DO UPDATE SET
room_id = EXCLUDED.room_id,
room_name = EXCLUDED.room_name,
rack_name = EXCLUDED.rack_name,
shelf_name = EXCLUDED.shelf_name,
shelf_index = EXCLUDED.shelf_index,
position_index = EXCLUDED.position_index,
position_label = EXCLUDED.position_label,
position_kind = EXCLUDED.position_kind,
position_status = EXCLUDED.position_status,
expected_mac = EXCLUDED.expected_mac,
miner_id = EXCLUDED.miner_id,
notes = EXCLUDED.notes,
source_file = EXCLUDED.source_file,
source_sheet = EXCLUDED.source_sheet,
updated_at = now();
WITH demo_miners AS (
SELECT
i,
('d1000000-0000-4000-8000-' || lpad(i::text, 12, '0'))::uuid AS miner_id,
CASE
WHEN i <= 18 THEN i
WHEN i <= 27 THEN i + 12
ELSE NULL
END AS position_ordinal
FROM generate_series(1, 30) AS series(i)
)
INSERT INTO miner_location_states (
miner_id, asset_state, location_position_id, state_notes,
updated_by_user_id, updated_at
)
SELECT
miner_id,
CASE
WHEN i IN (21, 22, 23) THEN 'repair'
WHEN i IN (25, 26, 29) THEN 'storage'
WHEN i = 28 THEN 'decommissioned'
WHEN position_ordinal IS NULL THEN 'waiting_placement'
ELSE 'shelved'
END,
CASE WHEN position_ordinal IS NULL THEN NULL ELSE
('d2000000-0000-4000-8000-' || lpad(position_ordinal::text, 12, '0'))::uuid
END,
'Synthetic development-only location state.',
(SELECT actor_id FROM demo_seed_context),
now()
FROM demo_miners
ON CONFLICT (miner_id) DO UPDATE SET
asset_state = EXCLUDED.asset_state,
location_position_id = EXCLUDED.location_position_id,
state_notes = EXCLUDED.state_notes,
updated_by_user_id = EXCLUDED.updated_by_user_id,
updated_at = now();
WITH telemetry AS (
SELECT
i,
('d1000000-0000-4000-8000-' || lpad(i::text, 12, '0'))::uuid AS miner_id,
CASE
WHEN i = 19 THEN now() - interval '3 days'
WHEN i IN (21, 22, 23) THEN now() - make_interval(hours => i - 17)
ELSE now() - make_interval(mins => i * 3)
END AS observed_at,
CASE ((i - 1) % 6)
WHEN 0 THEN 90
WHEN 1 THEN 100
WHEN 2 THEN 112
WHEN 3 THEN 126
WHEN 4 THEN 170
ELSE 186
END::numeric AS nominal_ths,
i IN (6, 12, 21, 22, 23, 24) AS stopped
FROM generate_series(1, 30) AS series(i)
WHERE i NOT IN (5, 10, 20, 25, 28, 29, 30)
)
INSERT INTO miner_status_snapshots (
id, miner_id, observed_ip, ip_address, mac_address, observed_at,
mhs_av, mhs_1m, mhs_15m, hs_rt, power_w, power_rate_w_per_th,
env_temp_c, chip_temp_min_c, chip_temp_max_c, chip_temp_avg_c,
fan_speed_in_rpm, fan_speed_out_rpm, uptime_seconds, elapsed_seconds,
hash_stable, pool_rejected_percent, raw_summary_json, created_at
)
SELECT
('d3000000-0000-4000-8000-' || lpad(i::text, 12, '0'))::uuid,
miner_id,
('192.0.2.' || i)::inet,
('192.0.2.' || i)::inet,
format('02:00:00:00:00:%s', upper(lpad(to_hex(i), 2, '0'))),
observed_at,
CASE WHEN stopped THEN 0 ELSE (nominal_ths + ((i % 5) - 2) * 0.7) * 1000000 END,
CASE WHEN stopped THEN 0 ELSE (nominal_ths + ((i % 4) - 1) * 0.6) * 1000000 END,
CASE WHEN stopped THEN 0 ELSE (nominal_ths + ((i % 3) - 1) * 0.4) * 1000000 END,
CASE WHEN stopped THEN 0 ELSE (nominal_ths + ((i % 5) - 2) * 0.7) * 1000000 END,
CASE WHEN stopped THEN 0 ELSE 2920 + nominal_ths * 4.8 + (i % 7) * 23 END,
CASE WHEN stopped THEN NULL ELSE round((28 + (i % 5) * 0.6)::numeric, 2) END,
24 + (i % 7),
48 + (i % 6) * 2,
58 + (i % 9) * 3,
54 + (i % 8) * 2,
CASE WHEN stopped THEN 0 ELSE 4700 + (i % 6) * 120 END,
CASE WHEN stopped THEN 0 ELSE 4950 + (i % 5) * 135 END,
CASE WHEN stopped THEN 0 ELSE 86400 + i * 731 END,
CASE WHEN stopped THEN 0 ELSE 86400 + i * 731 END,
CASE WHEN stopped THEN false WHEN i IN (9, 17) THEN false ELSE true END,
CASE WHEN stopped THEN 0 ELSE round((0.15 + (i % 6) * 0.08)::numeric, 2) END,
jsonb_build_object(
'fixture', 'MinerOps development demo',
'synthetic', true,
'model_index', i
),
observed_at
FROM telemetry
ON CONFLICT (id) DO UPDATE SET
observed_ip = EXCLUDED.observed_ip,
ip_address = EXCLUDED.ip_address,
mac_address = EXCLUDED.mac_address,
observed_at = EXCLUDED.observed_at,
mhs_av = EXCLUDED.mhs_av,
mhs_1m = EXCLUDED.mhs_1m,
mhs_15m = EXCLUDED.mhs_15m,
hs_rt = EXCLUDED.hs_rt,
power_w = EXCLUDED.power_w,
power_rate_w_per_th = EXCLUDED.power_rate_w_per_th,
env_temp_c = EXCLUDED.env_temp_c,
chip_temp_min_c = EXCLUDED.chip_temp_min_c,
chip_temp_max_c = EXCLUDED.chip_temp_max_c,
chip_temp_avg_c = EXCLUDED.chip_temp_avg_c,
fan_speed_in_rpm = EXCLUDED.fan_speed_in_rpm,
fan_speed_out_rpm = EXCLUDED.fan_speed_out_rpm,
uptime_seconds = EXCLUDED.uptime_seconds,
elapsed_seconds = EXCLUDED.elapsed_seconds,
hash_stable = EXCLUDED.hash_stable,
pool_rejected_percent = EXCLUDED.pool_rejected_percent,
raw_summary_json = EXCLUDED.raw_summary_json,
created_at = EXCLUDED.created_at;
INSERT INTO repairs (
id, miner_id, status, reason, opened_at, opened_by,
completed_at, completed_by, completion_summary, resulting_lifecycle
)
VALUES
(
'd4000000-0000-4000-8000-000000000001',
'd1000000-0000-4000-8000-000000000021',
'OPEN', 'CONTROL_BOARD', now() - interval '2 days',
(SELECT actor_id FROM demo_seed_context),
NULL, NULL, NULL, NULL
),
(
'd4000000-0000-4000-8000-000000000002',
'd1000000-0000-4000-8000-000000000022',
'WAITING_FOR_PARTS', 'PSU', now() - interval '5 days',
(SELECT actor_id FROM demo_seed_context),
NULL, NULL, NULL, NULL
),
(
'd4000000-0000-4000-8000-000000000003',
'd1000000-0000-4000-8000-000000000023',
'READY_FOR_TEST', 'HASHBOARD', now() - interval '1 day',
(SELECT actor_id FROM demo_seed_context),
NULL, NULL, NULL, NULL
),
(
'd4000000-0000-4000-8000-000000000004',
'd1000000-0000-4000-8000-000000000004',
'COMPLETED', 'PREVENTIVE_MAINTENANCE', now() - interval '21 days',
(SELECT actor_id FROM demo_seed_context),
now() - interval '20 days',
(SELECT actor_id FROM demo_seed_context),
'Synthetic scheduled inspection completed successfully.',
'active'
)
ON CONFLICT (id) DO NOTHING;
INSERT INTO repair_events (
id, repair_id, sequence, event_type, created_at, created_by, payload
)
VALUES
(
'd5000000-0000-4000-8000-000000000001',
'd4000000-0000-4000-8000-000000000001',
1, 'REPAIR_STARTED', now() - interval '2 days',
(SELECT actor_id FROM demo_seed_context),
jsonb_build_object('reason', 'CONTROL_BOARD', 'from_lifecycle', 'active', 'to_lifecycle', 'in_repair')
),
(
'd5000000-0000-4000-8000-000000000002',
'd4000000-0000-4000-8000-000000000001',
2, 'INITIAL_NOTE', now() - interval '2 days' + interval '10 minutes',
(SELECT actor_id FROM demo_seed_context),
jsonb_build_object('note', 'Synthetic intermittent control-board communication fault.')
),
(
'd5000000-0000-4000-8000-000000000003',
'd4000000-0000-4000-8000-000000000002',
1, 'REPAIR_STARTED', now() - interval '5 days',
(SELECT actor_id FROM demo_seed_context),
jsonb_build_object('reason', 'PSU', 'from_lifecycle', 'spare', 'to_lifecycle', 'in_repair')
),
(
'd5000000-0000-4000-8000-000000000004',
'd4000000-0000-4000-8000-000000000002',
2, 'INITIAL_NOTE', now() - interval '5 days' + interval '15 minutes',
(SELECT actor_id FROM demo_seed_context),
jsonb_build_object('note', 'Synthetic replacement PSU is awaiting delivery.')
),
(
'd5000000-0000-4000-8000-000000000005',
'd4000000-0000-4000-8000-000000000003',
1, 'REPAIR_STARTED', now() - interval '1 day',
(SELECT actor_id FROM demo_seed_context),
jsonb_build_object('reason', 'HASHBOARD', 'from_lifecycle', 'active', 'to_lifecycle', 'in_repair')
),
(
'd5000000-0000-4000-8000-000000000006',
'd4000000-0000-4000-8000-000000000003',
2, 'INITIAL_NOTE', now() - interval '1 day' + interval '20 minutes',
(SELECT actor_id FROM demo_seed_context),
jsonb_build_object('note', 'Synthetic hashboard repair passed bench checks and is ready for test.')
),
(
'd5000000-0000-4000-8000-000000000007',
'd4000000-0000-4000-8000-000000000004',
1, 'REPAIR_STARTED', now() - interval '21 days',
(SELECT actor_id FROM demo_seed_context),
jsonb_build_object('reason', 'PREVENTIVE_MAINTENANCE', 'from_lifecycle', 'active', 'to_lifecycle', 'in_repair')
),
(
'd5000000-0000-4000-8000-000000000008',
'd4000000-0000-4000-8000-000000000004',
2, 'REPAIR_COMPLETED', now() - interval '20 days',
(SELECT actor_id FROM demo_seed_context),
jsonb_build_object(
'previous_miner_lifecycle', 'in_repair',
'resulting_miner_lifecycle', 'active',
'pre_repair_lifecycle', 'active',
'completion_summary', 'Synthetic scheduled inspection completed successfully.'
)
)
ON CONFLICT (id) DO NOTHING;
INSERT INTO miner_maintenance_events (
id, miner_id, event_type, event_status, event_title, description,
event_at, created_by_user_id, created_at, updated_at
)
VALUES
(
'd6000000-0000-4000-8000-000000000001',
'd1000000-0000-4000-8000-000000000021',
'repair', 'open', 'Control board diagnostics',
'Synthetic repair projection. Intermittent control-board communication fault.',
now() - interval '2 days', (SELECT actor_id FROM demo_seed_context), now(), now()
),
(
'd6000000-0000-4000-8000-000000000002',
'd1000000-0000-4000-8000-000000000022',
'repair', 'waiting_parts', 'Awaiting replacement PSU',
'Synthetic repair projection. Replacement PSU is awaiting delivery.',
now() - interval '5 days', (SELECT actor_id FROM demo_seed_context), now(), now()
),
(
'd6000000-0000-4000-8000-000000000003',
'd1000000-0000-4000-8000-000000000023',
'repair', 'testing', 'Hashboard validation',
'Synthetic repair projection. Repaired hashboard is ready for a controlled test.',
now() - interval '1 day', (SELECT actor_id FROM demo_seed_context), now(), now()
),
(
'd6000000-0000-4000-8000-000000000004',
'd1000000-0000-4000-8000-000000000004',
'repair', 'completed', 'Scheduled inspection complete',
'Synthetic historical repair projection.',
now() - interval '20 days', (SELECT actor_id FROM demo_seed_context), now(), now()
)
ON CONFLICT (id) DO UPDATE SET
event_status = EXCLUDED.event_status,
event_title = EXCLUDED.event_title,
description = EXCLUDED.description,
event_at = EXCLUDED.event_at,
created_by_user_id = EXCLUDED.created_by_user_id,
updated_at = now();
DO $$
DECLARE
miner_count integer;
position_count integer;
repair_count integer;
BEGIN
SELECT count(*) INTO miner_count FROM miners WHERE asset_tag LIKE 'DEMO-MIN-%';
SELECT count(*) INTO position_count FROM location_positions WHERE source_file = 'MinerOps demo seed';
SELECT count(*) INTO repair_count FROM repairs WHERE id::text LIKE 'd4000000-0000-4000-8000-%';
IF miner_count <> 30 OR position_count <> 50 OR repair_count <> 4 THEN
RAISE EXCEPTION 'Demo seed verification failed: miners %, positions %, repairs %',
miner_count, position_count, repair_count;
END IF;
END $$;
COMMIT;