dev: add deterministic demo dataset
This commit is contained in:
1 parent
60a831950c
commit
d06ee7bcc6
5 files changed
+810
No files matched your search
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."
|
||||
Reference in new issue
Block a user