fix(ucepsa): simplify operator console tablet UI
This commit is contained in:
parent
94f6e9dc3f
commit
d525443de9
@ -22,10 +22,27 @@ PG_CONFIG = {
|
|||||||
"connect_timeout": 5,
|
"connect_timeout": 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
APP_TITLE = getenv("APP_TITLE", "MESAVAULT Edge-OEE - Consola de paros")
|
APP_TITLE = getenv("APP_TITLE", "MESAVAULT Edge-OEE - Consola de paros UCEPSA")
|
||||||
|
|
||||||
DEFAULT_MACHINE_ID = getenv("DEFAULT_MACHINE_ID", "CORT-01")
|
DEFAULT_MACHINE_ID = getenv("DEFAULT_MACHINE_ID", "CORT-01")
|
||||||
DEFAULT_ASSET = getenv("DEFAULT_ASSET", "revpi_oee_node_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__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -33,7 +50,7 @@ def get_conn():
|
|||||||
return psycopg2.connect(**PG_CONFIG)
|
return psycopg2.connect(**PG_CONFIG)
|
||||||
|
|
||||||
|
|
||||||
def fetch_pending_stops():
|
def fetch_pending_stops(machine_id):
|
||||||
sql = """
|
sql = """
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
@ -54,13 +71,14 @@ def fetch_pending_stops():
|
|||||||
cost_eur
|
cost_eur
|
||||||
FROM mv_hot.edge_oee_stops_recent
|
FROM mv_hot.edge_oee_stops_recent
|
||||||
WHERE classification_status = 'PENDING'
|
WHERE classification_status = 'PENDING'
|
||||||
|
AND machine_id = %(machine_id)s
|
||||||
ORDER BY started_at DESC
|
ORDER BY started_at DESC
|
||||||
LIMIT 50;
|
LIMIT 50;
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
|
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
|
||||||
cur.execute(sql)
|
cur.execute(sql, {"machine_id": machine_id})
|
||||||
return cur.fetchall()
|
return cur.fetchall()
|
||||||
|
|
||||||
|
|
||||||
@ -81,7 +99,7 @@ def fetch_causes():
|
|||||||
return cur.fetchall()
|
return cur.fetchall()
|
||||||
|
|
||||||
|
|
||||||
def classify_stop(stop_id, cause_code, operator_note):
|
def classify_stop(stop_id, machine_id, cause_code, operator_note):
|
||||||
sql = """
|
sql = """
|
||||||
UPDATE mv_hot.edge_oee_machine_stops
|
UPDATE mv_hot.edge_oee_machine_stops
|
||||||
SET
|
SET
|
||||||
@ -90,6 +108,7 @@ def classify_stop(stop_id, cause_code, operator_note):
|
|||||||
classification_status = 'CLASSIFIED',
|
classification_status = 'CLASSIFIED',
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = %(stop_id)s
|
WHERE id = %(stop_id)s
|
||||||
|
AND machine_id = %(machine_id)s
|
||||||
AND classification_status = 'PENDING';
|
AND classification_status = 'PENDING';
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -99,221 +118,313 @@ def classify_stop(stop_id, cause_code, operator_note):
|
|||||||
sql,
|
sql,
|
||||||
{
|
{
|
||||||
"stop_id": stop_id,
|
"stop_id": stop_id,
|
||||||
|
"machine_id": machine_id,
|
||||||
"cause_code": cause_code,
|
"cause_code": cause_code,
|
||||||
"operator_note": operator_note,
|
"operator_note": operator_note,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
return cur.rowcount
|
||||||
|
|
||||||
|
|
||||||
PAGE = """
|
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 = """
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="es">
|
<html lang="es">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>{{ app_title }}</title>
|
<title>{{ app_title }}</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta http-equiv="refresh" content="15">
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bg: #0f172a;
|
--bg: #0f172a;
|
||||||
--card: #111827;
|
--panel: #111827;
|
||||||
--card2: #1f2937;
|
--panel2: #1f2937;
|
||||||
--text: #e5e7eb;
|
--text: #e5e7eb;
|
||||||
--muted: #9ca3af;
|
--muted: #94a3b8;
|
||||||
--line: #374151;
|
--line: #334155;
|
||||||
--accent: #f97316;
|
--blue: #2563eb;
|
||||||
--danger: #ef4444;
|
--blue2: #60a5fa;
|
||||||
--ok: #22c55e;
|
--green: #22c55e;
|
||||||
--button: #2563eb;
|
--red: #ef4444;
|
||||||
--button-hover: #1d4ed8;
|
--orange: #f97316;
|
||||||
|
--input: #020617;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
padding: 18px 24px;
|
padding: 18px 22px;
|
||||||
border-bottom: 1px solid var(--line);
|
border-bottom: 1px solid var(--line);
|
||||||
background: #020617;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin: 0;
|
margin: 0 0 6px 0;
|
||||||
font-size: 24px;
|
font-size: 28px;
|
||||||
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
.subtitle {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
margin-top: 6px;
|
font-size: 15px;
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
padding: 24px;
|
padding: 22px;
|
||||||
max-width: 1400px;
|
max-width: 1800px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary {
|
.machine-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 8px;
|
||||||
|
margin: 0 0 22px 0;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin-bottom: 18px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-card {
|
.machine-tab {
|
||||||
background: var(--card);
|
padding: 9px 13px;
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 14px 18px;
|
background: var(--panel2);
|
||||||
min-width: 180px;
|
color: #dbeafe;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-card .label {
|
.machine-tab.active {
|
||||||
|
background: var(--blue);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--blue2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(220px, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 18px;
|
||||||
|
min-height: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 13px;
|
font-size: 15px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-card .value {
|
.card-value {
|
||||||
font-size: 26px;
|
font-size: 34px;
|
||||||
font-weight: bold;
|
line-height: 1;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-subtitle {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
background: var(--card);
|
min-width: 1300px;
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
th, td {
|
|
||||||
padding: 12px;
|
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
vertical-align: top;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
background: var(--card2);
|
background: var(--panel2);
|
||||||
color: #f9fafb;
|
color: white;
|
||||||
font-weight: bold;
|
text-align: left;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-bottom: 1px solid #1e293b;
|
||||||
|
vertical-align: top;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr:last-child td {
|
tr:last-child td {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.machine-main {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine-sub {
|
||||||
|
color: var(--muted);
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 4px 8px;
|
padding: 4px 9px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-open {
|
.badge-open {
|
||||||
background: rgba(239, 68, 68, 0.15);
|
background: rgba(239, 68, 68, 0.14);
|
||||||
color: #fca5a5;
|
color: #fecaca;
|
||||||
border: 1px solid rgba(239, 68, 68, 0.4);
|
border-color: rgba(239, 68, 68, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-closed {
|
.badge-closed {
|
||||||
background: rgba(34, 197, 94, 0.15);
|
background: rgba(34, 197, 94, 0.14);
|
||||||
color: #86efac;
|
color: #bbf7d0;
|
||||||
border: 1px solid rgba(34, 197, 94, 0.4);
|
border-color: rgba(34, 197, 94, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-pending {
|
.badge-pending {
|
||||||
background: rgba(249, 115, 22, 0.15);
|
background: rgba(249, 115, 22, 0.14);
|
||||||
color: #fdba74;
|
color: #fed7aa;
|
||||||
border: 1px solid rgba(249, 115, 22, 0.4);
|
border-color: rgba(249, 115, 22, 0.45);
|
||||||
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
select, input[type="text"] {
|
select,
|
||||||
|
input[type="text"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
min-width: 210px;
|
||||||
background: #020617;
|
padding: 10px 12px;
|
||||||
color: var(--text);
|
border-radius: 8px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 6px;
|
background: var(--input);
|
||||||
padding: 9px;
|
color: var(--text);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
background: var(--button);
|
padding: 10px 18px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 0;
|
||||||
|
background: var(--blue);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
font-weight: 700;
|
||||||
border-radius: 6px;
|
|
||||||
padding: 10px 14px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: bold;
|
font-size: 14px;
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
background: var(--button-hover);
|
background: #1d4ed8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
background: var(--card);
|
padding: 40px;
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 32px;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.small {
|
.footer-note {
|
||||||
font-size: 12px;
|
margin-top: 18px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nowrap {
|
@media (max-width: 900px) {
|
||||||
white-space: nowrap;
|
main {
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<h1>{{ app_title }}</h1>
|
<h1>{{ app_title }}</h1>
|
||||||
<div class="subtitle">
|
<div class="subtitle">
|
||||||
Clasificación manual de paros detectados automáticamente por Edge-OEE.
|
Clasificación manual de paros detectados por Edge-OEE ·
|
||||||
Última carga: {{ now }}
|
Máquina seleccionada: <strong>{{ selected_machine }}</strong> ·
|
||||||
|
Última carga: {{ loaded_at }}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<div class="summary">
|
<div class="machine-tabs">
|
||||||
<div class="summary-card">
|
{% for machine in allowed_machines %}
|
||||||
<div class="label">Paros pendientes</div>
|
<a
|
||||||
<div class="value">{{ stops|length }}</div>
|
class="machine-tab {% if machine == selected_machine %}active{% endif %}"
|
||||||
|
href="{{ url_for('index', machine_id=machine) }}"
|
||||||
|
>
|
||||||
|
{{ machine }}
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="cards">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">Paros pendientes</div>
|
||||||
|
<div class="card-value">{{ pending_count }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="summary-card">
|
<div class="card">
|
||||||
<div class="card-title">Máquina monitorizada</div>
|
<div class="card-title">Máquina monitorizada</div>
|
||||||
<div class="card-value">{{ default_machine }}</div>
|
<div class="card-value">{{ selected_machine }}</div>
|
||||||
<div class="card-subtitle">{{ default_asset }}</div>
|
<div class="card-subtitle">{{ default_asset }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if stops|length == 0 %}
|
<div class="table-wrap">
|
||||||
<div class="empty">
|
{% if stops %}
|
||||||
No hay paros pendientes de clasificar.
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -332,43 +443,40 @@ PAGE = """
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for stop in stops %}
|
{% for stop in stops %}
|
||||||
<tr>
|
<tr>
|
||||||
<form method="post" action="{{ url_for('classify') }}">
|
<td>#{{ stop.id }}</td>
|
||||||
<input type="hidden" name="stop_id" value="{{ stop.id }}">
|
|
||||||
|
|
||||||
<td class="nowrap">#{{ stop.id }}</td>
|
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<strong>{{ stop.machine_id or "-" }}</strong>
|
<div class="machine-main">{{ stop.machine_id }}</div>
|
||||||
<div class="small">{{ stop.asset }}</div>
|
<div class="machine-sub">{{ stop.asset }}</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="nowrap">
|
<td>{{ fmt_dt(stop.started_at) }}</td>
|
||||||
{{ stop.started_at }}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="nowrap">
|
<td>
|
||||||
{{ stop.ended_at or "Abierto" }}
|
{% if stop.ended_at %}
|
||||||
|
{{ fmt_dt(stop.ended_at) }}
|
||||||
|
{% else %}
|
||||||
|
Abierto
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
{% if stop.status == 'OPEN' %}
|
{% if stop.status == "OPEN" %}
|
||||||
<span class="badge badge-open">OPEN</span>
|
<span class="badge badge-open">OPEN</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge badge-closed">CLOSED</span>
|
<span class="badge badge-closed">{{ stop.status }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<br>
|
<br>
|
||||||
<span class="badge badge-pending">{{ stop.classification_status }}</span>
|
<span class="badge badge-pending">{{ stop.classification_status }}</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="nowrap">
|
<td>{{ fmt_num(stop.duration_min, 2) }} min</td>
|
||||||
{{ "%.2f"|format(stop.duration_min or 0) }} min
|
<td>{{ fmt_num(stop.cost_eur, 2) }} €</td>
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="nowrap">
|
|
||||||
{{ "%.2f"|format(stop.cost_eur or 0) }} €
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
|
<form method="post" action="{{ url_for('classify', stop_id=stop.id) }}">
|
||||||
|
<input type="hidden" name="machine_id" value="{{ selected_machine }}">
|
||||||
|
|
||||||
<select name="cause_code" required>
|
<select name="cause_code" required>
|
||||||
<option value="">Seleccionar causa...</option>
|
<option value="">Seleccionar causa...</option>
|
||||||
{% for cause in causes %}
|
{% for cause in causes %}
|
||||||
@ -380,19 +488,30 @@ PAGE = """
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="operator_note" placeholder="Nota opcional">
|
<input
|
||||||
|
type="text"
|
||||||
|
name="operator_note"
|
||||||
|
placeholder="Nota opcional"
|
||||||
|
value="{{ stop.operator_note or '' }}"
|
||||||
|
>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<button type="submit">Guardar</button>
|
<button type="submit">Guardar</button>
|
||||||
</td>
|
|
||||||
</form>
|
</form>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="empty">
|
||||||
|
No hay paros pendientes para {{ selected_machine }}.
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</main>
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
@ -400,30 +519,45 @@ PAGE = """
|
|||||||
|
|
||||||
@app.route("/", methods=["GET"])
|
@app.route("/", methods=["GET"])
|
||||||
def index():
|
def index():
|
||||||
stops = fetch_pending_stops()
|
selected_machine = resolve_machine_id(request.args.get("machine_id"))
|
||||||
|
|
||||||
|
stops = fetch_pending_stops(selected_machine)
|
||||||
causes = fetch_causes()
|
causes = fetch_causes()
|
||||||
|
|
||||||
return render_template_string(
|
return render_template_string(
|
||||||
PAGE,
|
HTML,
|
||||||
app_title=APP_TITLE,
|
app_title=APP_TITLE,
|
||||||
stops=stops,
|
stops=stops,
|
||||||
causes=causes,
|
causes=causes,
|
||||||
now=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
pending_count=len(stops),
|
||||||
default_machine=DEFAULT_MACHINE_ID,
|
selected_machine=selected_machine,
|
||||||
|
allowed_machines=ALLOWED_MACHINES,
|
||||||
|
default_machine=selected_machine,
|
||||||
default_asset=DEFAULT_ASSET,
|
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"])
|
@app.route("/classify/<int:stop_id>", methods=["POST"])
|
||||||
def classify():
|
def classify(stop_id):
|
||||||
stop_id = request.form.get("stop_id")
|
selected_machine = resolve_machine_id(
|
||||||
cause_code = request.form.get("cause_code")
|
request.form.get("machine_id") or request.args.get("machine_id")
|
||||||
operator_note = request.form.get("operator_note", "").strip() or None
|
)
|
||||||
|
|
||||||
if stop_id and cause_code:
|
cause_code = (request.form.get("cause_code") or "").strip()
|
||||||
classify_stop(stop_id, cause_code, operator_note)
|
operator_note = (request.form.get("operator_note") or "").strip()
|
||||||
|
|
||||||
return redirect(url_for("index"))
|
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"])
|
@app.route("/health", methods=["GET"])
|
||||||
@ -432,6 +566,14 @@ def health():
|
|||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute("SELECT 1;")
|
cur.execute("SELECT 1;")
|
||||||
|
cur.fetchone()
|
||||||
|
|
||||||
return {"status": "ok"}, 200
|
return {"status": "ok"}, 200
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return {"status": "error", "error": str(exc)}, 500
|
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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user