From f637d7813eeecdab28098d5db013ad71c95764bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leopoldo=20Rodr=C3=ADguez?= Date: Wed, 16 Sep 2026 15:13:30 -0600 Subject: [PATCH] descargas: firmware bring-up v5 0.1.1 + procedimiento actualizado --- backend/app.py | 166 +++++++++++++----- docs/admin.html | 86 ++++++++++ docs/descargas/SHA256SUMS.txt | 1 + docs/descargas/hbox-v5-bringup-README.md | 43 +++++ docs/descargas/hbox-v5-bringup_0.1.1.bin | Bin 0 -> 55104 bytes docs/descargas/index.md | 31 ++++ docs/formularios/acta.css | 3 + docs/formularios/acta.js | 164 ++++++++++++++---- docs/formularios/ver.html | 91 ++++++++++ docs/procedimientos/v5-prueba-y-flasheo.md | 8 +- docs/tablero.html | 190 +++++++++++++++++++++ mkdocs.yml | 8 +- scripts/sync-docs.sh | 4 + 13 files changed, 711 insertions(+), 84 deletions(-) create mode 100644 docs/admin.html create mode 100644 docs/descargas/SHA256SUMS.txt create mode 100644 docs/descargas/hbox-v5-bringup-README.md create mode 100644 docs/descargas/hbox-v5-bringup_0.1.1.bin create mode 100644 docs/descargas/index.md create mode 100644 docs/formularios/ver.html create mode 100644 docs/tablero.html diff --git a/backend/app.py b/backend/app.py index 494bb88..302ebb8 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,33 +1,72 @@ #!/usr/bin/env python3 -"""Actas HBOX — servicio mínimo para recibir formularios firmados de los técnicos. +"""Actas HBOX — servicio para recibir y consultar 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. +Reglas: + - Un acta real (serie que NO contiene PRUEBA) se puede enviar UNA sola vez por serie. + Un segundo envío se rechaza (409) hasta que un administrador la borre. + - Al seleccionar una serie ya enviada, el formulario la vuelve a cargar (llena, bloqueada). + - Solo el usuario 'leo' (administrador) puede borrar/reabrir un acta. + - Las series PRUEBA se guardan aparte (_pruebas), no cuentan y se pueden reenviar. + +El usuario autenticado llega en la cabecera X-Remote-User (la fija nginx desde $remote_user; +el cliente no puede falsificarla porque nginx la sobrescribe). 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) + GET /api/salud + GET /api/whoami -> {user, admin} + GET /api/acta?tipo=&serie= -> {exists, acta?} + POST /api/actas -> crea (409 si ya existe una real) + DELETE /api/acta?tipo=&serie= -> borra (solo admin) + GET /api/actas -> listado (ingeniería) """ 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") +ADMINS = {u.strip().lower() for u in os.environ.get("ACTAS_ADMINS", "leo").split(",") if u.strip()} SAFE = re.compile(r"[^A-Za-z0-9_\-]") +def slug(s, n=40): return SAFE.sub("", str(s or "").strip())[:n] -def slug(s, n=40): - return SAFE.sub("", str(s or "").strip())[:n] +def current_user(): + return (request.headers.get("X-Remote-User") or "").strip() + +def is_admin(): + return current_user().lower() in ADMINS + +def find_latest(tipo, serie): + """Ruta del acta real más reciente para (tipo, serie), o None.""" + carpeta = os.path.join(BASE, tipo) + if not os.path.isdir(carpeta): + return None + pref = serie + "_" + cand = sorted(fn for fn in os.listdir(carpeta) + if fn.startswith(pref) and fn.endswith(".json")) + return os.path.join(carpeta, cand[-1]) if cand else None @app.get("/api/salud") def salud(): return jsonify(ok=True, servicio="actas-hbox") +@app.get("/api/whoami") +def whoami(): + u = current_user() + return jsonify(ok=True, user=u, admin=(u.lower() in ADMINS)) + +@app.get("/api/acta") +def get_acta(): + tipo = slug(request.args.get("tipo")); serie = slug(request.args.get("serie"), 20) + if not tipo or not serie: + return jsonify(ok=False, error="faltan tipo/serie"), 400 + ruta = find_latest(tipo, serie) + if not ruta: + return jsonify(ok=True, exists=False) + with open(ruta, encoding="utf-8") as f: + d = json.load(f) + d["_archivo"] = os.path.basename(ruta) + return jsonify(ok=True, exists=True, acta=d) + @app.post("/api/actas") def crear(): try: @@ -37,8 +76,7 @@ def crear(): 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 + tipo = slug(d.get("tipo")); serie = slug(d.get("serie"), 20) tecnico = str(d.get("tecnico", "")).strip() if not tipo or not serie: return jsonify(ok=False, error="Faltan 'tipo' o 'serie'"), 400 @@ -47,28 +85,85 @@ def crear(): if not d.get("conforme"): return jsonify(ok=False, error="Falta la conformidad del técnico"), 400 + es_prueba = "PRUEBA" in serie.upper() + + # un acta real solo se puede enviar una vez + if not es_prueba and find_latest(tipo, serie): + return jsonify(ok=False, existe=True, + error=f"Ya existe un acta para {serie}. Pide a Leo que la borre para rehacerla."), 409 + ts = datetime.datetime.now().replace(microsecond=0) iso = ts.isoformat() d["_recibido"] = iso + d["_usuario"] = current_user() d["_ip"] = request.headers.get("X-Forwarded-For", request.remote_addr) + d["_prueba"] = es_prueba - carpeta = os.path.join(BASE, tipo) + carpeta = os.path.join(BASE, tipo, "_pruebas") if es_prueba else 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: + with open(os.path.join(carpeta, archivo), "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]) + if not es_prueba: + 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}") + sub = f"{tipo}/_pruebas/{archivo}" if es_prueba else f"{tipo}/{archivo}" + return jsonify(ok=True, archivo=sub, prueba=es_prueba) + +@app.delete("/api/acta") +def borrar_acta(): + if not is_admin(): + return jsonify(ok=False, error="Solo un administrador puede borrar actas."), 403 + tipo = slug(request.args.get("tipo")); serie = slug(request.args.get("serie"), 20) + if not tipo or not serie: + return jsonify(ok=False, error="faltan tipo/serie"), 400 + carpeta = os.path.join(BASE, tipo) + borrados = [] + if os.path.isdir(carpeta): + pref = serie + "_" + for fn in list(os.listdir(carpeta)): + if fn.startswith(pref) and fn.endswith(".json"): + os.remove(os.path.join(carpeta, fn)); borrados.append(fn) + idx = os.path.join(carpeta, "_indice.csv") + if os.path.exists(idx): + rows = list(csv.reader(open(idx, encoding="utf-8"))) + keep = ([rows[0]] if rows else []) + [r for r in rows[1:] if not (len(r) > 1 and r[1] == serie)] + with open(idx, "w", newline="", encoding="utf-8") as f: + csv.writer(f).writerows(keep) + return jsonify(ok=True, borrados=borrados) + +@app.delete("/api/actas") +def borrar_todas(): + """Reset admin: borra TODAS las actas reales de un tipo (opcional: también las de prueba).""" + if not is_admin(): + return jsonify(ok=False, error="Solo un administrador puede reiniciar actas."), 403 + tipo = slug(request.args.get("tipo")) + incl_pruebas = request.args.get("pruebas") == "1" + if not tipo: + return jsonify(ok=False, error="falta tipo"), 400 + carpeta = os.path.join(BASE, tipo) + n = 0 + if os.path.isdir(carpeta): + for fn in list(os.listdir(carpeta)): + fp = os.path.join(carpeta, fn) + if fn.endswith(".json"): + os.remove(fp); n += 1 + elif fn == "_indice.csv": + os.remove(fp) + if incl_pruebas: + pdir = os.path.join(carpeta, "_pruebas") + if os.path.isdir(pdir): + for fn in list(os.listdir(pdir)): + if fn.endswith(".json"): + os.remove(os.path.join(pdir, fn)); n += 1 + return jsonify(ok=True, borradas=n) @app.get("/api/actas") def listar(): @@ -85,24 +180,11 @@ def listar(): 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.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/docs/admin.html b/docs/admin.html new file mode 100644 index 0000000..ce14be1 --- /dev/null +++ b/docs/admin.html @@ -0,0 +1,86 @@ + + + + + +Administración — HBOX + + + + +
+

