#!/usr/bin/env bash set -Eeuo pipefail cleanup() { docker compose --env-file .env.example --profile tools down -v --remove-orphans >/dev/null 2>&1 || true } trap cleanup EXIT cleanup docker compose --env-file .env.example up -d db bootstrap_log="$(mktemp)" set +e docker compose --env-file .env.example --profile tools run --build --rm migrate 2>&1 | tee "$bootstrap_log" bootstrap_status=${PIPESTATUS[0]} set -e if [ "$bootstrap_status" -eq 0 ]; then echo 'ERROR: expected historical one-shot reset guard on clean database.' >&2 exit 1 fi grep -Fq 'Production reset aborted: expected exactly one username admin, found 0' "$bootstrap_log" || { echo 'ERROR: migration chain failed before the expected historical reset guard.' >&2 exit 1 } rm -f "$bootstrap_log" docker compose --env-file .env.example exec -T db \ psql -v ON_ERROR_STOP=1 -U dhv2_owner -d dhv2 <<'SQL' INSERT INTO typeorm_migrations ("timestamp", name) SELECT 1788652800000, 'ResetProductionOperationalData1788652800000' WHERE NOT EXISTS ( SELECT 1 FROM typeorm_migrations WHERE name='ResetProductionOperationalData1788652800000' ); SQL docker compose --env-file .env.example --profile tools run --rm migrate # Prove the presentation preload is a physical tree, not a flattened list. docker compose --env-file .env.example exec -T db \ psql -v ON_ERROR_STOP=1 -U dhv2_owner -d dhv2 <<'SQL' DO $$ DECLARE invalid_areas integer; invalid_yacimientos integer; BEGIN SELECT COUNT(*) INTO invalid_areas FROM assets area JOIN asset_types type ON type.id=area.asset_type_id LEFT JOIN assets department ON department.id=area.parent_id LEFT JOIN asset_types department_type ON department_type.id=department.asset_type_id WHERE lower(type.code)='area' AND (department.id IS NULL OR lower(department_type.code)<>'departamento'); IF invalid_areas <> 0 THEN RAISE EXCEPTION 'Invalid Area→Departamento relations: %', invalid_areas; END IF; SELECT COUNT(*) INTO invalid_yacimientos FROM assets yacimiento JOIN asset_types type ON type.id=yacimiento.asset_type_id LEFT JOIN assets area ON area.id=yacimiento.parent_id LEFT JOIN asset_types area_type ON area_type.id=area.asset_type_id WHERE lower(type.code)='yacimiento' AND (area.id IS NULL OR lower(area_type.code)<>'area' OR yacimiento.operational_area_id IS DISTINCT FROM area.id); IF invalid_yacimientos <> 0 THEN RAISE EXCEPTION 'Invalid Yacimiento→Area relations: %', invalid_yacimientos; END IF; END $$; SQL export JWT_ACCESS_SECRET='CI_ACCESS_SECRET_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' export REFRESH_TOKEN_PEPPER='CI_REFRESH_PEPPER_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' export SMTP_SETTINGS_MASTER_KEY='' docker compose --env-file .env.example build api # Disposable CI identities. The real Inspector invariant requires an email. docker compose --env-file .env.example exec -T db \ psql -v ON_ERROR_STOP=1 -U dhv2_owner -d dhv2 <<'SQL' INSERT INTO users(id,username,email,password_hash,first_name,last_name,status,must_change_password) VALUES ('11111111-1111-4111-8111-111111111111','ci_planner','ci-planner@example.invalid','ci-unused','CI','Planner','ACTIVE',false), ('22222222-2222-4222-8222-222222222222','ci_inspector','ci-inspector@example.invalid','ci-unused','CI','Inspector','ACTIVE',false); INSERT INTO user_roles(user_id,role_id,assigned_by) SELECT '11111111-1111-4111-8111-111111111111'::uuid, role.id, '11111111-1111-4111-8111-111111111111'::uuid FROM roles role WHERE EXISTS ( SELECT 1 FROM role_permissions rp JOIN permissions p ON p.id=rp.permission_id WHERE rp.role_id=role.id AND p.code='inspections.manage' ) AND EXISTS ( SELECT 1 FROM role_permissions rp JOIN permissions p ON p.id=rp.permission_id WHERE rp.role_id=role.id AND p.code='inspections.assign' ) ORDER BY role.is_system DESC,role.code LIMIT 1; INSERT INTO user_roles(user_id,role_id,assigned_by) SELECT '22222222-2222-4222-8222-222222222222'::uuid, role.id, '11111111-1111-4111-8111-111111111111'::uuid FROM roles role WHERE EXISTS ( SELECT 1 FROM role_permissions rp JOIN permissions p ON p.id=rp.permission_id WHERE rp.role_id=role.id AND p.code='inspections.execute' ) ORDER BY (lower(role.code)='inspector') DESC,role.is_system DESC,role.code LIMIT 1; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM user_roles WHERE user_id='11111111-1111-4111-8111-111111111111'::uuid) THEN RAISE EXCEPTION 'CI planner role with manage+assign permissions not found'; END IF; IF NOT EXISTS (SELECT 1 FROM user_roles WHERE user_id='22222222-2222-4222-8222-222222222222'::uuid) THEN RAISE EXCEPTION 'CI inspector role with execute permission not found'; END IF; END $$; INSERT INTO auth_sessions( id,user_id,refresh_token_hash,expires_at,last_used_at,revoked_at,replaced_by_session_id,ip,user_agent,device_label ) VALUES ( '33333333-3333-4333-8333-333333333333', '11111111-1111-4111-8111-111111111111','ci-unused', CURRENT_TIMESTAMP + INTERVAL '10 minutes',CURRENT_TIMESTAMP,NULL,NULL,NULL,'CI','inspection-planning-smoke' ); SQL docker compose --env-file .env.example up -d api for _ in $(seq 1 30); do curl -fsS http://127.0.0.1:3101/api/v3/health >/dev/null 2>&1 && break sleep 2 done curl -fsS http://127.0.0.1:3101/api/v3/health >/dev/null TOKEN="$(docker compose --env-file .env.example exec -T api node - <<'NODE' const jwt = require('jsonwebtoken'); process.stdout.write(jwt.sign({ sub:'11111111-1111-4111-8111-111111111111', sid:'33333333-3333-4333-8333-333333333333', username:'ci_planner',typ:'access' }, process.env.JWT_ACCESS_SECRET, { algorithm:'HS256',expiresIn:300,issuer:'dhv2-api',audience:'dhv2' })); NODE )" CONTEXT="$(docker compose --env-file .env.example exec -T db \ psql -At -F '|' -U dhv2_owner -d dhv2 -c " SELECT department.id,area.id,yacimiento.id,relation.company_id FROM assets department JOIN asset_types department_type ON department_type.id=department.asset_type_id JOIN assets area ON area.parent_id=department.id JOIN asset_types area_type ON area_type.id=area.asset_type_id JOIN assets yacimiento ON yacimiento.parent_id=area.id JOIN asset_types yacimiento_type ON yacimiento_type.id=yacimiento.asset_type_id JOIN area_company_relations relation ON relation.area_id=area.id AND relation.relation_role='OPERATOR' AND relation.valid_until IS NULL WHERE lower(department_type.code)='departamento' AND lower(area_type.code)='area' AND lower(yacimiento_type.code)='yacimiento' ORDER BY department.name,area.name,yacimiento.name LIMIT 1; ")" IFS='|' read -r DEPARTMENT_ID AREA_ID YACIMIENTO_ID COMPANY_ID <<< "$CONTEXT" test -n "$DEPARTMENT_ID" && test -n "$AREA_ID" && test -n "$YACIMIENTO_ID" && test -n "$COMPANY_ID" curl -fsS -H "Authorization: Bearer $TOKEN" \ http://127.0.0.1:3101/api/v3/inspection-visits/planning-context/departments \ >/tmp/dhv2-departments.json grep -Fq "$DEPARTMENT_ID" /tmp/dhv2-departments.json curl -fsS -H "Authorization: Bearer $TOKEN" \ "http://127.0.0.1:3101/api/v3/inspection-visits/planning-context/departments/$DEPARTMENT_ID/areas" \ >/tmp/dhv2-areas.json grep -Fq "$AREA_ID" /tmp/dhv2-areas.json curl -fsS -H "Authorization: Bearer $TOKEN" \ "http://127.0.0.1:3101/api/v3/inspection-visits/planning-context/areas/$AREA_ID/yacimientos" \ >/tmp/dhv2-yacimientos.json grep -Fq "$YACIMIENTO_ID" /tmp/dhv2-yacimientos.json START_AT="$(date -u -d '+1 day' +'%Y-%m-%dT%H:%M:%S.000Z')" HTTP_CODE="$(curl -sS -o /tmp/dhv2-created.json -w '%{http_code}' \ -X POST \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d "{\"operationalAreaId\":\"$AREA_ID\",\"scopeAssetId\":\"$YACIMIENTO_ID\",\"operatorCompanyId\":\"$COMPANY_ID\",\"plannedStartAt\":\"$START_AT\",\"leadInspectorUserId\":\"22222222-2222-4222-8222-222222222222\"}" \ http://127.0.0.1:3101/api/v3/inspection-visits)" if [ "$HTTP_CODE" != 201 ]; then echo "ERROR: real inspection create returned HTTP $HTTP_CODE" >&2 cat /tmp/dhv2-created.json >&2 docker compose --env-file .env.example logs --no-color api >&2 || true exit 1 fi VISIT_ID="$(python - <<'PY' import json with open('/tmp/dhv2-created.json',encoding='utf-8') as f: print(json.load(f)['id']) PY )" VERIFY="$(docker compose --env-file .env.example exec -T db \ psql -At -U dhv2_owner -d dhv2 -c " SELECT COUNT(*) FROM inspection_visits visit JOIN assets yacimiento ON yacimiento.id=visit.scope_asset_id JOIN asset_types yacimiento_type ON yacimiento_type.id=yacimiento.asset_type_id WHERE visit.id='$VISIT_ID'::uuid AND visit.operational_area_id='$AREA_ID'::uuid AND visit.scope_asset_id='$YACIMIENTO_ID'::uuid AND visit.operator_company_id='$COMPANY_ID'::uuid AND yacimiento.parent_id=visit.operational_area_id AND lower(yacimiento_type.code)='yacimiento'; ")" if [ "$VERIFY" != '1' ]; then echo 'ERROR: created Inspection did not preserve Area/Yacimiento hierarchy.' >&2 cat /tmp/dhv2-created.json >&2 exit 1 fi echo "F6.1 real inspection create smoke: OK ($VISIT_ID)"