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 }}
Paros pendientes
{{ stops|length }}
Máquina demo
{{DEFAULT_MACHINE_ID}}
{% if stops|length == 0 %}
No hay paros pendientes de clasificar.
{% else %}
| ID |
Máquina |
Inicio |
Fin |
Estado |
Duración |
Coste |
Causa |
Nota |
Guardar |
{% for stop in stops %}
{% endfor %}
{% 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