Administración de actas

Solo para el administrador.

+ +

Comprobando permisos…

+
+ + + diff --git a/docs/descargas/SHA256SUMS.txt b/docs/descargas/SHA256SUMS.txt new file mode 100644 index 0000000..b84ea21 --- /dev/null +++ b/docs/descargas/SHA256SUMS.txt @@ -0,0 +1 @@ +91ca6f87a86686be33ce013110f214a7e94ae7c8909c9eea1dbe846d9b5247f3 hbox-v5-bringup_0.1.1.bin diff --git a/docs/descargas/hbox-v5-bringup-README.md b/docs/descargas/hbox-v5-bringup-README.md new file mode 100644 index 0000000..f447570 --- /dev/null +++ b/docs/descargas/hbox-v5-bringup-README.md @@ -0,0 +1,43 @@ +# hbox_v5_bringup — EinarBoard v5 first-flash bring-up firmware + +Bench-proves a newly assembled v5 board over USB-C **J11** only. Never powers the RTK stack. +Checks: HSE 25 MHz + USB CDC · LSE Y4 · I2C: PCF8574 0x20 / SHT40 0x44 / TMP102 0x48 / +SC16IS752 0x4D · gauge + RGB LED walk · ADC · digital inputs. Prints PASS/FAIL, then a +`STAT` line every 2 s. Console commands: `r` rerun · `g` gauge walk · `i` I2C scan · `h` help. + +Procedure for technicians (ES): `06-Testing-Validation/2026-09-15_Drogheda_EinarBoard-v5_Procedimiento-Prueba-y-Flasheo_ES_v1.0.md` + +## Build +STM32duino core 3.x (`STMicroelectronics:stm32`). Target is the **generic F411RE**, not the Blackpill. +```bash +arduino-cli compile \ + --fqbn "STMicroelectronics:stm32:GenF4:pnum=GENERIC_F411RETX,usb=CDCgen,xserial=generic,rtlib=nanofp" \ + --build-property "build.extra_flags=-DHSE_VALUE=25000000UL" \ + --output-dir out hbox_v5_bringup +``` +`rtlib=nanofp` is required — the default newlib-nano build prints nothing for `%f`, so every voltage +and temperature line would be blank. `SystemClock_Config()` is overridden in the sketch (HSE 25 MHz → 96 MHz, USB 48 MHz) because the +generic variant defaults to HSI, which is not accurate enough for USB. + +## Flash (DFU, J11) +Hold **SW2** (BOOT0) → tap **SW1** (RESET) → release SW2 → device `0483:DF11` appears. +```bash +dfu-util -a 0 -s 0x08000000:leave -D release/hbox-v5-bringup_0.1.1.bin +``` +or STM32CubeProgrammer → USB → Connect → Download `release/hbox-v5-bringup_0.1.1.bin` @ `0x08000000`. + +## Release +| File | Version | SHA-256 (first 16) | Size | +|---|---|---|---| +| **`release/hbox-v5-bringup_0.1.1.bin`** | **0.1.1 (2026-09-16) — USE THIS** | `91ca6f87a86686be` | 55 104 B | +| `release/hbox-v5-bringup_0.1.0.bin` | 0.1.0 — SUPERSEDED (no float printf, ETH_CS driven) | `1a9a77ae479c8f86` | 41 040 B | + +## Changelog +- **0.1.1** (2026-09-16, audit): build with `rtlib=nanofp` (floats printed blank in 0.1.0); `ETH_CS` now + `INPUT_PULLUP` instead of driving HIGH into the unpowered W5500; reset cause reports `PIN` only when + `PINRSTF` is set (else `UNKNOWN`). +- **0.1.0** (2026-09-15): first build. + +## ⚠ Do not flash `blackpillrtkcode_v3` (v3.0.3) on v5 +Its `PIN_BATLED1..4` = PB12–PB15 are the W5500 SPI2 bus on this board. `board_v5.h` is the +verified v5 pin map — the production v5 firmware should include it rather than redefine pins. diff --git a/docs/descargas/hbox-v5-bringup_0.1.1.bin b/docs/descargas/hbox-v5-bringup_0.1.1.bin new file mode 100644 index 0000000000000000000000000000000000000000..488d01c3f6b1082b28f8d40cba83395de3704eec GIT binary patch literal 55104 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 Me`u}$E}pUf2jArXw*UYD literal 0 HcmV?d00001 diff --git a/docs/descargas/index.md b/docs/descargas/index.md new file mode 100644 index 0000000..efbefaa --- /dev/null +++ b/docs/descargas/index.md @@ -0,0 +1,31 @@ +# Descargas + +Archivos que el técnico necesita en el banco. Verificar el SHA-256 antes de flashear +(`shasum -a 256 archivo` en macOS, `sha256sum archivo` en Linux, `certutil -hashfile archivo SHA256` en Windows). + +## Firmware de bring-up — EinarBoard v5 + +| Archivo | Versión | SHA-256 | Uso | +|---|---|---|---| +| [**hbox-v5-bringup_0.1.1.bin**](hbox-v5-bringup_0.1.1.bin) | 0.1.1 (2026-09-16) | `91ca6f87a86686be…` ([SHA256SUMS](SHA256SUMS.txt)) | Sección 5 del [procedimiento de prueba y flasheo v5](../procedimientos/v5-prueba-y-flasheo.md) | + +Flasheo por DFU en **J11** (mantener **SW2**, pulsar y soltar **SW1**, soltar SW2): + +```bash +dfu-util -a 0 -s 0x08000000:leave -D hbox-v5-bringup_0.1.1.bin +``` + +o con **STM32CubeProgrammer** → USB → Connect → Download del `.bin` en `0x08000000`. + +!!! warning "Solo para la tarjeta v5" + Este firmware **no** es el de producción: solo prueba la tarjeta (reloj, USB, cristales, I²C, + LEDs, ADC) sin encender la etapa RTK ni el cargador. **Nunca flashear `blackpillrtkcode_v3` + (v3.0.3) en una v5.** + +Notas de compilación y cambios: [README del firmware de bring-up](hbox-v5-bringup-README.md). + +## Herramientas + +- **STM32CubeProgrammer** (Windows/macOS/Linux): https://www.st.com/en/development-tools/stm32cubeprog.html +- **dfu-util** (macOS: `brew install dfu-util`; Linux: `apt install dfu-util`) +- Terminal serie: CoolTerm, `screen /dev/tty.usbmodem* 115200`, o el monitor serie de Arduino IDE diff --git a/docs/formularios/acta.css b/docs/formularios/acta.css index b3706fb..55c442f 100644 --- a/docs/formularios/acta.css +++ b/docs/formularios/acta.css @@ -55,3 +55,6 @@ button.enviar:disabled{opacity:.5;cursor:not-allowed} .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} +button.limpiar{background:#fff;color:var(--tinta2);border:1px solid var(--linea); + border-radius:10px;padding:13px 16px;font-size:15px;cursor:pointer} +button.limpiar:hover{border-color:var(--naranja);color:var(--naranja)} diff --git a/docs/formularios/acta.js b/docs/formularios/acta.js index c5d5f58..4fa2200 100644 --- a/docs/formularios/acta.js +++ b/docs/formularios/acta.js @@ -1,28 +1,34 @@ -/* 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 */ +/* Motor de actas HBOX — renderiza el formulario, sincroniza con el servidor y firma. + - Un acta real se envía UNA vez: al elegir una serie ya enviada, se carga llena y bloqueada. + - Solo el administrador (leo) ve el botón "Borrar acta (reabrir)". + - La serie PRUEBA no se bloquea (para practicar). + Tipos de ítem: check | medida | texto | radio | nota · 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 || ''); + let admin = false; 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' })); + + const nav = $('div', { style: 'display:flex;justify-content:space-between;margin:2px 0 8px' }); + nav.append($('a', { class: 'volver', href: '../actas/', text: '← Volver a Actas' })); + nav.append($('a', { class: 'volver', href: '../salir.html', text: 'Cerrar sesión' })); + wrap.append(nav); 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 —' })); + serieSel.append($('option', { value: cfg.prefijo + '-PRUEBA', text: '⚙ PRUEBA (no cuenta)' })); 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 })); @@ -34,7 +40,9 @@ const cab = $('section', { class: 'paso' }); cab.append(datos); wrap.append(cab); - // secciones del procedimiento + const banner = $('div', { class: 'msg', id: 'f_banner' }); + wrap.append(banner); + cfg.secciones.forEach((sec, si) => { const s = $('section', { class: 'paso' }); s.append($('h2', { text: sec.titulo })); @@ -65,7 +73,6 @@ 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.' })); @@ -84,50 +91,121 @@ 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 limpiar = $('button', { class: 'limpiar', id: 'f_limpiar', text: 'Limpiar' }); + const borrar = $('button', { class: 'limpiar', id: 'f_borrar', text: 'Borrar acta (reabrir)', style: 'display:none;border-color:var(--falla);color:var(--falla)' }); const btn = $('button', { class: 'enviar', id: 'f_enviar', text: 'Enviar acta firmada' }); - barra.append(estado, btn); + const grp = $('div', { style: 'display:flex;gap:10px' }); grp.append(limpiar, borrar, btn); + barra.append(estado, grp); document.body.append(barra); - // ---- autoguardado ---- + // ---- helpers de estado ---- + const conforme = () => document.getElementById('f_conforme'); + function fields() { return wrap.querySelectorAll('[data-id]'); } + function setEnabled(on) { + fields().forEach(el => el.disabled = !on); + tecnico.disabled = !on; conforme().disabled = !on; + wrap.querySelectorAll('input[name=resultado]').forEach(el => el.disabled = !on); + } + function blankFields() { + fields().forEach(el => { if (el.type === 'checkbox') el.checked = false; else el.value = ''; }); + const rr = wrap.querySelector('input[name=resultado]:checked'); if (rr) rr.checked = false; + conforme().checked = false; tecnico.value = ''; + } + function fillFrom(a) { + tecnico.value = a.tecnico || ''; + conforme().checked = !!a.conforme; + if (a.resultado) { const el = wrap.querySelector(`input[name=resultado][value="${a.resultado}"]`); if (el) el.checked = true; } + for (const k in (a.respuestas || {})) { + const el = wrap.querySelector(`[data-id="${k}"]`); if (!el) continue; + if (el.type === 'checkbox') el.checked = !!a.respuestas[k].val; else el.value = a.respuestas[k].val || ''; + } + } + function showEditable() { + setEnabled(true); + limpiar.style.display = ''; btn.style.display = ''; borrar.style.display = 'none'; + wrap.classList.remove('enviada'); banner.className = 'msg'; + } + function showLocked(a) { + setEnabled(false); + limpiar.style.display = 'none'; btn.style.display = 'none'; + borrar.style.display = admin ? '' : 'none'; + banner.className = 'msg ok'; + const f = (a._recibido || '').replace('T', ' ').slice(0, 16); + banner.innerHTML = '✓ Acta ya enviada por ' + (a.tecnico || '—') + '' + + (f ? ' el ' + f : '') + '. Registro final' + + (a.resultado ? ' — resultado ' + a.resultado + '' : '') + '.' + + (admin ? '' : ' Para rehacerla, pide a Leo que la borre.'); + estado.textContent = 'Bloqueada'; + } + + // ---- autoguardado local (solo para actas nuevas, no enviadas) ---- + let t, locked = false; 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 }; - }); + fields().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 }; + conforme: conforme().checked, resultado: rr ? rr.value : '', respuestas: r }; } - let t; - function guardar() { - if (!serieSel.value) { estado.textContent = 'Elige el número de serie'; return; } + function guardarLocal() { + if (locked || !serieSel.value) 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() { + function cargarLocal() { 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'; + if (d) { fillFrom(d); estado.textContent = 'Borrador recuperado'; } + else estado.textContent = 'Nueva acta'; } - wrap.addEventListener('input', guardar); - serieSel.addEventListener('change', cargar); + + // ---- cargar una serie: consulta el servidor y bloquea si ya existe ---- + async function loadSerie() { + msg.className = 'msg'; locked = false; + const s = serieSel.value; + blankFields(); showEditable(); + if (!s) { estado.textContent = 'Elige el número de serie'; return; } + if (s.toUpperCase().indexOf('PRUEBA') >= 0) { cargarLocal(); estado.textContent = 'Modo prueba (no cuenta)'; return; } + estado.textContent = 'Consultando…'; + let j = null; + try { const r = await fetch(`/api/acta?tipo=${encodeURIComponent(cfg.tipo)}&serie=${encodeURIComponent(s)}`); j = await r.json(); } catch (e) {} + if (j && j.exists) { locked = true; fillFrom(j.acta); showLocked(j.acta); } + else { cargarLocal(); } + } + wrap.addEventListener('input', guardarLocal); + serieSel.addEventListener('change', loadSerie); + + // ---- limpiar (solo borrador local; no aplica a enviadas) ---- + limpiar.addEventListener('click', () => { + if (locked) return; + if (!confirm('¿Limpiar este formulario y empezar de nuevo?')) return; + try { localStorage.removeItem(key()); } catch (e) {} + blankFields(); msg.className = 'msg'; estado.textContent = 'Formulario limpio'; window.scrollTo(0, 0); + }); + + // ---- borrar / reabrir (solo admin) ---- + borrar.addEventListener('click', async () => { + if (!admin) return; + if (!confirm('¿Borrar el acta de ' + serieSel.value + '? Quedará disponible para rehacerse.')) return; + borrar.disabled = true; + try { + const r = await fetch(`/api/acta?tipo=${encodeURIComponent(cfg.tipo)}&serie=${encodeURIComponent(serieSel.value)}`, { method: 'DELETE' }); + const j = await r.json(); + if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status)); + locked = false; blankFields(); showEditable(); + msg.className = 'msg ok'; msg.textContent = 'Acta borrada. Ya puede rehacerse.'; + estado.textContent = 'Reabierta'; + } catch (e) { msg.className = 'msg err'; msg.textContent = 'No se pudo borrar: ' + e.message; } + borrar.disabled = false; + }); // ---- envío ---- btn.addEventListener('click', async () => { + if (locked) return; 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.'); @@ -137,20 +215,32 @@ 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.status === 409) { // ya existía: recargar bloqueada + try { localStorage.removeItem(key()); } catch (e) {} + await loadSerie(); + msg.className = 'msg err'; msg.textContent = j.error || 'Ya existe un acta para esta serie.'; + btn.disabled = false; btn.textContent = 'Enviar acta firmada'; + return; + } 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); + msg.className = 'msg ok'; + msg.innerHTML = '✓ Acta enviada y registrada como ' + d.serie + '.'; + if (!j.prueba) { locked = true; showLocked({ tecnico: d.tecnico, resultado: d.resultado, _recibido: new Date().toISOString() }); } + else { btn.disabled = false; btn.textContent = 'Enviar acta firmada'; estado.textContent = 'Prueba 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).'); + fail('No se pudo enviar: ' + e.message + '. Revisa tu conexión (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(); + // ---- init: saber si soy admin, luego cargar la serie elegida ---- + (async () => { + try { const r = await fetch('/api/whoami'); const j = await r.json(); admin = !!j.admin; } catch (e) {} + loadSerie(); + })(); } window.Acta = { render }; })(); diff --git a/docs/formularios/ver.html b/docs/formularios/ver.html new file mode 100644 index 0000000..e30f883 --- /dev/null +++ b/docs/formularios/ver.html @@ -0,0 +1,91 @@ + + + + + +Ver acta — HBOX + + + + +
+

