feat(ucepsa): add daily governance agenda shadow
This commit is contained in:
parent
72c57f4ce9
commit
b06f15534f
@ -0,0 +1,142 @@
|
|||||||
|
# UCEPSA — Agenda diaria de gobernanza SHADOW v0.3.12
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
Convertir la arquitectura de gobernanza en una revisión diaria de 15 minutos:
|
||||||
|
|
||||||
|
```text
|
||||||
|
datos automáticos
|
||||||
|
→ decisiones humanas consolidadas
|
||||||
|
→ agenda priorizada
|
||||||
|
→ snapshot auditable
|
||||||
|
→ revisión y seguimiento
|
||||||
|
```
|
||||||
|
|
||||||
|
Una fila de agenda equivale a una pregunta humana.
|
||||||
|
|
||||||
|
## Priorización
|
||||||
|
|
||||||
|
La severidad técnica se conserva.
|
||||||
|
|
||||||
|
La agenda añade un estado de atención independiente:
|
||||||
|
|
||||||
|
```text
|
||||||
|
0-8 h RECENT
|
||||||
|
8-24 h ATTENTION
|
||||||
|
>24 h OVERDUE
|
||||||
|
```
|
||||||
|
|
||||||
|
Estos tramos organizan la reunión. No convierten una advertencia en crítica.
|
||||||
|
|
||||||
|
## Objetos
|
||||||
|
|
||||||
|
```text
|
||||||
|
context_governance_daily_policies
|
||||||
|
context_governance_agenda_snapshots
|
||||||
|
context_governance_agenda_snapshot_items
|
||||||
|
|
||||||
|
li_context_governance_decision_evidence_pack_v1
|
||||||
|
li_context_governance_daily_agenda_v1
|
||||||
|
li_context_governance_daily_summary_v1
|
||||||
|
li_context_governance_agenda_snapshot_history_v1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Despliegue
|
||||||
|
|
||||||
|
```bash
|
||||||
|
FILE=/srv/mesavault/40-clients/ucepsa/edge-oee-demo/sql/versions/114_ucepsa_context_daily_governance_agenda_shadow_v0312.sql
|
||||||
|
|
||||||
|
docker exec -i mv_ucepsa_postgres_hot sh -lc \
|
||||||
|
'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"' \
|
||||||
|
< "$FILE"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Runtime
|
||||||
|
|
||||||
|
```bash
|
||||||
|
RUNTIME=/srv/mesavault/edge-oee-ucepsa
|
||||||
|
TARGET=/srv/mesavault/40-clients/ucepsa/edge-oee-demo
|
||||||
|
|
||||||
|
install -m 0644 \
|
||||||
|
"$TARGET/tools/context_daily_governance.py" \
|
||||||
|
"$RUNTIME/tools/"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Consola
|
||||||
|
|
||||||
|
```bash
|
||||||
|
MANAGER=/srv/mesavault/40-clients/ucepsa/edge-oee-demo/ops/manage_context_daily_governance_v0312.sh
|
||||||
|
|
||||||
|
"$MANAGER" agenda --limit 50
|
||||||
|
|
||||||
|
"$MANAGER" show \
|
||||||
|
--decision-group-key 'DECISION:...'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Guardar la agenda de una reunión
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"$MANAGER" snapshot \
|
||||||
|
--generated-by "Víctor Fraile" \
|
||||||
|
--notes "Revisión diaria con Dirección y Producción."
|
||||||
|
```
|
||||||
|
|
||||||
|
La salida devuelve `snapshot_id`.
|
||||||
|
|
||||||
|
## Exportar Markdown
|
||||||
|
|
||||||
|
Vista actual:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OUT=/srv/mesavault/exports/governance-review-$(date +%F).md
|
||||||
|
mkdir -p "$(dirname "$OUT")"
|
||||||
|
|
||||||
|
"$MANAGER" export-markdown \
|
||||||
|
--generated-by "MESAVAULT" \
|
||||||
|
> "$OUT"
|
||||||
|
```
|
||||||
|
|
||||||
|
Snapshot inmutable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"$MANAGER" export-markdown \
|
||||||
|
--snapshot-id NUMERO \
|
||||||
|
> "$OUT"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validación
|
||||||
|
|
||||||
|
```bash
|
||||||
|
VALIDATION=/srv/mesavault/40-clients/ucepsa/edge-oee-demo/ops/validate_context_daily_governance_v0312.sql
|
||||||
|
|
||||||
|
docker exec -i mv_ucepsa_postgres_hot sh -lc \
|
||||||
|
'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"' \
|
||||||
|
< "$VALIDATION"
|
||||||
|
```
|
||||||
|
|
||||||
|
Valores obligatorios:
|
||||||
|
|
||||||
|
```text
|
||||||
|
agenda_rows = decision_backlog_rows
|
||||||
|
duplicated_agenda_decisions = 0
|
||||||
|
agenda_rank_mismatches = 0
|
||||||
|
invalid_attention_status_rows = 0
|
||||||
|
snapshot_item_count_mismatches = 0
|
||||||
|
|
||||||
|
official_snapshot_rows = 0
|
||||||
|
official_snapshot_item_rows = 0
|
||||||
|
official_summary_rows = 0
|
||||||
|
official_operator_queue_rows = 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dashboard
|
||||||
|
|
||||||
|
Se mantiene el mismo UID y se añaden:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Agenda diaria de gobernanza
|
||||||
|
Paquete de evidencia por decisión
|
||||||
|
Indicadores de gobernanza diaria
|
||||||
|
```
|
||||||
|
|
||||||
|
Paneles totales esperados: 28.
|
||||||
@ -15,7 +15,7 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"description": "Supervisión SHADOW de sesiones, incidencias, arbitraje, casos raíz y decisiones humanas consolidadas. La carga de revisión se mide por preguntas operativas, conservando la auditoría de casos y evidencias.",
|
"description": "Supervisión SHADOW de sesiones, arbitraje, casos raíz, decisiones humanas y agenda diaria de gobernanza. La revisión se organiza por preguntas operativas y puede guardarse como snapshot auditable.",
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"fiscalYearStartMonth": 0,
|
"fiscalYearStartMonth": 0,
|
||||||
"graphTooltip": 1,
|
"graphTooltip": 1,
|
||||||
@ -2981,6 +2981,311 @@
|
|||||||
],
|
],
|
||||||
"title": "Historial de decisiones humanas · últimos 7 días",
|
"title": "Historial de decisiones humanas · últimos 7 días",
|
||||||
"type": "table"
|
"type": "table"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "bfnbcasbm6hhca"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {
|
||||||
|
"align": "auto",
|
||||||
|
"cellOptions": {
|
||||||
|
"type": "auto"
|
||||||
|
},
|
||||||
|
"filterable": true,
|
||||||
|
"inspect": false
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Severidad"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "mappings",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"type": "value",
|
||||||
|
"options": {
|
||||||
|
"WARNING": {
|
||||||
|
"text": "WARNING",
|
||||||
|
"color": "orange",
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
"CRITICAL": {
|
||||||
|
"text": "CRITICAL",
|
||||||
|
"color": "red",
|
||||||
|
"index": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom.cellOptions",
|
||||||
|
"value": {
|
||||||
|
"type": "color-background"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Atención"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "mappings",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"type": "value",
|
||||||
|
"options": {
|
||||||
|
"RECENT": {
|
||||||
|
"text": "Reciente",
|
||||||
|
"color": "green",
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
"ATTENTION": {
|
||||||
|
"text": "Atención",
|
||||||
|
"color": "orange",
|
||||||
|
"index": 1
|
||||||
|
},
|
||||||
|
"OVERDUE": {
|
||||||
|
"text": "Vencida",
|
||||||
|
"color": "red",
|
||||||
|
"index": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom.cellOptions",
|
||||||
|
"value": {
|
||||||
|
"type": "color-background"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Decisión"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 400
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Pregunta"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 520
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Acción"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 520
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 12,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 188
|
||||||
|
},
|
||||||
|
"id": 26,
|
||||||
|
"options": {
|
||||||
|
"cellHeight": "sm",
|
||||||
|
"footer": {
|
||||||
|
"countRows": false,
|
||||||
|
"fields": "",
|
||||||
|
"reducer": [
|
||||||
|
"sum"
|
||||||
|
],
|
||||||
|
"show": false
|
||||||
|
},
|
||||||
|
"showHeader": true,
|
||||||
|
"sortBy": []
|
||||||
|
},
|
||||||
|
"pluginVersion": "11.0.0",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "bfnbcasbm6hhca"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT\n agenda_rank AS \"Prioridad\",\n max_severity AS \"Severidad\",\n attention_status AS \"Atención\",\n machine_id AS \"Máquina\",\n decision_title AS \"Decisión\",\n CASE review_owner\n WHEN 'PRODUCTION' THEN 'Producción'\n WHEN 'TECHNICAL' THEN 'Técnico'\n ELSE 'Conjunto'\n END AS \"Responsable\",\n pending_age_hours AS \"Antigüedad h\",\n review_due_at AS \"Límite\",\n pending_root_case_count AS \"Casos pendientes\",\n linked_episode_count AS \"Evidencias\",\n array_to_string(\n production_orders,\n ', '\n ) AS \"Órdenes\",\n array_to_string(\n odoo_workorder_ids,\n ', '\n ) AS \"Workorders\",\n question_text AS \"Pregunta\",\n recommended_action AS \"Acción\"\nFROM\n mv_reports_ucepsa_prod\n .li_context_governance_daily_agenda_v1\nORDER BY agenda_rank\nLIMIT 50",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Agenda diaria de gobernanza",
|
||||||
|
"type": "table"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "bfnbcasbm6hhca"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {
|
||||||
|
"align": "auto",
|
||||||
|
"cellOptions": {
|
||||||
|
"type": "auto"
|
||||||
|
},
|
||||||
|
"filterable": true,
|
||||||
|
"inspect": false
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 10,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 200
|
||||||
|
},
|
||||||
|
"id": 27,
|
||||||
|
"options": {
|
||||||
|
"cellHeight": "sm",
|
||||||
|
"footer": {
|
||||||
|
"countRows": false,
|
||||||
|
"fields": "",
|
||||||
|
"reducer": [
|
||||||
|
"sum"
|
||||||
|
],
|
||||||
|
"show": false
|
||||||
|
},
|
||||||
|
"showHeader": true,
|
||||||
|
"sortBy": []
|
||||||
|
},
|
||||||
|
"pluginVersion": "11.0.0",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "bfnbcasbm6hhca"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT\n decision_group_key AS \"Decisión\",\n machine_id AS \"Máquina\",\n linked_root_case_count AS \"Casos raíz\",\n linked_episode_count AS \"Episodios\",\n array_to_string(\n root_case_ids,\n ', '\n ) AS \"IDs casos\",\n array_to_string(\n episode_ids,\n ', '\n ) AS \"IDs episodios\",\n array_to_string(\n incident_codes,\n ', '\n ) AS \"Códigos\",\n array_to_string(\n operator_names,\n ', '\n ) AS \"Operarios\",\n array_to_string(\n known_resolution_classifications,\n ', '\n ) AS \"Resolución conocida\",\n evidence_first_seen_at AS \"Primera evidencia\",\n evidence_last_seen_at AS \"Última evidencia\"\nFROM\n mv_reports_ucepsa_prod\n .li_context_governance_decision_evidence_pack_v1\nWHERE review_status =\n 'PENDING'\nORDER BY\n CASE max_severity\n WHEN 'CRITICAL' THEN 1\n ELSE 2\n END,\n first_seen_at\nLIMIT 50",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Paquete de evidencia por decisión",
|
||||||
|
"type": "table"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "bfnbcasbm6hhca"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {
|
||||||
|
"align": "auto",
|
||||||
|
"cellOptions": {
|
||||||
|
"type": "auto"
|
||||||
|
},
|
||||||
|
"filterable": true,
|
||||||
|
"inspect": false
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 210
|
||||||
|
},
|
||||||
|
"id": 28,
|
||||||
|
"options": {
|
||||||
|
"cellHeight": "sm",
|
||||||
|
"footer": {
|
||||||
|
"countRows": false,
|
||||||
|
"fields": "",
|
||||||
|
"reducer": [
|
||||||
|
"sum"
|
||||||
|
],
|
||||||
|
"show": false
|
||||||
|
},
|
||||||
|
"showHeader": true,
|
||||||
|
"sortBy": []
|
||||||
|
},
|
||||||
|
"pluginVersion": "11.0.0",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "postgres",
|
||||||
|
"uid": "bfnbcasbm6hhca"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"format": "table",
|
||||||
|
"rawQuery": true,
|
||||||
|
"rawSql": "SELECT\n agenda_local_date AS \"Fecha\",\n pending_decision_count AS \"Decisiones pendientes\",\n pending_critical_decision_count AS \"Críticas\",\n attention_decision_count AS \"Atención\",\n overdue_decision_count AS \"Vencidas\",\n production_pending_count AS \"Producción\",\n technical_pending_count AS \"Técnico\",\n joint_pending_count AS \"Conjunto\",\n pending_root_case_count AS \"Casos raíz\",\n linked_episode_count AS \"Evidencias\",\n decisions_first_seen_today AS \"Nuevas hoy\",\n decisions_reviewed_today AS \"Resueltas hoy\",\n average_review_resolution_hours AS \"Resolución media h\",\n evidence_per_pending_decision AS \"Evidencias por decisión\"\nFROM\n mv_reports_ucepsa_prod\n .li_context_governance_daily_summary_v1",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Indicadores de gobernanza diaria",
|
||||||
|
"type": "table"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"refresh": "15s",
|
"refresh": "15s",
|
||||||
@ -3005,7 +3310,11 @@
|
|||||||
"v0310",
|
"v0310",
|
||||||
"decision-groups",
|
"decision-groups",
|
||||||
"temporal-consolidation",
|
"temporal-consolidation",
|
||||||
"v0311"
|
"v0311",
|
||||||
|
"daily-governance",
|
||||||
|
"agenda",
|
||||||
|
"evidence-pack",
|
||||||
|
"v0312"
|
||||||
],
|
],
|
||||||
"templating": {
|
"templating": {
|
||||||
"list": []
|
"list": []
|
||||||
|
|||||||
259
ucepsa/edge-oee-demo/ops/deploy_context_daily_governance_dashboard_v0312.sh
Executable file
259
ucepsa/edge-oee-demo/ops/deploy_context_daily_governance_dashboard_v0312.sh
Executable file
@ -0,0 +1,259 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ACTION="${1:-deploy}"
|
||||||
|
GRAFANA_CONTAINER="${GRAFANA_CONTAINER:-mv_ucepsa_grafana}"
|
||||||
|
REPO_ROOT="${DAILY_GOVERNANCE_DASHBOARD_REPO_ROOT:-/srv/mesavault/40-clients/ucepsa/edge-oee-demo}"
|
||||||
|
DASHBOARD_FILE="${DAILY_GOVERNANCE_DASHBOARD_FILE:-$REPO_ROOT/grafana/dashboards/ucepsa-shopfloor-context-health.dashboard.json}"
|
||||||
|
FOLDER_UID="${DAILY_GOVERNANCE_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 daily governance agenda "
|
||||||
|
"SHADOW v0.3.12"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
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", [])) != 28:
|
||||||
|
errors.append(
|
||||||
|
"El dashboard no tiene 28 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 = {
|
||||||
|
26: "li_context_governance_daily_agenda_v1",
|
||||||
|
27: "li_context_governance_decision_evidence_pack_v1",
|
||||||
|
28: "li_context_governance_daily_summary_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}"
|
||||||
|
)
|
||||||
|
|
||||||
|
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"),
|
||||||
|
"daily_governance_panels": {
|
||||||
|
"agenda": 26 in panels,
|
||||||
|
"evidence_pack": 27 in panels,
|
||||||
|
"summary": 28 in panels,
|
||||||
|
},
|
||||||
|
"errors":
|
||||||
|
errors,
|
||||||
|
}
|
||||||
|
|
||||||
|
print(json.dumps(
|
||||||
|
output,
|
||||||
|
ensure_ascii=False,
|
||||||
|
indent=2,
|
||||||
|
))
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise SystemExit(
|
||||||
|
"Uso: deploy_context_daily_governance_dashboard_v0312.sh "
|
||||||
|
"{deploy|validate}"
|
||||||
|
)
|
||||||
|
PY
|
||||||
47
ucepsa/edge-oee-demo/ops/manage_context_daily_governance_v0312.sh
Executable file
47
ucepsa/edge-oee-demo/ops/manage_context_daily_governance_v0312.sh
Executable file
@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ACTION="${1:-agenda}"
|
||||||
|
ROOT="${DAILY_GOVERNANCE_RUNTIME_ROOT:-/srv/mesavault/edge-oee-ucepsa}"
|
||||||
|
NETWORK="${DAILY_GOVERNANCE_NETWORK:-mv_ucepsa_net}"
|
||||||
|
POSTGRES_CONTAINER="${DAILY_GOVERNANCE_POSTGRES_CONTAINER:-mv_ucepsa_postgres_hot}"
|
||||||
|
TENANT_OVERRIDE="${DAILY_GOVERNANCE_TENANT_OVERRIDE:-ucepsa}"
|
||||||
|
SITE_OVERRIDE="${DAILY_GOVERNANCE_SITE_OVERRIDE:-ucepsa_onpremise}"
|
||||||
|
PGHOST_OVERRIDE="${DAILY_GOVERNANCE_PGHOST_OVERRIDE:-mv_ucepsa_postgres_hot}"
|
||||||
|
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
IMAGE="$(
|
||||||
|
docker inspect \
|
||||||
|
mv_ucepsa_odoo_order_state_pg_sink \
|
||||||
|
--format '{{.Config.Image}}'
|
||||||
|
)"
|
||||||
|
|
||||||
|
PGDATABASE_OVERRIDE="${DAILY_GOVERNANCE_PGDATABASE_OVERRIDE:-$(
|
||||||
|
docker inspect "$POSTGRES_CONTAINER" \
|
||||||
|
--format '{{range .Config.Env}}{{println .}}{{end}}' \
|
||||||
|
| sed -n 's/^POSTGRES_DB=//p' \
|
||||||
|
| head -n 1
|
||||||
|
)}"
|
||||||
|
|
||||||
|
if [[ -z "$PGDATABASE_OVERRIDE" ]]; then
|
||||||
|
echo "ERROR: no se pudo determinar POSTGRES_DB." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
shift || true
|
||||||
|
|
||||||
|
docker run --rm -i \
|
||||||
|
--network "$NETWORK" \
|
||||||
|
--env-file .env \
|
||||||
|
-e "TENANT_OVERRIDE=$TENANT_OVERRIDE" \
|
||||||
|
-e "SITE_OVERRIDE=$SITE_OVERRIDE" \
|
||||||
|
-e "PGHOST=$PGHOST_OVERRIDE" \
|
||||||
|
-e "PGDATABASE=$PGDATABASE_OVERRIDE" \
|
||||||
|
-v "$ROOT:/app:ro" \
|
||||||
|
-w /app \
|
||||||
|
--entrypoint python \
|
||||||
|
"$IMAGE" \
|
||||||
|
tools/context_daily_governance.py \
|
||||||
|
"$ACTION" \
|
||||||
|
"$@"
|
||||||
@ -0,0 +1,181 @@
|
|||||||
|
\pset pager off
|
||||||
|
|
||||||
|
\echo '=== 1. Objetos v0.3.12 ==='
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
to_regclass(
|
||||||
|
'mv_loss_intelligence.context_governance_daily_policies'
|
||||||
|
) AS policy_table,
|
||||||
|
to_regclass(
|
||||||
|
'mv_loss_intelligence.context_governance_agenda_snapshots'
|
||||||
|
) AS snapshots_table,
|
||||||
|
to_regclass(
|
||||||
|
'mv_loss_intelligence.context_governance_agenda_snapshot_items'
|
||||||
|
) AS snapshot_items_table,
|
||||||
|
to_regclass(
|
||||||
|
'mv_reports_ucepsa_prod.li_context_governance_decision_evidence_pack_v1'
|
||||||
|
) AS evidence_pack_view,
|
||||||
|
to_regclass(
|
||||||
|
'mv_reports_ucepsa_prod.li_context_governance_daily_agenda_v1'
|
||||||
|
) AS agenda_view,
|
||||||
|
to_regclass(
|
||||||
|
'mv_reports_ucepsa_prod.li_context_governance_daily_summary_v1'
|
||||||
|
) AS summary_view,
|
||||||
|
to_regclass(
|
||||||
|
'mv_reports_ucepsa_prod.li_context_governance_agenda_snapshot_history_v1'
|
||||||
|
) AS snapshot_history_view;
|
||||||
|
|
||||||
|
\echo '=== 2. Política diaria ==='
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
timezone_name,
|
||||||
|
attention_after_hours,
|
||||||
|
overdue_after_hours,
|
||||||
|
agenda_limit,
|
||||||
|
active
|
||||||
|
FROM
|
||||||
|
mv_loss_intelligence
|
||||||
|
.context_governance_daily_policies
|
||||||
|
WHERE tenant = 'ucepsa'
|
||||||
|
AND site = 'ucepsa_onpremise'
|
||||||
|
AND policy_code =
|
||||||
|
'CONTEXT_DAILY_GOVERNANCE_V1';
|
||||||
|
|
||||||
|
\echo '=== 3. Una fila de agenda por decisión pendiente ==='
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_context_governance_daily_agenda_v1
|
||||||
|
) AS agenda_rows,
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_context_decision_group_backlog_v1
|
||||||
|
) AS decision_backlog_rows;
|
||||||
|
|
||||||
|
\echo '=== 4. Decisiones duplicadas en agenda ==='
|
||||||
|
|
||||||
|
SELECT COUNT(*) AS duplicated_agenda_decisions
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
decision_group_key,
|
||||||
|
COUNT(*) AS n
|
||||||
|
FROM
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_context_governance_daily_agenda_v1
|
||||||
|
GROUP BY decision_group_key
|
||||||
|
HAVING COUNT(*) > 1
|
||||||
|
) duplicated;
|
||||||
|
|
||||||
|
\echo '=== 5. Ranking continuo ==='
|
||||||
|
|
||||||
|
WITH ranked AS (
|
||||||
|
SELECT
|
||||||
|
agenda_rank,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
ORDER BY agenda_rank
|
||||||
|
) AS expected_rank
|
||||||
|
FROM
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_context_governance_daily_agenda_v1
|
||||||
|
)
|
||||||
|
SELECT COUNT(*) AS agenda_rank_mismatches
|
||||||
|
FROM ranked
|
||||||
|
WHERE agenda_rank <>
|
||||||
|
expected_rank;
|
||||||
|
|
||||||
|
\echo '=== 6. Evidencia de las tres decisiones actuales ==='
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
decision_group_key,
|
||||||
|
machine_id,
|
||||||
|
linked_root_case_count,
|
||||||
|
linked_episode_count,
|
||||||
|
root_case_ids,
|
||||||
|
episode_ids,
|
||||||
|
production_orders,
|
||||||
|
odoo_workorder_ids,
|
||||||
|
operator_names,
|
||||||
|
review_owner,
|
||||||
|
review_status
|
||||||
|
FROM
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_context_governance_decision_evidence_pack_v1
|
||||||
|
WHERE decision_group_key IN (
|
||||||
|
'DECISION:UNCONFIRMED_ACTIVITY:CORT-00:20260720:1',
|
||||||
|
'DECISION:ODOO_ORDER_START_CONFLICT:CORT-00:20260720:5',
|
||||||
|
'DECISION:UNCONFIRMED_ACTIVITY:CORT-02:20260720:7'
|
||||||
|
)
|
||||||
|
ORDER BY decision_group_key;
|
||||||
|
|
||||||
|
\echo '=== 7. Resumen diario ==='
|
||||||
|
|
||||||
|
SELECT *
|
||||||
|
FROM
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_context_governance_daily_summary_v1;
|
||||||
|
|
||||||
|
\echo '=== 8. Tramos de atención válidos ==='
|
||||||
|
|
||||||
|
SELECT COUNT(*) AS invalid_attention_status_rows
|
||||||
|
FROM
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_context_governance_daily_agenda_v1
|
||||||
|
WHERE attention_status NOT IN (
|
||||||
|
'RECENT',
|
||||||
|
'ATTENTION',
|
||||||
|
'OVERDUE'
|
||||||
|
)
|
||||||
|
OR pending_age_hours < 0
|
||||||
|
OR review_due_at <
|
||||||
|
first_seen_at;
|
||||||
|
|
||||||
|
\echo '=== 9. Snapshots coherentes ==='
|
||||||
|
|
||||||
|
SELECT COUNT(*) AS snapshot_item_count_mismatches
|
||||||
|
FROM
|
||||||
|
mv_loss_intelligence
|
||||||
|
.context_governance_agenda_snapshots s
|
||||||
|
WHERE s.item_count <>
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM
|
||||||
|
mv_loss_intelligence
|
||||||
|
.context_governance_agenda_snapshot_items i
|
||||||
|
WHERE i.snapshot_id =
|
||||||
|
s.snapshot_id
|
||||||
|
);
|
||||||
|
|
||||||
|
\echo '=== 10. Nada oficial ==='
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM
|
||||||
|
mv_loss_intelligence
|
||||||
|
.context_governance_agenda_snapshots
|
||||||
|
WHERE official_eligible
|
||||||
|
) AS official_snapshot_rows,
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM
|
||||||
|
mv_loss_intelligence
|
||||||
|
.context_governance_agenda_snapshot_items
|
||||||
|
WHERE official_eligible
|
||||||
|
) AS official_snapshot_item_rows,
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_context_governance_daily_summary_v1
|
||||||
|
WHERE official_eligible
|
||||||
|
) AS official_summary_rows,
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM
|
||||||
|
mv_reports_ucepsa_prod.li_operator_stop_queue_v2
|
||||||
|
) AS official_operator_queue_rows;
|
||||||
@ -0,0 +1,633 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS
|
||||||
|
mv_loss_intelligence.context_governance_daily_policies (
|
||||||
|
tenant text NOT NULL,
|
||||||
|
site text NOT NULL,
|
||||||
|
policy_code text NOT NULL,
|
||||||
|
timezone_name text NOT NULL DEFAULT 'Europe/Madrid',
|
||||||
|
attention_after_hours numeric(10, 2) NOT NULL DEFAULT 8
|
||||||
|
CHECK (attention_after_hours >= 0),
|
||||||
|
overdue_after_hours numeric(10, 2) NOT NULL DEFAULT 24
|
||||||
|
CHECK (
|
||||||
|
overdue_after_hours >
|
||||||
|
attention_after_hours
|
||||||
|
),
|
||||||
|
agenda_limit integer NOT NULL DEFAULT 50
|
||||||
|
CHECK (agenda_limit > 0),
|
||||||
|
active boolean NOT NULL DEFAULT true,
|
||||||
|
notes text,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (
|
||||||
|
tenant,
|
||||||
|
site,
|
||||||
|
policy_code
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE
|
||||||
|
mv_loss_intelligence.context_governance_daily_policies IS
|
||||||
|
'Política SHADOW para priorizar la agenda diaria de gobernanza. No modifica severidad, contexto, Odoo ni elegibilidad oficial.';
|
||||||
|
|
||||||
|
INSERT INTO
|
||||||
|
mv_loss_intelligence.context_governance_daily_policies (
|
||||||
|
tenant,
|
||||||
|
site,
|
||||||
|
policy_code,
|
||||||
|
timezone_name,
|
||||||
|
attention_after_hours,
|
||||||
|
overdue_after_hours,
|
||||||
|
agenda_limit,
|
||||||
|
active,
|
||||||
|
notes
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
'ucepsa',
|
||||||
|
'ucepsa_onpremise',
|
||||||
|
'CONTEXT_DAILY_GOVERNANCE_V1',
|
||||||
|
'Europe/Madrid',
|
||||||
|
8,
|
||||||
|
24,
|
||||||
|
50,
|
||||||
|
true,
|
||||||
|
'0-8 h reciente; 8-24 h requiere atención; más de 24 h vencido. Estos tramos organizan la revisión y no sustituyen la severidad técnica.'
|
||||||
|
)
|
||||||
|
ON CONFLICT (
|
||||||
|
tenant,
|
||||||
|
site,
|
||||||
|
policy_code
|
||||||
|
)
|
||||||
|
DO UPDATE SET
|
||||||
|
timezone_name =
|
||||||
|
EXCLUDED.timezone_name,
|
||||||
|
attention_after_hours =
|
||||||
|
EXCLUDED.attention_after_hours,
|
||||||
|
overdue_after_hours =
|
||||||
|
EXCLUDED.overdue_after_hours,
|
||||||
|
agenda_limit =
|
||||||
|
EXCLUDED.agenda_limit,
|
||||||
|
active = true,
|
||||||
|
notes =
|
||||||
|
EXCLUDED.notes,
|
||||||
|
updated_at = now();
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS
|
||||||
|
mv_loss_intelligence.context_governance_agenda_snapshots (
|
||||||
|
snapshot_id bigserial PRIMARY KEY,
|
||||||
|
tenant text NOT NULL,
|
||||||
|
site text NOT NULL,
|
||||||
|
agenda_local_date date NOT NULL,
|
||||||
|
generated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
generated_by text NOT NULL,
|
||||||
|
item_count integer NOT NULL
|
||||||
|
CHECK (item_count >= 0),
|
||||||
|
source_state_hash text NOT NULL,
|
||||||
|
source_view text NOT NULL DEFAULT
|
||||||
|
'mv_reports_ucepsa_prod.li_context_governance_daily_agenda_v1',
|
||||||
|
notes text,
|
||||||
|
official_eligible boolean NOT NULL DEFAULT false
|
||||||
|
CHECK (official_eligible = false),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS
|
||||||
|
idx_context_governance_snapshots_date
|
||||||
|
ON
|
||||||
|
mv_loss_intelligence.context_governance_agenda_snapshots (
|
||||||
|
tenant,
|
||||||
|
site,
|
||||||
|
agenda_local_date DESC,
|
||||||
|
generated_at DESC
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS
|
||||||
|
mv_loss_intelligence.context_governance_agenda_snapshot_items (
|
||||||
|
snapshot_id bigint NOT NULL
|
||||||
|
REFERENCES
|
||||||
|
mv_loss_intelligence
|
||||||
|
.context_governance_agenda_snapshots (
|
||||||
|
snapshot_id
|
||||||
|
)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
item_rank integer NOT NULL
|
||||||
|
CHECK (item_rank > 0),
|
||||||
|
decision_group_key text NOT NULL,
|
||||||
|
decision_status text NOT NULL,
|
||||||
|
max_severity text NOT NULL,
|
||||||
|
machine_id text,
|
||||||
|
decision_family text NOT NULL,
|
||||||
|
decision_title text NOT NULL,
|
||||||
|
question_text text NOT NULL,
|
||||||
|
recommended_action text NOT NULL,
|
||||||
|
review_owner text NOT NULL,
|
||||||
|
first_seen_at timestamptz NOT NULL,
|
||||||
|
last_seen_at timestamptz NOT NULL,
|
||||||
|
ended_at timestamptz,
|
||||||
|
pending_age_hours numeric(14, 2) NOT NULL,
|
||||||
|
attention_status text NOT NULL
|
||||||
|
CHECK (
|
||||||
|
attention_status IN (
|
||||||
|
'RECENT',
|
||||||
|
'ATTENTION',
|
||||||
|
'OVERDUE'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
review_due_at timestamptz NOT NULL,
|
||||||
|
pending_root_case_count integer NOT NULL,
|
||||||
|
reviewed_root_case_count integer NOT NULL,
|
||||||
|
linked_root_case_count integer NOT NULL,
|
||||||
|
linked_episode_count integer NOT NULL,
|
||||||
|
root_case_ids bigint[] NOT NULL DEFAULT ARRAY[]::bigint[],
|
||||||
|
episode_ids bigint[] NOT NULL DEFAULT ARRAY[]::bigint[],
|
||||||
|
production_orders text[] NOT NULL DEFAULT ARRAY[]::text[],
|
||||||
|
odoo_workorder_ids bigint[] NOT NULL DEFAULT ARRAY[]::bigint[],
|
||||||
|
known_resolution_classifications text[] NOT NULL
|
||||||
|
DEFAULT ARRAY[]::text[],
|
||||||
|
evidence_json jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
official_eligible boolean NOT NULL DEFAULT false
|
||||||
|
CHECK (official_eligible = false),
|
||||||
|
PRIMARY KEY (
|
||||||
|
snapshot_id,
|
||||||
|
item_rank
|
||||||
|
),
|
||||||
|
CONSTRAINT ux_context_governance_snapshot_decision
|
||||||
|
UNIQUE (
|
||||||
|
snapshot_id,
|
||||||
|
decision_group_key
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE
|
||||||
|
mv_loss_intelligence.context_governance_agenda_snapshots IS
|
||||||
|
'Cabecera inmutable de una agenda diaria SHADOW guardada para auditoría de la reunión.';
|
||||||
|
|
||||||
|
COMMENT ON TABLE
|
||||||
|
mv_loss_intelligence.context_governance_agenda_snapshot_items IS
|
||||||
|
'Contenido inmutable de cada agenda guardada. Conserva la pregunta y su evidencia tal como se presentaron ese día.';
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW
|
||||||
|
mv_reports_ucepsa_prod.li_context_governance_decision_evidence_pack_v1
|
||||||
|
AS
|
||||||
|
WITH evidence AS (
|
||||||
|
SELECT
|
||||||
|
d.decision_group_key,
|
||||||
|
COUNT(
|
||||||
|
DISTINCT e.episode_id
|
||||||
|
) AS linked_episode_count,
|
||||||
|
ARRAY_AGG(
|
||||||
|
DISTINCT e.episode_id
|
||||||
|
ORDER BY e.episode_id
|
||||||
|
) AS episode_ids,
|
||||||
|
COALESCE(
|
||||||
|
ARRAY_REMOVE(
|
||||||
|
ARRAY_AGG(
|
||||||
|
DISTINCT e.incident_code
|
||||||
|
ORDER BY e.incident_code
|
||||||
|
),
|
||||||
|
NULL
|
||||||
|
),
|
||||||
|
ARRAY[]::text[]
|
||||||
|
) AS incident_codes,
|
||||||
|
COALESCE(
|
||||||
|
ARRAY_REMOVE(
|
||||||
|
ARRAY_AGG(
|
||||||
|
DISTINCT e.incident_scope
|
||||||
|
ORDER BY e.incident_scope
|
||||||
|
),
|
||||||
|
NULL
|
||||||
|
),
|
||||||
|
ARRAY[]::text[]
|
||||||
|
) AS incident_scopes,
|
||||||
|
COALESCE(
|
||||||
|
ARRAY_REMOVE(
|
||||||
|
ARRAY_AGG(
|
||||||
|
DISTINCT e.operator_name
|
||||||
|
ORDER BY e.operator_name
|
||||||
|
),
|
||||||
|
NULL
|
||||||
|
),
|
||||||
|
ARRAY[]::text[]
|
||||||
|
) AS operator_names,
|
||||||
|
MIN(e.first_seen_at)
|
||||||
|
AS evidence_first_seen_at,
|
||||||
|
MAX(e.last_seen_at)
|
||||||
|
AS evidence_last_seen_at,
|
||||||
|
JSONB_AGG(
|
||||||
|
JSONB_BUILD_OBJECT(
|
||||||
|
'case_id',
|
||||||
|
d.case_id,
|
||||||
|
'episode_id',
|
||||||
|
e.episode_id,
|
||||||
|
'evidence_role',
|
||||||
|
e.evidence_role,
|
||||||
|
'incident_scope',
|
||||||
|
e.incident_scope,
|
||||||
|
'incident_code',
|
||||||
|
e.incident_code,
|
||||||
|
'incident_title',
|
||||||
|
e.incident_title,
|
||||||
|
'production_order',
|
||||||
|
e.production_order,
|
||||||
|
'odoo_workorder_id',
|
||||||
|
e.odoo_workorder_id,
|
||||||
|
'operator_name',
|
||||||
|
e.operator_name,
|
||||||
|
'episode_status',
|
||||||
|
e.episode_status,
|
||||||
|
'first_seen_at',
|
||||||
|
e.first_seen_at,
|
||||||
|
'last_seen_at',
|
||||||
|
e.last_seen_at,
|
||||||
|
'ended_at',
|
||||||
|
e.ended_at,
|
||||||
|
'episode_duration_min',
|
||||||
|
e.episode_duration_min,
|
||||||
|
'episode_severity',
|
||||||
|
e.episode_severity,
|
||||||
|
'episode_review_status',
|
||||||
|
e.episode_review_status,
|
||||||
|
'latest_detail',
|
||||||
|
e.latest_detail,
|
||||||
|
'recommended_action',
|
||||||
|
e.latest_recommended_action
|
||||||
|
)
|
||||||
|
ORDER BY
|
||||||
|
d.case_id,
|
||||||
|
e.episode_id
|
||||||
|
) AS episode_evidence
|
||||||
|
FROM
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_context_decision_group_cases_v1 d
|
||||||
|
JOIN
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_context_review_case_evidence_v1 e
|
||||||
|
ON e.case_id =
|
||||||
|
d.case_id
|
||||||
|
GROUP BY
|
||||||
|
d.decision_group_key
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
h.*,
|
||||||
|
COALESCE(
|
||||||
|
e.linked_episode_count,
|
||||||
|
0
|
||||||
|
) AS linked_episode_count,
|
||||||
|
COALESCE(
|
||||||
|
e.episode_ids,
|
||||||
|
ARRAY[]::bigint[]
|
||||||
|
) AS episode_ids,
|
||||||
|
COALESCE(
|
||||||
|
e.incident_codes,
|
||||||
|
ARRAY[]::text[]
|
||||||
|
) AS incident_codes,
|
||||||
|
COALESCE(
|
||||||
|
e.incident_scopes,
|
||||||
|
ARRAY[]::text[]
|
||||||
|
) AS incident_scopes,
|
||||||
|
COALESCE(
|
||||||
|
e.operator_names,
|
||||||
|
ARRAY[]::text[]
|
||||||
|
) AS operator_names,
|
||||||
|
e.evidence_first_seen_at,
|
||||||
|
e.evidence_last_seen_at,
|
||||||
|
COALESCE(
|
||||||
|
e.episode_evidence,
|
||||||
|
'[]'::jsonb
|
||||||
|
) AS episode_evidence
|
||||||
|
FROM
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_context_decision_group_history_v1 h
|
||||||
|
LEFT JOIN evidence e
|
||||||
|
ON e.decision_group_key =
|
||||||
|
h.decision_group_key;
|
||||||
|
|
||||||
|
COMMENT ON VIEW
|
||||||
|
mv_reports_ucepsa_prod.li_context_governance_decision_evidence_pack_v1 IS
|
||||||
|
'Paquete legible de evidencia para cada decisión humana: casos raíz, episodios, órdenes, operarios, detalles y acciones.';
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW
|
||||||
|
mv_reports_ucepsa_prod.li_context_governance_daily_agenda_v1
|
||||||
|
AS
|
||||||
|
WITH policy AS (
|
||||||
|
SELECT *
|
||||||
|
FROM
|
||||||
|
mv_loss_intelligence
|
||||||
|
.context_governance_daily_policies
|
||||||
|
WHERE tenant = 'ucepsa'
|
||||||
|
AND site = 'ucepsa_onpremise'
|
||||||
|
AND policy_code =
|
||||||
|
'CONTEXT_DAILY_GOVERNANCE_V1'
|
||||||
|
AND active
|
||||||
|
ORDER BY updated_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
),
|
||||||
|
base AS (
|
||||||
|
SELECT
|
||||||
|
e.*,
|
||||||
|
p.timezone_name,
|
||||||
|
p.attention_after_hours,
|
||||||
|
p.overdue_after_hours,
|
||||||
|
p.agenda_limit,
|
||||||
|
ROUND(
|
||||||
|
GREATEST(
|
||||||
|
EXTRACT(
|
||||||
|
EPOCH FROM (
|
||||||
|
now()
|
||||||
|
- e.first_seen_at
|
||||||
|
)
|
||||||
|
) / 3600.0,
|
||||||
|
0
|
||||||
|
)::numeric,
|
||||||
|
2
|
||||||
|
) AS pending_age_hours
|
||||||
|
FROM
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_context_governance_decision_evidence_pack_v1 e
|
||||||
|
CROSS JOIN policy p
|
||||||
|
WHERE e.review_required
|
||||||
|
AND e.review_status =
|
||||||
|
'PENDING'
|
||||||
|
),
|
||||||
|
classified AS (
|
||||||
|
SELECT
|
||||||
|
b.*,
|
||||||
|
CASE
|
||||||
|
WHEN b.pending_age_hours >=
|
||||||
|
b.overdue_after_hours
|
||||||
|
THEN 'OVERDUE'
|
||||||
|
WHEN b.pending_age_hours >=
|
||||||
|
b.attention_after_hours
|
||||||
|
THEN 'ATTENTION'
|
||||||
|
ELSE 'RECENT'
|
||||||
|
END AS attention_status,
|
||||||
|
b.first_seen_at
|
||||||
|
+ (
|
||||||
|
b.overdue_after_hours
|
||||||
|
* interval '1 hour'
|
||||||
|
) AS review_due_at
|
||||||
|
FROM base b
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
ORDER BY
|
||||||
|
CASE max_severity
|
||||||
|
WHEN 'CRITICAL' THEN 1
|
||||||
|
ELSE 2
|
||||||
|
END,
|
||||||
|
CASE attention_status
|
||||||
|
WHEN 'OVERDUE' THEN 1
|
||||||
|
WHEN 'ATTENTION' THEN 2
|
||||||
|
ELSE 3
|
||||||
|
END,
|
||||||
|
CASE decision_status
|
||||||
|
WHEN 'OPEN' THEN 1
|
||||||
|
ELSE 2
|
||||||
|
END,
|
||||||
|
first_seen_at
|
||||||
|
) AS agenda_rank,
|
||||||
|
(
|
||||||
|
now()
|
||||||
|
AT TIME ZONE timezone_name
|
||||||
|
)::date AS agenda_local_date,
|
||||||
|
c.*
|
||||||
|
FROM classified c
|
||||||
|
ORDER BY agenda_rank;
|
||||||
|
|
||||||
|
COMMENT ON VIEW
|
||||||
|
mv_reports_ucepsa_prod.li_context_governance_daily_agenda_v1 IS
|
||||||
|
'Agenda diaria priorizada de gobernanza. Una fila equivale a una pregunta humana pendiente.';
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW
|
||||||
|
mv_reports_ucepsa_prod.li_context_governance_daily_summary_v1
|
||||||
|
AS
|
||||||
|
WITH policy AS (
|
||||||
|
SELECT *
|
||||||
|
FROM
|
||||||
|
mv_loss_intelligence
|
||||||
|
.context_governance_daily_policies
|
||||||
|
WHERE tenant = 'ucepsa'
|
||||||
|
AND site = 'ucepsa_onpremise'
|
||||||
|
AND policy_code =
|
||||||
|
'CONTEXT_DAILY_GOVERNANCE_V1'
|
||||||
|
AND active
|
||||||
|
ORDER BY updated_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
),
|
||||||
|
today AS (
|
||||||
|
SELECT
|
||||||
|
p.*,
|
||||||
|
(
|
||||||
|
now()
|
||||||
|
AT TIME ZONE p.timezone_name
|
||||||
|
)::date AS local_today
|
||||||
|
FROM policy p
|
||||||
|
),
|
||||||
|
agenda_summary AS (
|
||||||
|
SELECT
|
||||||
|
COUNT(*) AS pending_decision_count,
|
||||||
|
COUNT(*) FILTER (
|
||||||
|
WHERE max_severity =
|
||||||
|
'CRITICAL'
|
||||||
|
) AS pending_critical_decision_count,
|
||||||
|
COUNT(*) FILTER (
|
||||||
|
WHERE attention_status =
|
||||||
|
'ATTENTION'
|
||||||
|
) AS attention_decision_count,
|
||||||
|
COUNT(*) FILTER (
|
||||||
|
WHERE attention_status =
|
||||||
|
'OVERDUE'
|
||||||
|
) AS overdue_decision_count,
|
||||||
|
COUNT(*) FILTER (
|
||||||
|
WHERE review_owner =
|
||||||
|
'PRODUCTION'
|
||||||
|
) AS production_pending_count,
|
||||||
|
COUNT(*) FILTER (
|
||||||
|
WHERE review_owner =
|
||||||
|
'TECHNICAL'
|
||||||
|
) AS technical_pending_count,
|
||||||
|
COUNT(*) FILTER (
|
||||||
|
WHERE review_owner =
|
||||||
|
'JOINT'
|
||||||
|
) AS joint_pending_count,
|
||||||
|
COALESCE(
|
||||||
|
SUM(pending_root_case_count),
|
||||||
|
0
|
||||||
|
) AS pending_root_case_count,
|
||||||
|
COALESCE(
|
||||||
|
SUM(linked_episode_count),
|
||||||
|
0
|
||||||
|
) AS linked_episode_count
|
||||||
|
FROM
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_context_governance_daily_agenda_v1
|
||||||
|
),
|
||||||
|
history_summary AS (
|
||||||
|
SELECT
|
||||||
|
COUNT(*) FILTER (
|
||||||
|
WHERE (
|
||||||
|
h.first_seen_at
|
||||||
|
AT TIME ZONE t.timezone_name
|
||||||
|
)::date =
|
||||||
|
t.local_today
|
||||||
|
) AS decisions_first_seen_today,
|
||||||
|
COUNT(*) FILTER (
|
||||||
|
WHERE h.reviewed_at IS NOT NULL
|
||||||
|
AND (
|
||||||
|
h.reviewed_at
|
||||||
|
AT TIME ZONE t.timezone_name
|
||||||
|
)::date =
|
||||||
|
t.local_today
|
||||||
|
) AS decisions_reviewed_today,
|
||||||
|
ROUND(
|
||||||
|
(
|
||||||
|
AVG(
|
||||||
|
EXTRACT(
|
||||||
|
EPOCH FROM (
|
||||||
|
h.reviewed_at
|
||||||
|
- h.first_seen_at
|
||||||
|
)
|
||||||
|
) / 3600.0
|
||||||
|
) FILTER (
|
||||||
|
WHERE h.reviewed_at IS NOT NULL
|
||||||
|
AND h.reviewed_at >=
|
||||||
|
h.first_seen_at
|
||||||
|
)
|
||||||
|
)::numeric,
|
||||||
|
2
|
||||||
|
) AS average_review_resolution_hours
|
||||||
|
FROM
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_context_governance_decision_evidence_pack_v1 h
|
||||||
|
CROSS JOIN today t
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
now() AS observed_at,
|
||||||
|
t.local_today AS agenda_local_date,
|
||||||
|
a.pending_decision_count,
|
||||||
|
a.pending_critical_decision_count,
|
||||||
|
a.attention_decision_count,
|
||||||
|
a.overdue_decision_count,
|
||||||
|
a.production_pending_count,
|
||||||
|
a.technical_pending_count,
|
||||||
|
a.joint_pending_count,
|
||||||
|
a.pending_root_case_count,
|
||||||
|
a.linked_episode_count,
|
||||||
|
h.decisions_first_seen_today,
|
||||||
|
h.decisions_reviewed_today,
|
||||||
|
h.average_review_resolution_hours,
|
||||||
|
ROUND(
|
||||||
|
CASE
|
||||||
|
WHEN a.pending_decision_count = 0
|
||||||
|
THEN 0
|
||||||
|
ELSE
|
||||||
|
a.linked_episode_count::numeric
|
||||||
|
/ a.pending_decision_count
|
||||||
|
END,
|
||||||
|
2
|
||||||
|
) AS evidence_per_pending_decision,
|
||||||
|
false AS official_eligible
|
||||||
|
FROM today t
|
||||||
|
CROSS JOIN agenda_summary a
|
||||||
|
CROSS JOIN history_summary h;
|
||||||
|
|
||||||
|
COMMENT ON VIEW
|
||||||
|
mv_reports_ucepsa_prod.li_context_governance_daily_summary_v1 IS
|
||||||
|
'Indicadores de gobernanza: preguntas pendientes, vencidas, responsables, reducción de evidencias y tiempo de resolución.';
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW
|
||||||
|
mv_reports_ucepsa_prod.li_context_governance_agenda_snapshot_history_v1
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
s.snapshot_id,
|
||||||
|
s.tenant,
|
||||||
|
s.site,
|
||||||
|
s.agenda_local_date,
|
||||||
|
s.generated_at,
|
||||||
|
s.generated_by,
|
||||||
|
s.item_count,
|
||||||
|
s.source_state_hash,
|
||||||
|
s.source_view,
|
||||||
|
s.notes,
|
||||||
|
i.item_rank,
|
||||||
|
i.decision_group_key,
|
||||||
|
i.decision_status,
|
||||||
|
i.max_severity,
|
||||||
|
i.machine_id,
|
||||||
|
i.decision_family,
|
||||||
|
i.decision_title,
|
||||||
|
i.question_text,
|
||||||
|
i.recommended_action,
|
||||||
|
i.review_owner,
|
||||||
|
i.first_seen_at,
|
||||||
|
i.last_seen_at,
|
||||||
|
i.ended_at,
|
||||||
|
i.pending_age_hours,
|
||||||
|
i.attention_status,
|
||||||
|
i.review_due_at,
|
||||||
|
i.pending_root_case_count,
|
||||||
|
i.reviewed_root_case_count,
|
||||||
|
i.linked_root_case_count,
|
||||||
|
i.linked_episode_count,
|
||||||
|
i.root_case_ids,
|
||||||
|
i.episode_ids,
|
||||||
|
i.production_orders,
|
||||||
|
i.odoo_workorder_ids,
|
||||||
|
i.known_resolution_classifications,
|
||||||
|
i.evidence_json,
|
||||||
|
s.official_eligible
|
||||||
|
FROM
|
||||||
|
mv_loss_intelligence
|
||||||
|
.context_governance_agenda_snapshots s
|
||||||
|
LEFT JOIN
|
||||||
|
mv_loss_intelligence
|
||||||
|
.context_governance_agenda_snapshot_items i
|
||||||
|
ON i.snapshot_id =
|
||||||
|
s.snapshot_id;
|
||||||
|
|
||||||
|
COMMENT ON VIEW
|
||||||
|
mv_reports_ucepsa_prod.li_context_governance_agenda_snapshot_history_v1 IS
|
||||||
|
'Histórico inmutable de agendas guardadas y evidencia presentada en cada reunión.';
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_roles
|
||||||
|
WHERE rolname =
|
||||||
|
'grafana_ucepsa_ro'
|
||||||
|
) THEN
|
||||||
|
GRANT SELECT
|
||||||
|
ON
|
||||||
|
mv_loss_intelligence
|
||||||
|
.context_governance_daily_policies
|
||||||
|
TO grafana_ucepsa_ro;
|
||||||
|
|
||||||
|
GRANT SELECT
|
||||||
|
ON
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_context_governance_decision_evidence_pack_v1
|
||||||
|
TO grafana_ucepsa_ro;
|
||||||
|
|
||||||
|
GRANT SELECT
|
||||||
|
ON
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_context_governance_daily_agenda_v1
|
||||||
|
TO grafana_ucepsa_ro;
|
||||||
|
|
||||||
|
GRANT SELECT
|
||||||
|
ON
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_context_governance_daily_summary_v1
|
||||||
|
TO grafana_ucepsa_ro;
|
||||||
|
|
||||||
|
GRANT SELECT
|
||||||
|
ON
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_context_governance_agenda_snapshot_history_v1
|
||||||
|
TO grafana_ucepsa_ro;
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
673
ucepsa/edge-oee-demo/tools/context_daily_governance.py
Executable file
673
ucepsa/edge-oee-demo/tools/context_daily_governance.py
Executable file
@ -0,0 +1,673 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Agenda diaria y evidencia de gobernanza UCEPSA v0.3.12."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import date, datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import psycopg2
|
||||||
|
from psycopg2.extras import Json, RealDictCursor
|
||||||
|
|
||||||
|
|
||||||
|
VERSION = "0.3.12"
|
||||||
|
|
||||||
|
|
||||||
|
def env_first(*names: str, default: str | None = None) -> str | None:
|
||||||
|
for name in names:
|
||||||
|
value = os.getenv(name)
|
||||||
|
if value not in (None, ""):
|
||||||
|
return value
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def connect():
|
||||||
|
database = env_first("PGDATABASE", "POSTGRES_DB")
|
||||||
|
user = env_first("PGUSER", "POSTGRES_USER")
|
||||||
|
password = env_first("PGPASSWORD", "POSTGRES_PASSWORD")
|
||||||
|
|
||||||
|
missing = [
|
||||||
|
name
|
||||||
|
for name, value in (
|
||||||
|
("PGDATABASE", database),
|
||||||
|
("PGUSER", user),
|
||||||
|
("PGPASSWORD", password),
|
||||||
|
)
|
||||||
|
if not value
|
||||||
|
]
|
||||||
|
if missing:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Faltan variables: " + ", ".join(missing)
|
||||||
|
)
|
||||||
|
|
||||||
|
return psycopg2.connect(
|
||||||
|
host=env_first(
|
||||||
|
"PGHOST",
|
||||||
|
default="mv_ucepsa_postgres_hot",
|
||||||
|
),
|
||||||
|
port=int(
|
||||||
|
env_first("PGPORT", default="5432")
|
||||||
|
or "5432"
|
||||||
|
),
|
||||||
|
dbname=database,
|
||||||
|
user=user,
|
||||||
|
password=password,
|
||||||
|
connect_timeout=10,
|
||||||
|
application_name=(
|
||||||
|
"context_daily_governance_v0312"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def json_safe(value: Any) -> Any:
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return {
|
||||||
|
str(key): json_safe(item)
|
||||||
|
for key, item in value.items()
|
||||||
|
}
|
||||||
|
if isinstance(value, (list, tuple, set)):
|
||||||
|
return [
|
||||||
|
json_safe(item)
|
||||||
|
for item in value
|
||||||
|
]
|
||||||
|
if isinstance(value, (datetime, date)):
|
||||||
|
return value.isoformat()
|
||||||
|
if isinstance(value, Decimal):
|
||||||
|
return str(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def print_json(value: Any) -> None:
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
json_safe(value),
|
||||||
|
ensure_ascii=False,
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_agenda(cursor, limit: int) -> list[dict[str, Any]]:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT *
|
||||||
|
FROM
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_context_governance_daily_agenda_v1
|
||||||
|
ORDER BY agenda_rank
|
||||||
|
LIMIT %s
|
||||||
|
""",
|
||||||
|
(limit,),
|
||||||
|
)
|
||||||
|
return [
|
||||||
|
dict(row)
|
||||||
|
for row in cursor.fetchall()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_summary(cursor) -> dict[str, Any]:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT *
|
||||||
|
FROM
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_context_governance_daily_summary_v1
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
return dict(row) if row else {}
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_decision(
|
||||||
|
cursor,
|
||||||
|
decision_group_key: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT *
|
||||||
|
FROM
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_context_governance_decision_evidence_pack_v1
|
||||||
|
WHERE decision_group_key = %s
|
||||||
|
""",
|
||||||
|
(decision_group_key,),
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if not row:
|
||||||
|
raise RuntimeError(
|
||||||
|
"No existe la decisión "
|
||||||
|
f"{decision_group_key!r}"
|
||||||
|
)
|
||||||
|
return dict(row)
|
||||||
|
|
||||||
|
|
||||||
|
def agenda_command(limit: int) -> None:
|
||||||
|
with connect() as connection:
|
||||||
|
with connection.cursor(
|
||||||
|
cursor_factory=RealDictCursor,
|
||||||
|
) as cursor:
|
||||||
|
print_json({
|
||||||
|
"version": VERSION,
|
||||||
|
"summary": fetch_summary(cursor),
|
||||||
|
"agenda": fetch_agenda(cursor, limit),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def show_command(decision_group_key: str) -> None:
|
||||||
|
with connect() as connection:
|
||||||
|
with connection.cursor(
|
||||||
|
cursor_factory=RealDictCursor,
|
||||||
|
) as cursor:
|
||||||
|
print_json(
|
||||||
|
fetch_decision(
|
||||||
|
cursor,
|
||||||
|
decision_group_key,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot_payload(
|
||||||
|
agenda: list[dict[str, Any]],
|
||||||
|
) -> str:
|
||||||
|
payload = json.dumps(
|
||||||
|
json_safe(agenda),
|
||||||
|
ensure_ascii=False,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
).encode("utf-8")
|
||||||
|
return hashlib.sha256(payload).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot_command(
|
||||||
|
generated_by: str,
|
||||||
|
notes: str | None,
|
||||||
|
limit: int,
|
||||||
|
) -> None:
|
||||||
|
with connect() as connection:
|
||||||
|
with connection.cursor(
|
||||||
|
cursor_factory=RealDictCursor,
|
||||||
|
) as cursor:
|
||||||
|
agenda = fetch_agenda(cursor, limit)
|
||||||
|
summary = fetch_summary(cursor)
|
||||||
|
local_date = (
|
||||||
|
agenda[0]["agenda_local_date"]
|
||||||
|
if agenda
|
||||||
|
else summary.get(
|
||||||
|
"agenda_local_date"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if local_date is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"No se pudo resolver la fecha local."
|
||||||
|
)
|
||||||
|
|
||||||
|
state_hash = snapshot_payload(agenda)
|
||||||
|
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO
|
||||||
|
mv_loss_intelligence
|
||||||
|
.context_governance_agenda_snapshots (
|
||||||
|
tenant,
|
||||||
|
site,
|
||||||
|
agenda_local_date,
|
||||||
|
generated_by,
|
||||||
|
item_count,
|
||||||
|
source_state_hash,
|
||||||
|
notes,
|
||||||
|
official_eligible
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
'ucepsa',
|
||||||
|
'ucepsa_onpremise',
|
||||||
|
%s,
|
||||||
|
%s,
|
||||||
|
%s,
|
||||||
|
%s,
|
||||||
|
%s,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
RETURNING *
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
local_date,
|
||||||
|
generated_by,
|
||||||
|
len(agenda),
|
||||||
|
state_hash,
|
||||||
|
notes,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
snapshot = dict(cursor.fetchone())
|
||||||
|
|
||||||
|
for row in agenda:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO
|
||||||
|
mv_loss_intelligence
|
||||||
|
.context_governance_agenda_snapshot_items (
|
||||||
|
snapshot_id,
|
||||||
|
item_rank,
|
||||||
|
decision_group_key,
|
||||||
|
decision_status,
|
||||||
|
max_severity,
|
||||||
|
machine_id,
|
||||||
|
decision_family,
|
||||||
|
decision_title,
|
||||||
|
question_text,
|
||||||
|
recommended_action,
|
||||||
|
review_owner,
|
||||||
|
first_seen_at,
|
||||||
|
last_seen_at,
|
||||||
|
ended_at,
|
||||||
|
pending_age_hours,
|
||||||
|
attention_status,
|
||||||
|
review_due_at,
|
||||||
|
pending_root_case_count,
|
||||||
|
reviewed_root_case_count,
|
||||||
|
linked_root_case_count,
|
||||||
|
linked_episode_count,
|
||||||
|
root_case_ids,
|
||||||
|
episode_ids,
|
||||||
|
production_orders,
|
||||||
|
odoo_workorder_ids,
|
||||||
|
known_resolution_classifications,
|
||||||
|
evidence_json,
|
||||||
|
official_eligible
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
%s, %s, %s, %s, %s,
|
||||||
|
%s, %s, %s, %s, %s,
|
||||||
|
%s, %s, %s, %s, %s,
|
||||||
|
%s, %s, %s, %s, %s,
|
||||||
|
%s, %s, %s, %s, %s,
|
||||||
|
%s, %s, false
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
snapshot["snapshot_id"],
|
||||||
|
row["agenda_rank"],
|
||||||
|
row["decision_group_key"],
|
||||||
|
row["decision_status"],
|
||||||
|
row["max_severity"],
|
||||||
|
row["machine_id"],
|
||||||
|
row["decision_family"],
|
||||||
|
row["decision_title"],
|
||||||
|
row["question_text"],
|
||||||
|
row["recommended_action"],
|
||||||
|
row["review_owner"],
|
||||||
|
row["first_seen_at"],
|
||||||
|
row["last_seen_at"],
|
||||||
|
row["ended_at"],
|
||||||
|
row["pending_age_hours"],
|
||||||
|
row["attention_status"],
|
||||||
|
row["review_due_at"],
|
||||||
|
row["pending_root_case_count"],
|
||||||
|
row["reviewed_root_case_count"],
|
||||||
|
row["linked_root_case_count"],
|
||||||
|
row["linked_episode_count"],
|
||||||
|
row["root_case_ids"],
|
||||||
|
row["episode_ids"],
|
||||||
|
row["production_orders"],
|
||||||
|
row["odoo_workorder_ids"],
|
||||||
|
row[
|
||||||
|
"known_resolution_classifications"
|
||||||
|
],
|
||||||
|
Json({
|
||||||
|
"incident_codes":
|
||||||
|
row["incident_codes"],
|
||||||
|
"incident_scopes":
|
||||||
|
row["incident_scopes"],
|
||||||
|
"operator_names":
|
||||||
|
row["operator_names"],
|
||||||
|
"episode_evidence":
|
||||||
|
json_safe(
|
||||||
|
row["episode_evidence"]
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
print_json({
|
||||||
|
"version": VERSION,
|
||||||
|
"status": "snapshot_created",
|
||||||
|
"snapshot": snapshot,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_snapshot(
|
||||||
|
cursor,
|
||||||
|
snapshot_id: int,
|
||||||
|
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT *
|
||||||
|
FROM
|
||||||
|
mv_loss_intelligence
|
||||||
|
.context_governance_agenda_snapshots
|
||||||
|
WHERE snapshot_id = %s
|
||||||
|
""",
|
||||||
|
(snapshot_id,),
|
||||||
|
)
|
||||||
|
header = cursor.fetchone()
|
||||||
|
if not header:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"No existe el snapshot {snapshot_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT *
|
||||||
|
FROM
|
||||||
|
mv_loss_intelligence
|
||||||
|
.context_governance_agenda_snapshot_items
|
||||||
|
WHERE snapshot_id = %s
|
||||||
|
ORDER BY item_rank
|
||||||
|
""",
|
||||||
|
(snapshot_id,),
|
||||||
|
)
|
||||||
|
items = [
|
||||||
|
dict(row)
|
||||||
|
for row in cursor.fetchall()
|
||||||
|
]
|
||||||
|
return dict(header), items
|
||||||
|
|
||||||
|
|
||||||
|
def array_text(values: list[Any] | None) -> str:
|
||||||
|
if not values:
|
||||||
|
return "—"
|
||||||
|
return ", ".join(str(value) for value in values)
|
||||||
|
|
||||||
|
|
||||||
|
def dt_text(value: Any) -> str:
|
||||||
|
if value is None:
|
||||||
|
return "—"
|
||||||
|
if isinstance(value, datetime):
|
||||||
|
return value.strftime("%d/%m/%Y %H:%M:%S %z")
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def markdown_document(
|
||||||
|
title_date: Any,
|
||||||
|
generated_at: Any,
|
||||||
|
generated_by: str,
|
||||||
|
summary: dict[str, Any],
|
||||||
|
items: list[dict[str, Any]],
|
||||||
|
snapshot_id: int | None = None,
|
||||||
|
notes: str | None = None,
|
||||||
|
) -> str:
|
||||||
|
lines = [
|
||||||
|
f"# Revisión diaria de gobernanza — {title_date}",
|
||||||
|
"",
|
||||||
|
f"- Generada: {dt_text(generated_at)}",
|
||||||
|
f"- Generada por: {generated_by}",
|
||||||
|
f"- Snapshot: {snapshot_id if snapshot_id is not None else 'vista actual'}",
|
||||||
|
"- Modo: SHADOW",
|
||||||
|
"- Ledger oficial: desactivado",
|
||||||
|
]
|
||||||
|
|
||||||
|
if notes:
|
||||||
|
lines.extend([
|
||||||
|
f"- Notas: {notes}",
|
||||||
|
])
|
||||||
|
|
||||||
|
lines.extend([
|
||||||
|
"",
|
||||||
|
"## Resumen",
|
||||||
|
"",
|
||||||
|
f"- Decisiones pendientes: {summary.get('pending_decision_count', len(items))}",
|
||||||
|
f"- Críticas: {summary.get('pending_critical_decision_count', '—')}",
|
||||||
|
f"- Requieren atención: {summary.get('attention_decision_count', '—')}",
|
||||||
|
f"- Vencidas: {summary.get('overdue_decision_count', '—')}",
|
||||||
|
f"- Casos raíz pendientes: {summary.get('pending_root_case_count', '—')}",
|
||||||
|
f"- Evidencias enlazadas: {summary.get('linked_episode_count', '—')}",
|
||||||
|
"",
|
||||||
|
"## Agenda",
|
||||||
|
"",
|
||||||
|
])
|
||||||
|
|
||||||
|
if not items:
|
||||||
|
lines.append(
|
||||||
|
"No hay decisiones humanas pendientes."
|
||||||
|
)
|
||||||
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
rank = (
|
||||||
|
item.get("agenda_rank")
|
||||||
|
or item.get("item_rank")
|
||||||
|
)
|
||||||
|
lines.extend([
|
||||||
|
f"### {rank}. {item['max_severity']} · {item.get('machine_id') or 'GLOBAL'}",
|
||||||
|
"",
|
||||||
|
f"**{item['decision_title']}**",
|
||||||
|
"",
|
||||||
|
f"- Estado: {item['decision_status']}",
|
||||||
|
f"- Atención: {item['attention_status']}",
|
||||||
|
f"- Responsable: {item['review_owner']}",
|
||||||
|
f"- Inicio: {dt_text(item['first_seen_at'])}",
|
||||||
|
f"- Última evidencia: {dt_text(item['last_seen_at'])}",
|
||||||
|
f"- Antigüedad pendiente: {item['pending_age_hours']} h",
|
||||||
|
f"- Límite de revisión: {dt_text(item['review_due_at'])}",
|
||||||
|
f"- Casos raíz: {array_text(item.get('root_case_ids'))}",
|
||||||
|
f"- Episodios: {array_text(item.get('episode_ids'))}",
|
||||||
|
f"- Órdenes: {array_text(item.get('production_orders'))}",
|
||||||
|
f"- Workorders: {array_text(item.get('odoo_workorder_ids'))}",
|
||||||
|
f"- Resolución conocida: {array_text(item.get('known_resolution_classifications'))}",
|
||||||
|
"",
|
||||||
|
"**Pregunta**",
|
||||||
|
"",
|
||||||
|
item["question_text"],
|
||||||
|
"",
|
||||||
|
"**Acción recomendada**",
|
||||||
|
"",
|
||||||
|
item["recommended_action"],
|
||||||
|
"",
|
||||||
|
])
|
||||||
|
|
||||||
|
evidence = item.get("episode_evidence")
|
||||||
|
if evidence is None:
|
||||||
|
evidence_json = item.get(
|
||||||
|
"evidence_json",
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
evidence = evidence_json.get(
|
||||||
|
"episode_evidence",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
if evidence:
|
||||||
|
lines.extend([
|
||||||
|
"**Evidencia técnica resumida**",
|
||||||
|
"",
|
||||||
|
])
|
||||||
|
for row in evidence:
|
||||||
|
lines.append(
|
||||||
|
"- "
|
||||||
|
f"Episodio {row.get('episode_id')}: "
|
||||||
|
f"{row.get('incident_title')}; "
|
||||||
|
f"{row.get('latest_detail') or 'sin detalle adicional'}"
|
||||||
|
)
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
lines.extend([
|
||||||
|
f"`{item['decision_group_key']}`",
|
||||||
|
"",
|
||||||
|
])
|
||||||
|
|
||||||
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def export_markdown_command(
|
||||||
|
snapshot_id: int | None,
|
||||||
|
generated_by: str,
|
||||||
|
) -> None:
|
||||||
|
with connect() as connection:
|
||||||
|
with connection.cursor(
|
||||||
|
cursor_factory=RealDictCursor,
|
||||||
|
) as cursor:
|
||||||
|
if snapshot_id is not None:
|
||||||
|
header, items = fetch_snapshot(
|
||||||
|
cursor,
|
||||||
|
snapshot_id,
|
||||||
|
)
|
||||||
|
summary = {
|
||||||
|
"pending_decision_count":
|
||||||
|
header["item_count"],
|
||||||
|
"pending_critical_decision_count":
|
||||||
|
sum(
|
||||||
|
1
|
||||||
|
for row in items
|
||||||
|
if row["max_severity"] ==
|
||||||
|
"CRITICAL"
|
||||||
|
),
|
||||||
|
"attention_decision_count":
|
||||||
|
sum(
|
||||||
|
1
|
||||||
|
for row in items
|
||||||
|
if row["attention_status"] ==
|
||||||
|
"ATTENTION"
|
||||||
|
),
|
||||||
|
"overdue_decision_count":
|
||||||
|
sum(
|
||||||
|
1
|
||||||
|
for row in items
|
||||||
|
if row["attention_status"] ==
|
||||||
|
"OVERDUE"
|
||||||
|
),
|
||||||
|
"pending_root_case_count":
|
||||||
|
sum(
|
||||||
|
row[
|
||||||
|
"pending_root_case_count"
|
||||||
|
]
|
||||||
|
for row in items
|
||||||
|
),
|
||||||
|
"linked_episode_count":
|
||||||
|
sum(
|
||||||
|
row[
|
||||||
|
"linked_episode_count"
|
||||||
|
]
|
||||||
|
for row in items
|
||||||
|
),
|
||||||
|
}
|
||||||
|
document = markdown_document(
|
||||||
|
title_date=
|
||||||
|
header["agenda_local_date"],
|
||||||
|
generated_at=
|
||||||
|
header["generated_at"],
|
||||||
|
generated_by=
|
||||||
|
header["generated_by"],
|
||||||
|
summary=summary,
|
||||||
|
items=items,
|
||||||
|
snapshot_id=snapshot_id,
|
||||||
|
notes=header["notes"],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
items = fetch_agenda(cursor, 1000)
|
||||||
|
summary = fetch_summary(cursor)
|
||||||
|
document = markdown_document(
|
||||||
|
title_date=
|
||||||
|
summary.get(
|
||||||
|
"agenda_local_date"
|
||||||
|
),
|
||||||
|
generated_at=summary.get(
|
||||||
|
"observed_at",
|
||||||
|
datetime.now(),
|
||||||
|
),
|
||||||
|
generated_by=generated_by,
|
||||||
|
summary=summary,
|
||||||
|
items=items,
|
||||||
|
)
|
||||||
|
|
||||||
|
sys.stdout.write(document)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description=(
|
||||||
|
"Agenda diaria, evidencia y snapshots "
|
||||||
|
"de gobernanza UCEPSA."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
sub = parser.add_subparsers(
|
||||||
|
dest="command",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
agenda = sub.add_parser("agenda")
|
||||||
|
agenda.add_argument(
|
||||||
|
"--limit",
|
||||||
|
type=int,
|
||||||
|
default=50,
|
||||||
|
)
|
||||||
|
|
||||||
|
show = sub.add_parser("show")
|
||||||
|
show.add_argument(
|
||||||
|
"--decision-group-key",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
snapshot = sub.add_parser("snapshot")
|
||||||
|
snapshot.add_argument(
|
||||||
|
"--generated-by",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
snapshot.add_argument("--notes")
|
||||||
|
snapshot.add_argument(
|
||||||
|
"--limit",
|
||||||
|
type=int,
|
||||||
|
default=50,
|
||||||
|
)
|
||||||
|
|
||||||
|
export = sub.add_parser("export-markdown")
|
||||||
|
export.add_argument(
|
||||||
|
"--snapshot-id",
|
||||||
|
type=int,
|
||||||
|
)
|
||||||
|
export.add_argument(
|
||||||
|
"--generated-by",
|
||||||
|
default="MESAVAULT",
|
||||||
|
)
|
||||||
|
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
if args.command == "agenda":
|
||||||
|
agenda_command(args.limit)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.command == "show":
|
||||||
|
show_command(
|
||||||
|
args.decision_group_key
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.command == "snapshot":
|
||||||
|
snapshot_command(
|
||||||
|
generated_by=args.generated_by,
|
||||||
|
notes=args.notes,
|
||||||
|
limit=args.limit,
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.command == "export-markdown":
|
||||||
|
export_markdown_command(
|
||||||
|
snapshot_id=args.snapshot_id,
|
||||||
|
generated_by=args.generated_by,
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
raise RuntimeError("Comando no soportado")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Loading…
x
Reference in New Issue
Block a user