import os from datetime import datetime import psycopg2 import psycopg2.extras from flask import Flask, redirect, render_template_string, request, url_for def getenv(name, default=None, required=False): value = os.getenv(name, default) if required and not value: raise RuntimeError(f"Missing required environment variable: {name}") return value PG_CONFIG = { "host": getenv("PGHOST", required=True), "port": int(getenv("PGPORT", "5432")), "dbname": getenv("PGDATABASE", required=True), "user": getenv("PGUSER", required=True), "password": getenv("PGPASSWORD", required=True), "connect_timeout": 5, } APP_TITLE = getenv("APP_TITLE", "MESAVAULT Edge-OEE - Consola de paros") DEFAULT_MACHINE_ID = getenv("DEFAULT_MACHINE_ID", "CORTADORA 01") app = Flask(__name__) def get_conn(): return psycopg2.connect(**PG_CONFIG) def fetch_pending_stops(): sql = """ SELECT id, tenant, site, vertical, asset, machine_id, order_id, started_at, ended_at, status, classification_status, cause_code, cause_name, operator_note, duration_min, cost_eur FROM mv_hot.edge_oee_stops_recent WHERE classification_status = 'PENDING' ORDER BY started_at DESC LIMIT 50; """ with get_conn() as conn: with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: cur.execute(sql) return cur.fetchall() def fetch_causes(): sql = """ SELECT cause_code, cause_name, description, is_planned FROM mv_hot.edge_oee_stop_cause_options ORDER BY sort_order, cause_name; """ with get_conn() as conn: with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: cur.execute(sql) return cur.fetchall() def classify_stop(stop_id, cause_code, operator_note): sql = """ UPDATE mv_hot.edge_oee_machine_stops SET cause_code = %(cause_code)s, operator_note = %(operator_note)s, classification_status = 'CLASSIFIED', updated_at = now() WHERE id = %(stop_id)s AND classification_status = 'PENDING'; """ with get_conn() as conn: with conn.cursor() as cur: cur.execute( sql, { "stop_id": stop_id, "cause_code": cause_code, "operator_note": operator_note, }, ) PAGE = """ {{ app_title }}

{{ app_title }}

Clasificación manual de paros detectados automáticamente por Edge-OEE. Última carga: {{ now }}
Paros pendientes
{{ stops|length }}
Máquina demo
{{DEFAULT_MACHINE_ID}}
{% if stops|length == 0 %}
No hay paros pendientes de clasificar.
{% else %} {% for stop in stops %} {% endfor %}
ID Máquina Inicio Fin Estado Duración Coste Causa Nota Guardar
#{{ stop.id }} {{ stop.machine_id or "-" }}
{{ stop.asset }}
{{ stop.started_at }} {{ stop.ended_at or "Abierto" }} {% if stop.status == 'OPEN' %} OPEN {% else %} CLOSED {% endif %}
{{ stop.classification_status }}
{{ "%.2f"|format(stop.duration_min or 0) }} min {{ "%.2f"|format(stop.cost_eur or 0) }} €
{% endif %}
""" @app.route("/", methods=["GET"]) def index(): stops = fetch_pending_stops() causes = fetch_causes() return render_template_string( PAGE, app_title=APP_TITLE, stops=stops, causes=causes, now=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), default_machine=DEFAULT_MACHINE_ID, ) @app.route("/classify", methods=["POST"]) def classify(): stop_id = request.form.get("stop_id") cause_code = request.form.get("cause_code") operator_note = request.form.get("operator_note", "").strip() or None if stop_id and cause_code: classify_stop(stop_id, cause_code, operator_note) return redirect(url_for("index")) @app.route("/health", methods=["GET"]) def health(): try: with get_conn() as conn: with conn.cursor() as cur: cur.execute("SELECT 1;") return {"status": "ok"}, 200 except Exception as exc: return {"status": "error", "error": str(exc)}, 500