Acta

Cargando…

+ +
+
+ + + diff --git a/docs/procedimientos/v5-prueba-y-flasheo.md b/docs/procedimientos/v5-prueba-y-flasheo.md index 0a9e4cc..f168446 100644 --- a/docs/procedimientos/v5-prueba-y-flasheo.md +++ b/docs/procedimientos/v5-prueba-y-flasheo.md @@ -37,7 +37,7 @@ y dejarla flasheada con el firmware de arranque entregado por ingeniería. | 1 | Fuente de banco 0–30 V con límite de corriente ajustable | | 1 | Cable USB-C ↔ USB-C (o USB-A ↔ USB-C) **con datos** (no solo carga) | | 1 | Laptop con **STM32CubeProgrammer** instalado (o `dfu-util` en terminal) | -| 1 | Archivo de firmware: **`hbox-v5-bringup_0.1.0.bin`** (repo: `04-Source-Code/firmware/hbox_v5_bringup/release/`, SHA-256 inicia `1a9a77ae`) | +| 1 | Archivo de firmware: **`hbox-v5-bringup_0.1.1.bin`** — descarga: **https://drogheda.helgen.online/descargas/** (repo: `04-Source-Code/firmware/hbox_v5_bringup/release/`, SHA-256 inicia `91ca6f87`) | | 1 | Jumper 2.54 mm para P11 (interruptor de corte) | | 1 | Lupa o lámpara con aumento | | — | Hoja de registro (sección 7) | @@ -183,7 +183,7 @@ Solo para tarjetas que pasaron las secciones 1 a 4. 2. Arriba a la derecha, en el selector de conexión elegir **USB**. Pulsar el botón de refrescar; debe aparecer `USB1`. Pulsar **Connect**. 3. Ir a la pestaña **Erasing & Programming** (icono de flecha hacia abajo, lado izquierdo). -4. **File path** → seleccionar `hbox-v5-bringup_0.1.0.bin`. +4. **File path** → seleccionar `hbox-v5-bringup_0.1.1.bin`. 5. **Start address** = `0x08000000`. Marcar **Verify programming** y **Run after programming**. 6. Pulsar **Start Programming**. Esperar "File download complete" y "Verify… successful". 7. Pulsar **Disconnect**. @@ -192,7 +192,7 @@ Solo para tarjetas que pasaron las secciones 1 a 4. ```bash dfu-util -l # debe listar [0483:df11] -dfu-util -a 0 -s 0x08000000:leave -D hbox-v5-bringup_0.1.0.bin +dfu-util -a 0 -s 0x08000000:leave -D hbox-v5-bringup_0.1.1.bin ``` ### 5d. Verificación después del flasheo @@ -206,7 +206,7 @@ dfu-util -a 0 -s 0x08000000:leave -D hbox-v5-bringup_0.1.0.bin | **D17** (LED azul) + LED de panel en P8 (si hay) | **Parpadean** a 1 Hz (MCU ejecutando el firmware) | | **DB1 → DB4** (LEDs del indicador de batería) | Se encienden **en secuencia** una vez, luego se apagan; después R, G, B en P14 si hay LED RGB conectado | | Laptop | Aparece un **puerto serie** nuevo ("STMicroelectronics Virtual COM Port" / `usbmodem…`) | -| Terminal serie a **115200 baud** | Encabezado `hbox-v5-bringup 0.1.0`, luego líneas `[PASS]` / `[FAIL]`, y al final **`>>> BOARD OK`** | +| Terminal serie a **115200 baud** | Encabezado `hbox-v5-bringup 0.1.1`, luego líneas `[PASS]` / `[FAIL]`, y al final **`>>> BOARD OK`** | 3. Si el terminal se abrió tarde y no se vio el reporte: teclear **`r`** en el terminal → repite todas las pruebas (y vuelve a encender los LEDs en secuencia). diff --git a/docs/tablero.html b/docs/tablero.html new file mode 100644 index 0000000..d01385f --- /dev/null +++ b/docs/tablero.html @@ -0,0 +1,190 @@ + + + + + +Tablero de estado — HBOX + + + +
+
+

