dev: add deterministic demo dataset
This commit is contained in:
1 parent
60a831950c
commit
d06ee7bcc6
5 files changed
+810
No files matched your search
@@ -142,6 +142,9 @@ def _map_content(can_manage_locations: bool = False) -> str:
|
|||||||
<section class="mine-map-workspace" id="mapWorkspace" hidden>
|
<section class="mine-map-workspace" id="mapWorkspace" hidden>
|
||||||
<div class="map-main-column">
|
<div class="map-main-column">
|
||||||
<div id="mapLegend" class="map-legend" aria-label="Map legend"></div>
|
<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="mapNotice" class="map-notice" role="status" hidden></div>
|
||||||
<div id="mapViewport" class="map-viewport-v1"
|
<div id="mapViewport" class="map-viewport-v1"
|
||||||
aria-label="Room racks, shelves, and positions"></div>
|
aria-label="Room racks, shelves, and positions"></div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import re
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
@@ -157,6 +158,31 @@ class FrontendShellTests(unittest.TestCase):
|
|||||||
self.assertIn(value, response.text)
|
self.assertIn(value, response.text)
|
||||||
self.assertNotIn("Live layout and telemetry data are not", 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):
|
def test_data_views_include_all_request_states(self):
|
||||||
self.authenticate()
|
self.authenticate()
|
||||||
for path in ("/operations/repairs", "/assets/miners"):
|
for path in ("/operations/repairs", "/assets/miners"):
|
||||||
|
|||||||
Executable
+107
@@ -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."
|
||||||
Executable
+173
@@ -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."
|
||||||
@@ -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;
|
||||||
Reference in new issue
Block a user