#!/usr/bin/env bash set -euo pipefail ACTION="${1:-deploy}" GRAFANA_CONTAINER="${GRAFANA_CONTAINER:-mv_ucepsa_grafana}" REPO_ROOT="${REGRESSION_DASHBOARD_REPO_ROOT:-/srv/mesavault/40-clients/ucepsa/edge-oee-demo}" DASHBOARD_FILE="${REGRESSION_DASHBOARD_FILE:-$REPO_ROOT/grafana/dashboards/ucepsa-shopfloor-context-health.dashboard.json}" FOLDER_UID="${REGRESSION_DASHBOARD_FOLDER_UID:-ucepsa-mesavault}" DASHBOARD_UID="ucepsa-shopfloor-context-health" DATASOURCE_UID="bfnbcasbm6hhca" resolve_url() { if [[ -n "${GRAFANA_URL:-}" ]]; then printf '%s' "$GRAFANA_URL" return fi local port_line host_port container_ip port_line="$( docker port "$GRAFANA_CONTAINER" 3000/tcp 2>/dev/null \ | head -n 1 || true )" if [[ -n "$port_line" ]]; then host_port="${port_line##*:}" printf 'http://127.0.0.1:%s' "$host_port" return fi container_ip="$( docker inspect "$GRAFANA_CONTAINER" \ --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' )" if [[ -z "$container_ip" ]]; then echo "ERROR: no se pudo resolver Grafana." >&2 exit 1 fi printf 'http://%s:3000' "$container_ip" } if [[ -z "${GRAFANA_API_TOKEN:-}" ]]; then echo "ERROR: falta GRAFANA_API_TOKEN." >&2 exit 1 fi export GRAFANA_URL_RESOLVED GRAFANA_URL_RESOLVED="$(resolve_url)" python3 - \ "$ACTION" \ "$DASHBOARD_FILE" \ "$FOLDER_UID" \ "$DASHBOARD_UID" \ "$DATASOURCE_UID" <<'PY' import json import os import sys import urllib.error import urllib.parse import urllib.request action, dashboard_file, folder_uid, dashboard_uid, datasource_uid = sys.argv[1:] base_url = os.environ["GRAFANA_URL_RESOLVED"].rstrip("/") token = os.environ["GRAFANA_API_TOKEN"] def request(method, path, payload=None): body = None headers = { "Accept": "application/json", "Authorization": f"Bearer {token}", } if payload is not None: body = json.dumps( payload, ensure_ascii=False, ).encode("utf-8") headers["Content-Type"] = "application/json" req = urllib.request.Request( base_url + path, data=body, headers=headers, method=method, ) try: with urllib.request.urlopen( req, timeout=20, ) as response: raw = response.read() return ( json.loads(raw.decode("utf-8")) if raw else None ) except urllib.error.HTTPError as exc: detail = exc.read().decode( "utf-8", errors="replace", ) raise RuntimeError( f"Grafana API {method} {path}: " f"HTTP {exc.code}: {detail}" ) from exc health = request("GET", "/api/health") if not health or health.get("database") != "ok": raise RuntimeError( f"Grafana no está saludable: {health!r}" ) if action == "deploy": with open( dashboard_file, encoding="utf-8", ) as handle: dashboard = json.load(handle) result = request( "POST", "/api/dashboards/db", { "dashboard": dashboard, "folderUid": folder_uid, "overwrite": True, "message": ( "MESAVAULT governance review UI " "SHADOW v0.3.14" ), }, ) print(json.dumps( { "status": "deployed", "dashboard_uid": dashboard_uid, "folder_uid": folder_uid, "response": result, }, ensure_ascii=False, indent=2, )) elif action == "validate": result = request( "GET", "/api/dashboards/uid/" + urllib.parse.quote(dashboard_uid), ) dashboard = result["dashboard"] panels = { panel.get("id"): panel for panel in dashboard.get("panels", []) } datasource_uids = sorted({ panel.get("datasource", {}).get("uid") for panel in dashboard.get("panels", []) if isinstance( panel.get("datasource"), dict, ) and panel.get( "datasource", {}, ).get("uid") }) errors = [] if len(dashboard.get("panels", [])) != 31: errors.append( "El dashboard no tiene 31 paneles" ) if dashboard.get("refresh") != "15s": errors.append( "El refresco no está en 15s" ) if datasource_uids != [datasource_uid]: errors.append( f"Datasource inesperado: {datasource_uids!r}" ) expected = { 29: "li_context_regression_latest_run_v1", 30: "li_context_regression_failures_v1", 31: "li_context_regression_run_history_v1", } for panel_id, fragment in expected.items(): sql = ( panels.get(panel_id, {}) .get("targets", [{}])[0] .get("rawSql", "") ) if fragment not in sql: errors.append( f"Panel {panel_id} no consulta {fragment}" ) governance_expected = { 23: "li_context_governance_ui_queue_v1", 26: "li_context_governance_ui_queue_v1", } for panel_id, fragment in governance_expected.items(): sql = ( panels.get(panel_id, {}) .get("targets", [{}])[0] .get("rawSql", "") ) if fragment not in sql: errors.append( f"Panel {panel_id} no consulta {fragment}" ) if ( '"Incidencia"' not in sql or '"Revisión"' not in sql ): errors.append( f"Panel {panel_id} no separa Incidencia y Revisión" ) output = { "status": "ok" if not errors else "error", "dashboard_uid": dashboard.get("uid"), "folder_uid": result.get( "meta", {}, ).get("folderUid"), "panel_count": len( dashboard.get( "panels", [], ) ), "refresh": dashboard.get("refresh"), "datasource_uids": datasource_uids, "version": dashboard.get("version"), "url": result.get( "meta", {}, ).get("url"), "regression_panels": { "latest_run": 29 in panels, "issues": 30 in panels, "history": 31 in panels, }, "governance_ui_panels": { "pending_review": 23 in panels, "daily_agenda": 26 in panels, "semantic_status_split": all( ( '"Incidencia"' in ( panels.get(panel_id, {}) .get("targets", [{}])[0] .get("rawSql", "") ) and '"Revisión"' in ( panels.get(panel_id, {}) .get("targets", [{}])[0] .get("rawSql", "") ) ) for panel_id in (23, 26) ), }, "errors": errors, } print(json.dumps( output, ensure_ascii=False, indent=2, )) if errors: raise SystemExit(1) else: raise SystemExit( "Uso: deploy_governance_review_ui_dashboard_v0314.sh " "{deploy|validate}" ) PY