From 57f67332ebb20b916f5b46fefcd5c1af72fd5d25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leopoldo=20Rodr=C3=ADguez?= Date: Wed, 16 Sep 2026 18:05:16 +0000 Subject: [PATCH] actas: formularios de prueba V5/V4 + checklist de ensamble + backend - backend Flask (backend/): recibe formularios firmados, guarda JSON por numero de serie en /srv/projects/drogheda/actas, indice CSV, GET /api/actas + systemd unit + README de instalacion (nginx /api proxy) - formularios (docs/formularios/): acta.css/acta.js (motor con autoguardado y envio), V5 fiel al procedimiento verificado, V4 borrador adaptado, checklist de ensamble borrador; firma = nombre + conformidad - docs/actas/index.md + guia-ensamble-qc.md; nav actualizada - probado end-to-end (headless): envio + validacion + guardado por serie Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01YJ4eAB6zjQT49RVwpgfKdt --- backend/README.md | 63 +++++++++++++ backend/actas-hbox.service | 16 ++++ backend/app.py | 108 +++++++++++++++++++++ backend/requirements.txt | 2 + docs/actas/index.md | 34 +++++++ docs/formularios/acta.css | 57 +++++++++++ docs/formularios/acta.js | 156 +++++++++++++++++++++++++++++++ docs/formularios/ensamble.html | 83 ++++++++++++++++ docs/formularios/v4.html | 85 +++++++++++++++++ docs/formularios/v5.html | 91 ++++++++++++++++++ docs/montaje/guia-ensamble-qc.md | 52 +++++++++++ mkdocs.yml | 7 ++ scripts/deploy.sh | 0 13 files changed, 754 insertions(+) create mode 100644 backend/README.md create mode 100644 backend/actas-hbox.service create mode 100644 backend/app.py create mode 100644 backend/requirements.txt create mode 100644 docs/actas/index.md create mode 100644 docs/formularios/acta.css create mode 100644 docs/formularios/acta.js create mode 100644 docs/formularios/ensamble.html create mode 100644 docs/formularios/v4.html create mode 100644 docs/formularios/v5.html create mode 100644 docs/montaje/guia-ensamble-qc.md mode change 100644 => 100755 scripts/deploy.sh diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..ab97d95 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,63 @@ +# Backend de actas — configuración en el servidor (una sola vez) + +Servicio Flask que recibe los formularios firmados de los técnicos y los guarda como +JSON por número de serie en `/srv/projects/drogheda/actas/`. Corre detrás de nginx en +`127.0.0.1:8081`; nginx expone `/api/` en el mismo dominio, con la misma contraseña. + +## 1. Instalar (en el servidor, como leo) + +```bash +cd /srv/projects/drogheda/hbox-docs/backend +python3 -m venv venv +./venv/bin/pip install -r requirements.txt + +# carpeta donde se guardan las actas +mkdir -p /srv/projects/drogheda/actas +``` + +## 2. Servicio systemd (arranque automático) + +```bash +sudo cp actas-hbox.service /etc/systemd/system/actas-hbox.service +sudo systemctl daemon-reload +sudo systemctl enable --now actas-hbox +systemctl status actas-hbox # debe decir "active (running)" +curl -s http://127.0.0.1:8081/api/salud # {"ok":true,...} +``` + +## 3. nginx — exponer /api/ en el mismo dominio + +Dentro del bloque `server { ... }` de `drogheda.helgen.online` (el mismo archivo +`/etc/nginx/sites-enabled/drogheda.helgen.online.conf`, en la parte de `listen 443`), +agrega **antes** del `location / {`: + +```nginx + location /api/ { + proxy_pass http://127.0.0.1:8081; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # hereda el auth_basic del server; si quieres, puedes repetirlo aquí. + } +``` + +Luego: + +```bash +sudo nginx -t && sudo systemctl reload nginx +curl -u leo:TU_PASS https://drogheda.helgen.online/api/salud # {"ok":true,...} +``` + +## 4. Ver las actas recibidas + +- Índice rápido por tipo: `/srv/projects/drogheda/actas//_indice.csv` (ábrelo en Excel). +- Listado JSON: `https://drogheda.helgen.online/api/actas` +- Un acta concreta: `https://drogheda.helgen.online/api/actas/prueba-v5/V5-07_....json` +- Respaldo: basta con copiar la carpeta `/srv/projects/drogheda/actas/`. + +## Actualizar el backend + +Tras un `git pull` que cambie `app.py`: + +```bash +sudo systemctl restart actas-hbox +``` diff --git a/backend/actas-hbox.service b/backend/actas-hbox.service new file mode 100644 index 0000000..93b2c4a --- /dev/null +++ b/backend/actas-hbox.service @@ -0,0 +1,16 @@ +[Unit] +Description=Actas HBOX — recepcion de formularios de tecnicos +After=network.target + +[Service] +User=leo +Group=leo +WorkingDirectory=/srv/projects/drogheda/hbox-docs/backend +Environment=ACTAS_DIR=/srv/projects/drogheda/actas +Environment=PORT=8081 +ExecStart=/srv/projects/drogheda/hbox-docs/backend/venv/bin/gunicorn -w 2 -b 127.0.0.1:8081 app:app +Restart=on-failure +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 0000000..494bb88 --- /dev/null +++ b/backend/app.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +"""Actas HBOX — servicio mínimo para recibir formularios firmados de los técnicos. + +Guarda cada envío como un JSON por número de serie, y mantiene un índice CSV por tipo +de acta. Sin base de datos: un archivo por envío, fácil de respaldar y de leer. + +Rutas: + POST /api/actas -> recibe un acta (JSON), la guarda, devuelve {ok, archivo} + GET /api/actas -> lista los envíos (para ingeniería) + GET /api/actas// -> devuelve un acta concreta + GET /api/salud -> healthcheck + +Variables de entorno: + ACTAS_DIR carpeta donde se guardan las actas (default /srv/projects/drogheda/actas) +""" +import os, re, json, csv, datetime +from flask import Flask, request, jsonify, abort + +app = Flask(__name__) +BASE = os.environ.get("ACTAS_DIR", "/srv/projects/drogheda/actas") + +SAFE = re.compile(r"[^A-Za-z0-9_\-]") + +def slug(s, n=40): + return SAFE.sub("", str(s or "").strip())[:n] + +@app.get("/api/salud") +def salud(): + return jsonify(ok=True, servicio="actas-hbox") + +@app.post("/api/actas") +def crear(): + try: + d = request.get_json(force=True) + except Exception: + return jsonify(ok=False, error="JSON inválido"), 400 + if not isinstance(d, dict): + return jsonify(ok=False, error="Se esperaba un objeto"), 400 + + tipo = slug(d.get("tipo")) # p.ej. prueba-v5, prueba-v4, ensamble + serie = slug(d.get("serie"), 20) # p.ej. V5-07 + tecnico = str(d.get("tecnico", "")).strip() + if not tipo or not serie: + return jsonify(ok=False, error="Faltan 'tipo' o 'serie'"), 400 + if not tecnico: + return jsonify(ok=False, error="Falta el nombre del técnico"), 400 + if not d.get("conforme"): + return jsonify(ok=False, error="Falta la conformidad del técnico"), 400 + + ts = datetime.datetime.now().replace(microsecond=0) + iso = ts.isoformat() + d["_recibido"] = iso + d["_ip"] = request.headers.get("X-Forwarded-For", request.remote_addr) + + carpeta = os.path.join(BASE, tipo) + os.makedirs(carpeta, exist_ok=True) + archivo = f"{serie}_{iso.replace(':', '-')}.json" + ruta = os.path.join(carpeta, archivo) + with open(ruta, "w", encoding="utf-8") as f: + json.dump(d, f, ensure_ascii=False, indent=2) + + # índice CSV acumulado por tipo (fácil de abrir en Excel) + idx = os.path.join(carpeta, "_indice.csv") + nuevo = not os.path.exists(idx) + with open(idx, "a", newline="", encoding="utf-8") as f: + w = csv.writer(f) + if nuevo: + w.writerow(["recibido", "serie", "tecnico", "resultado", "archivo"]) + w.writerow([iso, serie, tecnico, d.get("resultado", ""), archivo]) + + return jsonify(ok=True, archivo=f"{tipo}/{archivo}") + +@app.get("/api/actas") +def listar(): + filas = [] + if os.path.isdir(BASE): + for tipo in sorted(os.listdir(BASE)): + carpeta = os.path.join(BASE, tipo) + if not os.path.isdir(carpeta): + continue + for fn in sorted(os.listdir(carpeta)): + if fn.endswith(".json"): + try: + with open(os.path.join(carpeta, fn), encoding="utf-8") as f: + d = json.load(f) + except Exception: + continue + filas.append({ + "tipo": tipo, "archivo": f"{tipo}/{fn}", + "serie": d.get("serie"), "tecnico": d.get("tecnico"), + "resultado": d.get("resultado"), "recibido": d.get("_recibido"), + }) + filas.sort(key=lambda r: (r.get("recibido") or ""), reverse=True) + return jsonify(ok=True, total=len(filas), actas=filas) + +@app.get("/api/actas//") +def leer(tipo, archivo): + tipo = slug(tipo); archivo = slug(archivo, 120) + if not archivo.endswith("json"): + archivo += ".json" if not archivo.endswith(".json") else "" + ruta = os.path.join(BASE, tipo, archivo) + if not os.path.isfile(ruta): + abort(404) + with open(ruta, encoding="utf-8") as f: + return app.response_class(f.read(), mimetype="application/json") + +if __name__ == "__main__": + app.run(host="127.0.0.1", port=int(os.environ.get("PORT", "8081"))) diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..8d24d2d --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,2 @@ +flask>=3.0 +gunicorn>=21.2 diff --git a/docs/actas/index.md b/docs/actas/index.md new file mode 100644 index 0000000..c9681c7 --- /dev/null +++ b/docs/actas/index.md @@ -0,0 +1,34 @@ +# Actas y control de calidad + +Formularios que el técnico llena **en pantalla mientras prueba o ensambla** cada +tarjeta. Al terminar se firman (nombre + conformidad) y se envían al servidor, donde +quedan registrados por número de serie. Así queda constancia de que se siguió el +procedimiento en cada unidad. + +## Formularios + +**[▶ Acta de prueba — EinarBoard V5](../formularios/v5.html)** +Lote de 15 tarjetas (V5-01 … V5-15). Procedimiento verificado §1–§6: inspección, +continuidad, rieles 3V3 / 5 V / 24 V, flasheo DFU y POST. + +**[▶ Acta de prueba — EinarBoard V4](../formularios/v4.html)** — *borrador* +V4-01 … V4-15. Adaptado de V5; la sección de MCU/firmware está marcada como +**pendiente de definir** para V4. Revisar antes de uso oficial. + +**[▶ Checklist de ensamble — HBOX](../formularios/ensamble.html)** — *borrador* +Control de calidad durante el armado: recepción de PCBA, módulos, mecánica, arnés, +etiquetado y energizado final. Ver la [guía de ensamble](../montaje/guia-ensamble-qc.md). + +## Cómo se usan + +1. Abre el formulario de la versión que estás probando y **elige el número de serie**. +2. Ve llenando cada sección conforme avanzas. El formulario **se guarda solo** en el + navegador: si se cierra, al reabrir con el mismo número de serie recuperas lo escrito. +3. Al final marca el **resultado** (Aprobada / Falla), escribe tu **nombre** y confirma + que seguiste el procedimiento. +4. Pulsa **Enviar acta firmada**. Queda registrada en el servidor. + +!!! note "Para ingeniería" + Los envíos se guardan en el servidor por tipo y número de serie. Índice rápido en + `…/actas//_indice.csv`, o el listado completo en + [`/api/actas`](/api/actas){:target="_blank"}. diff --git a/docs/formularios/acta.css b/docs/formularios/acta.css new file mode 100644 index 0000000..b3706fb --- /dev/null +++ b/docs/formularios/acta.css @@ -0,0 +1,57 @@ +/* Estilo compartido de las actas HBOX — pensado para tablet/laptop en el banco */ +:root{ + --naranja:#e8590c; --naranja-suave:#fff1e8; --tinta:#1d1c19; --tinta2:#5a564d; + --linea:#e2ddd2; --papel:#faf8f4; --tarjeta:#ffffff; --ok:#2f9e44; --falla:#e03131; + --ok-bg:#ebfbee; --falla-bg:#fff5f5; --aviso:#f08c00; --aviso-bg:#fff9db; +} +*{box-sizing:border-box} +body{margin:0;background:var(--papel);color:var(--tinta); + font:16px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;-webkit-text-size-adjust:100%} +.wrap{max-width:820px;margin:0 auto;padding:16px 16px 120px} +header.top{background:var(--tinta);color:#fff;padding:16px;border-radius:12px;margin-bottom:16px} +header.top h1{margin:0;font-size:20px} +header.top p{margin:4px 0 0;color:#d7d2c8;font-size:14px} +.borrador{background:var(--aviso-bg);border:1px solid var(--aviso);color:#a5620a; + padding:10px 14px;border-radius:10px;margin-bottom:16px;font-size:14px} +.datos{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:8px} +@media(max-width:560px){.datos{grid-template-columns:1fr}} +label{display:block;font-weight:600;font-size:14px;margin-bottom:4px} +input[type=text],input[type=number],select,textarea{ + width:100%;padding:11px 12px;border:1px solid var(--linea);border-radius:9px; + font-size:16px;background:#fff;color:var(--tinta)} +input:focus,select:focus,textarea:focus{outline:2px solid var(--naranja);border-color:var(--naranja)} +section.paso{background:var(--tarjeta);border:1px solid var(--linea);border-radius:12px; + padding:14px 16px;margin:14px 0} +section.paso h2{margin:0 0 4px;font-size:17px;color:var(--naranja)} +section.paso .sub{color:var(--tinta2);font-size:13.5px;margin:0 0 10px} +.item{padding:10px 0;border-top:1px solid var(--linea)} +.item:first-of-type{border-top:0} +.chk{display:flex;gap:12px;align-items:flex-start;cursor:pointer} +.chk input{width:26px;height:26px;margin:1px 0 0;flex:0 0 auto;accent-color:var(--naranja)} +.chk .txt{flex:1} +.medida{display:flex;flex-wrap:wrap;gap:8px 12px;align-items:center} +.medida .txt{flex:1 1 100%;font-weight:600} +.medida .esp{flex:1 1 100%;color:var(--tinta2);font-size:13px;margin-top:-2px} +.medida input{max-width:150px} +.medida .u{color:var(--tinta2)} +.nota-item{color:var(--tinta2);font-size:14px} +.aviso{background:var(--aviso-bg);border-left:3px solid var(--aviso);padding:8px 12px; + border-radius:0 8px 8px 0;font-size:13.5px;margin:6px 0} +.firma{background:var(--naranja-suave);border:1px solid #ffd8a8} +.resultado{display:flex;gap:10px;margin-top:6px} +.resultado label{flex:1;border:2px solid var(--linea);border-radius:10px;padding:12px; + text-align:center;cursor:pointer;font-weight:700;margin:0} +.resultado input{position:absolute;opacity:0} +.resultado label:has(input:checked).ok{border-color:var(--ok);background:var(--ok-bg);color:var(--ok)} +.resultado label:has(input:checked).falla{border-color:var(--falla);background:var(--falla-bg);color:var(--falla)} +.barra{position:fixed;left:0;right:0;bottom:0;background:#fff;border-top:1px solid var(--linea); + padding:12px 16px;display:flex;gap:12px;align-items:center;justify-content:space-between} +.barra .estado{font-size:13px;color:var(--tinta2)} +button.enviar{background:var(--naranja);color:#fff;border:0;border-radius:10px; + padding:13px 22px;font-size:16px;font-weight:700;cursor:pointer} +button.enviar:disabled{opacity:.5;cursor:not-allowed} +.msg{padding:12px 14px;border-radius:10px;margin-top:12px;font-weight:600;display:none} +.msg.ok{display:block;background:var(--ok-bg);color:var(--ok);border:1px solid var(--ok)} +.msg.err{display:block;background:var(--falla-bg);color:var(--falla);border:1px solid var(--falla)} +.enviada{opacity:.6;pointer-events:none} +a.volver{color:var(--naranja);font-size:14px;text-decoration:none} diff --git a/docs/formularios/acta.js b/docs/formularios/acta.js new file mode 100644 index 0000000..c5d5f58 --- /dev/null +++ b/docs/formularios/acta.js @@ -0,0 +1,156 @@ +/* Motor de actas HBOX — renderiza el formulario desde una config y lo envía al servidor. + Uso: luego Acta.render(CONFIG) + Tipos de ítem: check | medida | texto | radio | nota + Autoguardado en localStorage por (tipo + serie). Envío: POST /api/actas */ +(function () { + const $ = (t, a, ...k) => { const e = document.createElement(t); if (a) for (const p in a){ if(p==='html')e.innerHTML=a[p]; else if(p==='text')e.textContent=a[p]; else e.setAttribute(p,a[p]); } k.forEach(c=>e.append(c)); return e; }; + + function render(cfg) { + const root = document.getElementById('acta'); + const key = () => 'acta:' + cfg.tipo + ':' + (serieSel.value || ''); + const wrap = $('div', { class: 'wrap' }); + + const head = $('header', { class: 'top' }); + head.append($('h1', { text: cfg.titulo })); + head.append($('p', { text: cfg.subtitulo || 'Helgen Technologies · llena mientras pruebas la tarjeta' })); + wrap.append(head); + wrap.append($('a', { class: 'volver', href: '../actas/' , text: '← Volver a Actas' })); + + if (cfg.borrador) + wrap.append($('div', { class: 'borrador', html: '⚠ BORRADOR — este formulario aún debe revisarse con ingeniería antes de usarlo como registro oficial.' })); + + // datos de cabecera + const datos = $('div', { class: 'datos' }); + const serieSel = $('select', { id: 'f_serie' }); + serieSel.append($('option', { value: '', text: '— elegir —' })); + for (let i = 1; i <= (cfg.series || 15); i++) { + const s = cfg.prefijo + '-' + String(i).padStart(2, '0'); + serieSel.append($('option', { value: s, text: s })); + } + const tecnico = $('input', { type: 'text', id: 'f_tecnico', placeholder: 'Nombre y apellido' }); + const dS = $('div'); dS.append($('label', { text: 'Número de serie de la tarjeta' }), serieSel); + const dT = $('div'); dT.append($('label', { text: 'Técnico' }), tecnico); + datos.append(dS, dT); + const cab = $('section', { class: 'paso' }); cab.append(datos); + wrap.append(cab); + + // secciones del procedimiento + cfg.secciones.forEach((sec, si) => { + const s = $('section', { class: 'paso' }); + s.append($('h2', { text: sec.titulo })); + if (sec.sub) s.append($('p', { class: 'sub', text: sec.sub })); + (sec.items || []).forEach((it, ii) => { + const id = `s${si}_${ii}`; + const box = $('div', { class: 'item' }); + if (it.aviso) box.append($('div', { class: 'aviso', html: it.aviso })); + if (it.tipo === 'check') { + const lab = $('label', { class: 'chk' }); + lab.append($('input', { type: 'checkbox', 'data-id': id, 'data-lbl': it.txt })); + lab.append($('span', { class: 'txt', html: it.txt })); + box.append(lab); + } else if (it.tipo === 'medida') { + box.classList.add('medida'); + box.append($('span', { class: 'txt', text: it.txt })); + if (it.esperado) box.append($('span', { class: 'esp', html: 'Esperado: ' + it.esperado })); + box.append($('input', { type: 'text', inputmode: 'decimal', 'data-id': id, 'data-lbl': it.txt, placeholder: 'medido' })); + if (it.u) box.append($('span', { class: 'u', text: it.u })); + } else if (it.tipo === 'texto') { + box.append($('label', { text: it.txt })); + box.append($('textarea', { rows: '2', 'data-id': id, 'data-lbl': it.txt, placeholder: it.ph || '' })); + } else if (it.tipo === 'nota') { + box.append($('div', { class: 'nota-item', html: it.txt })); + } + s.append(box); + }); + wrap.append(s); + }); + + // firma / conformidad + const firma = $('section', { class: 'paso firma' }); + firma.append($('h2', { text: 'Cierre y firma' })); + firma.append($('p', { class: 'sub', text: 'Al enviar, quedas registrado como responsable de esta prueba.' })); + const res = $('div', { class: 'resultado' }); + const rOK = $('label', { class: 'ok' }); rOK.append($('input', { type: 'radio', name: 'resultado', value: 'OK' }), document.createTextNode('APROBADA')); + const rNO = $('label', { class: 'falla' }); rNO.append($('input', { type: 'radio', name: 'resultado', value: 'FALLA' }), document.createTextNode('FALLA')); + res.append(rOK, rNO); + firma.append($('label', { text: 'Resultado final' }), res); + const conf = $('label', { class: 'chk', style: 'margin-top:14px' }); + conf.append($('input', { type: 'checkbox', id: 'f_conforme' })); + conf.append($('span', { class: 'txt', html: 'Confirmo que seguí el procedimiento completo y que los datos anotados son verídicos.' })); + firma.append(conf); + wrap.append(firma); + + const msg = $('div', { class: 'msg', id: 'f_msg' }); + wrap.append(msg); + root.append(wrap); + + // barra fija de envío + const barra = $('div', { class: 'barra' }); + const estado = $('span', { class: 'estado', id: 'f_estado', text: 'Sin guardar' }); + const btn = $('button', { class: 'enviar', id: 'f_enviar', text: 'Enviar acta firmada' }); + barra.append(estado, btn); + document.body.append(barra); + + // ---- autoguardado ---- + function snapshot() { + const r = {}; + wrap.querySelectorAll('[data-id]').forEach(el => { + r[el.dataset.id] = { lbl: el.dataset.lbl, val: el.type === 'checkbox' ? el.checked : el.value }; + }); + const rr = wrap.querySelector('input[name=resultado]:checked'); + return { tipo: cfg.tipo, serie: serieSel.value, tecnico: tecnico.value, + conforme: document.getElementById('f_conforme').checked, + resultado: rr ? rr.value : '', respuestas: r }; + } + let t; + function guardar() { + if (!serieSel.value) { estado.textContent = 'Elige el número de serie'; return; } + clearTimeout(t); + t = setTimeout(() => { + try { localStorage.setItem(key(), JSON.stringify(snapshot())); } catch (e) {} + estado.textContent = 'Guardado local ' + new Date().toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' }); + }, 400); + } + function cargar() { + let d; try { d = JSON.parse(localStorage.getItem(key()) || 'null'); } catch (e) {} + if (!d) { estado.textContent = 'Nueva acta'; return; } + tecnico.value = d.tecnico || ''; + document.getElementById('f_conforme').checked = !!d.conforme; + if (d.resultado) { const el = wrap.querySelector(`input[name=resultado][value="${d.resultado}"]`); if (el) el.checked = true; } + for (const k in (d.respuestas || {})) { + const el = wrap.querySelector(`[data-id="${k}"]`); if (!el) continue; + if (el.type === 'checkbox') el.checked = !!d.respuestas[k].val; else el.value = d.respuestas[k].val || ''; + } + estado.textContent = 'Borrador recuperado'; + } + wrap.addEventListener('input', guardar); + serieSel.addEventListener('change', cargar); + + // ---- envío ---- + btn.addEventListener('click', async () => { + const d = snapshot(); + if (!d.serie) return fail('Elige el número de serie de la tarjeta.'); + if (!d.tecnico.trim()) return fail('Escribe tu nombre.'); + if (!d.resultado) return fail('Marca el resultado final (Aprobada o Falla).'); + if (!d.conforme) return fail('Debes confirmar que seguiste el procedimiento.'); + btn.disabled = true; btn.textContent = 'Enviando…'; + try { + const r = await fetch('/api/actas', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(d) }); + const j = await r.json(); + if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status)); + msg.className = 'msg ok'; + msg.innerHTML = '✓ Acta enviada y registrada como ' + d.serie + '. Puedes cerrar esta página.'; + try { localStorage.removeItem(key()); } catch (e) {} + wrap.classList.add('enviada'); estado.textContent = 'Enviada'; + btn.textContent = 'Enviada ✓'; window.scrollTo(0, document.body.scrollHeight); + } catch (e) { + fail('No se pudo enviar: ' + e.message + '. Revisa tu conexión e inténtalo otra vez (tu borrador está guardado).'); + btn.disabled = false; btn.textContent = 'Enviar acta firmada'; + } + }); + function fail(m) { msg.className = 'msg err'; msg.textContent = m; window.scrollTo(0, document.body.scrollHeight); } + + cargar(); + } + window.Acta = { render }; +})(); diff --git a/docs/formularios/ensamble.html b/docs/formularios/ensamble.html new file mode 100644 index 0000000..fc8da1b --- /dev/null +++ b/docs/formularios/ensamble.html @@ -0,0 +1,83 @@ + + + + + +Checklist de ensamble — HBOX (borrador) + + + +
+ + + + diff --git a/docs/formularios/v4.html b/docs/formularios/v4.html new file mode 100644 index 0000000..65a64d0 --- /dev/null +++ b/docs/formularios/v4.html @@ -0,0 +1,85 @@ + + + + + +Acta de prueba — EinarBoard V4 (borrador) + + + +
+ + + + diff --git a/docs/formularios/v5.html b/docs/formularios/v5.html new file mode 100644 index 0000000..2daee9a --- /dev/null +++ b/docs/formularios/v5.html @@ -0,0 +1,91 @@ + + + + + +Acta de prueba — EinarBoard V5 + + + +
+ + + + diff --git a/docs/montaje/guia-ensamble-qc.md b/docs/montaje/guia-ensamble-qc.md new file mode 100644 index 0000000..75a5cb5 --- /dev/null +++ b/docs/montaje/guia-ensamble-qc.md @@ -0,0 +1,52 @@ +# Guía de ensamble y control de calidad — HBOX + +!!! warning "Borrador para revisión" + Esta guía es un **borrador** basado en el BOM de ensamble y el pinout de arneses. + Ingeniería debe validarla antes de adoptarla como procedimiento oficial. El + **checklist interactivo** que la acompaña está en + [Actas → Checklist de ensamble](../actas/index.md). + +El objetivo es que cada HBOX se arme siempre igual y quede un registro firmado por +unidad. El ensamble parte de una **PCBA ya probada y aprobada** en su acta de prueba +(V4 o V5); esta guía cubre lo que ocurre *después* de esa prueba: montaje de módulos, +mecánica, arnés y energizado final. + +## Orden de trabajo + +1. **Recepción de la PCBA.** Inspección visual de la tarjeta que llega de JLCPCB: + componentes completos, sin soldadura fría ni puentes, conectores rectos. +2. **Soldadura manual** (si el lote la requiere): terminal de potencia P1, conectores + JST, relevador K1, botón SW1, electrolíticos radiales y zócalos de módulos. Cuidar + polaridad de diodos y electrolíticos. +3. **Prueba eléctrica de la tarjeta** → se registra en su acta de prueba + ([V5](../formularios/v5.html) / [V4](../formularios/v4.html)). **No se montan módulos + hasta que la tarjeta pase.** +4. **Módulos enchufables** sobre la tarjeta aprobada: MCU, simpleRTK2B en MOD1, módulo + 4G. Verificar orientación **antes** de energizar. +5. **Mecánica:** fijar la tarjeta con separadores en sus barrenos, montar ventilador + (dirección de flujo), alinear conectores de panel con el gabinete. +6. **Arnés:** conectar cada JST contra el pinout, sin intercambiar conectores. +7. **Etiquetado:** número de serie del equipo, visible y legible. +8. **Energizado final** del equipo armado y registro del acta de ensamble. + +## Referencia de conectores (arnés) + +| Conector | Función | Puntos clave | +|---|---|---| +| **P1** GX16-5 | Alimentación de máquina + ignición | 1 = 24V ⚡, 3 = ignición, 2 = GND | +| **P2** | Entrada 5 V (Mean Well SD-15B) | 1 = VEH5V ⚡, 2 = GND | +| **P4** | Batería 1S4P + NTC | 1 = BAT+ ⚡, 3 = NTC, 2 = GND | +| **P6** | Ventilador | 1 = 5V_FAN ⚡, 2 = retorno, 4 = tacómetro | +| **P11** | Corte de energía (SYS→MAIN 5 V) | 1 = MAIN5V ⚡, 2 = SYS5V ⚡ | +| **MOD1** | Zócalo simpleRTK2B Pro | pin "5V" debe estar en 0 V con la tarjeta virgen | + +*(Pinout completo en [Pinout de arneses](pinout-arneses.md). La guía de ensamble +detallada del gabinete es el [PDF 0007-1000](assembly-guide.md).)* + +## Reglas de calidad + +- Cada unidad lleva **dos actas**: la de **prueba de la tarjeta** y la de **ensamble**. + La de ensamble anota el número de serie de la tarjeta usada, para trazabilidad. +- Una tarjeta con FALLA **no se ensambla**; va a la bolsa de rechazos con su nota. +- Cualquier diferencia entre lo que ves y lo que dice el documento: **detente y + pregunta** a ingeniería antes de continuar. diff --git a/mkdocs.yml b/mkdocs.yml index e764853..c67be98 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -30,6 +30,7 @@ markdown_extensions: - admonition - tables - attr_list + - md_in_html - pymdownx.details - pymdownx.superfences - pymdownx.tasklist: @@ -39,6 +40,11 @@ markdown_extensions: nav: - Inicio: index.md + - Actas y control de calidad: + - "Índice de actas": actas/index.md + - "Acta de prueba V5": formularios/v5.html + - "Acta de prueba V4 (borrador)": formularios/v4.html + - "Checklist de ensamble (borrador)": formularios/ensamble.html - Procedimientos de banco: - "EinarBoard v5 — Prueba y flasheo (lote 15 pzas)": procedimientos/v5-prueba-y-flasheo.md - "EinarBoard 1.0 — Bring-up runbook": procedimientos/einarboard-1.0-bringup.md @@ -48,6 +54,7 @@ nav: - Montaje y arneses: - "Pinout de arneses y conectores": montaje/pinout-arneses.md - "BOM dividido para ensamble": montaje/bom-split.md + - "Guía de ensamble y control de calidad": montaje/guia-ensamble-qc.md - "Guía de ensamble (PDF)": montaje/assembly-guide.md - Diagnóstico: - "Protocolo de diagnóstico remoto": diagnostico/protocolo-diagnostico-remoto.md diff --git a/scripts/deploy.sh b/scripts/deploy.sh old mode 100644 new mode 100755