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 UCEPSA")
DEFAULT_MACHINE_ID = getenv("DEFAULT_MACHINE_ID", "CORT-01")
DEFAULT_ASSET = getenv("DEFAULT_ASSET", "revpi_oee_node_01")
ALLOWED_MACHINES = [
item.strip()
for item in getenv("ALLOWED_MACHINES", DEFAULT_MACHINE_ID).split(",")
if item.strip()
]
def resolve_machine_id(raw_value):
machine_id = (raw_value or DEFAULT_MACHINE_ID).strip()
if machine_id not in ALLOWED_MACHINES:
return DEFAULT_MACHINE_ID
return machine_id
app = Flask(__name__)
def get_conn():
return psycopg2.connect(**PG_CONFIG)
def fetch_pending_stops(machine_id):
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'
AND machine_id = %(machine_id)s
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, {"machine_id": machine_id})
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, machine_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 machine_id = %(machine_id)s
AND classification_status = 'PENDING';
"""
with get_conn() as conn:
with conn.cursor() as cur:
cur.execute(
sql,
{
"stop_id": stop_id,
"machine_id": machine_id,
"cause_code": cause_code,
"operator_note": operator_note,
},
)
return cur.rowcount
def fmt_dt(value):
if not value:
return ""
if isinstance(value, datetime):
return value.strftime("%Y-%m-%d %H:%M:%S")
return str(value)
def fmt_num(value, decimals=2):
if value is None:
return "0"
try:
return f"{float(value):.{decimals}f}"
except Exception:
return str(value)
HTML = """
{{ app_title }}
Paros pendientes
{{ pending_count }}
Máquina monitorizada
{{ selected_machine }}
{{ default_asset }}
{% if stops %}
{% else %}
No hay paros pendientes para {{ selected_machine }}.
{% endif %}
"""
@app.route("/", methods=["GET"])
def index():
selected_machine = resolve_machine_id(request.args.get("machine_id"))
stops = fetch_pending_stops(selected_machine)
causes = fetch_causes()
return render_template_string(
HTML,
app_title=APP_TITLE,
stops=stops,
causes=causes,
pending_count=len(stops),
selected_machine=selected_machine,
allowed_machines=ALLOWED_MACHINES,
default_machine=selected_machine,
default_asset=DEFAULT_ASSET,
loaded_at=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
fmt_dt=fmt_dt,
fmt_num=fmt_num,
)
@app.route("/classify/", methods=["POST"])
def classify(stop_id):
selected_machine = resolve_machine_id(
request.form.get("machine_id") or request.args.get("machine_id")
)
cause_code = (request.form.get("cause_code") or "").strip()
operator_note = (request.form.get("operator_note") or "").strip()
if cause_code:
classify_stop(
stop_id=stop_id,
machine_id=selected_machine,
cause_code=cause_code,
operator_note=operator_note,
)
return redirect(url_for("index", machine_id=selected_machine))
@app.route("/health", methods=["GET"])
def health():
try:
with get_conn() as conn:
with conn.cursor() as cur:
cur.execute("SELECT 1;")
cur.fetchone()
return {"status": "ok"}, 200
except Exception as exc:
return {"status": "error", "error": str(exc)}, 500
if __name__ == "__main__":
port = int(getenv("APP_PORT", "8080"))
app.run(host="0.0.0.0", port=port)