Tablero de estado — HBOX

Cargando…
+
+ ← Documentación + +
+
+
+
+ + + diff --git a/mkdocs.yml b/mkdocs.yml index c67be98..c71f318 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,6 +1,6 @@ site_name: HBOX — Documentación Técnica site_description: Procedimientos de banco, montaje y diagnóstico del HBOX (EinarBoard) — Helgen Technologies -site_url: https://drogheda.helgen.online/ +site_url: https://docs.helgen.online/hbox/ copyright: "© Helgen Technologies — uso interno" theme: @@ -40,6 +40,7 @@ markdown_extensions: nav: - Inicio: index.md + - "Tablero de estado": tablero.html - Actas y control de calidad: - "Índice de actas": actas/index.md - "Acta de prueba V5": formularios/v5.html @@ -61,3 +62,8 @@ nav: - Git: - "Guía Git (español)": git/guia-git-es.md - "Git guide (English)": git/git-guide-en.md + - Descargas: + - "Firmware y herramientas": descargas/index.md + - "README firmware bring-up v5": descargas/hbox-v5-bringup-README.md + - "Administración (Leo)": admin.html + - "Salir": salir.html diff --git a/scripts/sync-docs.sh b/scripts/sync-docs.sh index 932de77..85d39ec 100755 --- a/scripts/sync-docs.sh +++ b/scripts/sync-docs.sh @@ -18,6 +18,10 @@ declare -a MAP=( "03-Architecture-Hardware/0007-1000_Assembly_Guide.pdf|docs/montaje/0007-1000_Assembly_Guide.pdf" "03-Architecture-Hardware/kicad/docs/GIT-GUIDE_Colaboradores-Externos_ES.md|docs/git/guia-git-es.md" "03-Architecture-Hardware/kicad/docs/GIT-GUIDE_External-Collaborators_EN.md|docs/git/git-guide-en.md" + # Descargas para técnicos (firmware de bring-up v5) + "04-Source-Code/firmware/hbox_v5_bringup/release/hbox-v5-bringup_0.1.1.bin|docs/descargas/hbox-v5-bringup_0.1.1.bin" + "04-Source-Code/firmware/hbox_v5_bringup/release/SHA256SUMS|docs/descargas/SHA256SUMS.txt" + "04-Source-Code/firmware/hbox_v5_bringup/README.md|docs/descargas/hbox-v5-bringup-README.md" ) n=0