// EL SERVIDOR LOCAL DE LA ESTACION FISCAL.
//
// Corre SOLO en 127.0.0.1 (la propia laptop). Que no este expuesto a la red del
// local no quiere decir que adentro no haga falta portero: quien se sienta en
// esa computadora no puede poder configurar el RIF ni anular una factura sin
// que quede escrito quien fue.
//
// Cuatro puertas, cada una con su llave:
//   - EL DUEÑO: configura, anula, pone las claves y lee la bitacora.
//   - EL CAJERO: mira como va la cosa y empuja lo que quedo por transmitir.
//   - LOS PROGRAMAS DEL NEGOCIO (Gustito, el Cerebrito): entregan la venta por
//     /api/venta con la llave de maquina.
//   - EL FISCAL: consulta y exporta por /fiscal, sin poder tocar nada.
//
// Las galletas de sesion van HttpOnly y SameSite=Strict: eso ademas cierra el
// paso a que una pagina cualquiera abierta en el mismo navegador mande formularios
// a la estacion por la espalda.
//
// ⚠ La cara definitiva se trabaja aparte (decision de Gilberto: primero verlo
// andando). Esto es funcional y sobrio a proposito, no una propuesta de diseño.
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
import { Acceso, NOMBRE_ROL, Sesiones, puede, type Accion, type Rol } from './acceso.js';
import { celdaCsv } from './auditor.js';
import { autoarranqueActivo, marcarApagado, ponerAutoarranque, retirarDelEquipo } from './arranque.js';
import { crearAvisador, VERSION_ESTACION } from './actualizador.js';
import { EstacionFiscal } from './estacion.js';
import { guardarConfig, leerConfig, leerSerial, revisar, type ConfigFiscal, type ConfigImprenta } from './config.js';
import { crearImprenta } from './imprenta.js';
import { armarDocumento } from './armador.js';
import { lineasFactura, type ModeloFactura } from './factura.js';
import type { VentaDeNube } from './nube.js';
import type { Asiento, VentaEntrante } from './tipos.js';

const PUERTO = 8739; // el Cerebrito usa 8737 y 8738: este no se pisa con ellos
const GALLETA = 'gxf_estacion';

// Iconitos de la antesala (inline, cero dependencias).
const SVG_CANDADO = `<svg class="icono" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="4.5" y="10.5" width="15" height="9.5" rx="2.5"/><path d="M8 10.5V7.5a4 4 0 0 1 8 0v3"/></svg>`;
const SVG_OJO = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M2.5 12s3.5-6.5 9.5-6.5S21.5 12 21.5 12s-3.5 6.5-9.5 6.5S2.5 12 2.5 12z"/><circle cx="12" cy="12" r="2.7"/></svg>`;

/**
 * Un asiento de ejemplo para la muestra del modelo de factura: datos marcados
 * como muestra, sin numero real y sin tocar el libro.
 */
function asientoMuestra(c: ConfigFiscal): Asiento {
  const documento = armarDocumento(
    {
      origen_id: 'muestra',
      canal: 'mesa',
      cliente: { nombre: 'Cliente de muestra' },
      renglones: [
        { nombre: 'Producto de muestra', cantidad: 2, precio_usd: 5, alicuota: c.emisor.alicuota_general },
        { nombre: 'Otro producto', cantidad: 1, precio_usd: 3, alicuota: c.emisor.alicuota_general },
      ],
      tasa_bs: 100,
      pago: { metodo: 'efectivo' },
    },
    c.emisor,
  );
  return {
    seq: 0,
    serie: c.serie,
    numero: 0,
    control: 0,
    tipo: 'factura',
    documento,
    sellado_en: new Date().toISOString(),
    equipo: c.equipo,
    hash_anterior: '',
    hash: '',
  };
}

/**
 * El tamaño de letra de una cifra grande, segun su largo. Un monto de millones
 * a 38px no cabe en su tarjeta y salia cortado, que en pantallas de dinero es
 * inaceptable. Se usa en las tarjetas KPI.
 */
function tamanoCifra(valor: string | number): number {
  const n = String(valor).length;
  if (n <= 6) return 38;
  if (n <= 9) return 31;
  if (n <= 12) return 25;
  if (n <= 15) return 21;
  if (n <= 18) return 18;
  return 15;
}

function json(res: ServerResponse, datos: unknown, estado = 200): void {
  const cuerpo = JSON.stringify(datos);
  res.writeHead(estado, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
  res.end(cuerpo);
}

function html(res: ServerResponse, cuerpo: string, estado = 200): void {
  res.writeHead(estado, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
  // Tablero de escritorio a lo ancho (molde elegido por Gilberto 2026-07-29):
  // fondo azul noche, dorado de acento, numeros en monoespaciada, y las
  // pantallas SIN barra superior (entrar, primer uso, avisos) quedan como
  // columna centrada, que ahi si es lo correcto.
  res.end(`<!doctype html><html lang="es"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="theme-color" content="#0B0D14">
<link rel="icon" type="image/svg+xml" href="/icono.svg">
<link rel="manifest" href="/manifest.webmanifest">
<title>Estacion Fiscal</title>
<style>
*{box-sizing:border-box;margin:0}
:root{--fondo:#0B0D14;--panel:#131624;--panel2:#1A1E30;--borde:#2A2F4A;--texto:#FFFFFF;--texto2:#D6DAEA;--oro:#FBBF24;--verde:#34D399;--rojo:#F87171}
body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--fondo);color:var(--texto);font-size:15px;line-height:1.5}
.caja{width:100%;padding:18px 22px 50px}
.caja:not(:has(.topbar)){max-width:560px;margin:36px auto;padding:0 18px 40px}
h1{font-size:20px;margin:0 0 4px}
h2{font-size:12px;font-weight:800;text-transform:uppercase;letter-spacing:.14em;color:var(--texto2);margin:22px 0 10px}
.tarjeta{background:var(--panel);border:1px solid var(--borde);border-radius:12px;padding:16px 18px;margin-bottom:12px}
label{display:block;margin:10px 0 4px;font-size:12px;color:var(--texto2);font-weight:700}
input,select,textarea{width:100%;padding:9px 11px;border-radius:8px;border:1px solid var(--borde);background:var(--fondo);color:var(--texto);font-size:14px;font-family:inherit}
textarea{font-family:Consolas,monospace;font-size:13px;resize:vertical}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--oro)}
button{margin-top:14px;padding:10px 17px;border:0;border-radius:9px;background:var(--oro);color:#0B0D14;font-weight:800;font-size:13.5px;cursor:pointer}
button.gris{background:transparent;color:var(--texto2);border:1px solid var(--borde);font-weight:600}
button.gris:hover{color:var(--texto)}
a{color:var(--oro)}
table{width:100%;border-collapse:collapse;font-size:13.5px}
th{text-align:left;font-size:10.5px;text-transform:uppercase;letter-spacing:.1em;color:var(--texto2);padding:8px 10px;border-bottom:1px solid var(--borde);font-weight:700}
td{padding:9px 10px;border-bottom:1px solid rgba(42,47,74,.5)}
tr:hover td{background:var(--panel2)}
td.num,th.num{text-align:right;font-family:Consolas,monospace;font-size:14px}
.doc{font-family:Consolas,monospace;font-weight:700}
.bien{color:var(--verde)}.mal{color:var(--rojo)}.aviso{color:var(--oro)}
.dato{display:flex;justify-content:space-between;gap:10px;padding:8px 0;border-bottom:1px solid rgba(42,47,74,.5);font-size:13.5px;color:var(--texto2)}
.dato:last-child{border-bottom:none}
.dato b{color:var(--texto);font-family:Consolas,monospace}
.pie{color:var(--texto2);font-size:12.5px;margin-top:18px;line-height:1.6}
.barra{display:flex;justify-content:space-between;align-items:center;gap:12px}
code{background:var(--fondo);border:1px solid var(--borde);border-radius:8px;padding:6px 9px;display:block;font-size:13px;word-break:break-all;font-family:Consolas,monospace}
.topbar{background:var(--panel);border-bottom:1px solid var(--borde);margin:-18px -22px 18px;padding:10px 22px;display:flex;align-items:center;gap:18px;flex-wrap:wrap}
.topbar .cubo{width:38px;height:38px;border-radius:9px;background:linear-gradient(135deg,var(--oro),#B45309);display:grid;place-items:center;font-weight:900;color:#0B0D14;font-size:14px;flex:none}
.topbar .nombre b{font-size:16px}
.topbar .nombre span{display:block;font-size:11.5px;color:var(--texto2);font-family:Consolas,monospace}
.topbar nav{display:flex;gap:4px;flex-wrap:wrap;margin-left:8px}
.topbar nav a{color:var(--texto2);text-decoration:none;font-size:13.5px;font-weight:600;padding:8px 12px;border-radius:8px}
.topbar nav a:hover{color:var(--texto);background:var(--panel2)}
.topbar nav a.activa{color:#0B0D14;background:var(--oro);font-weight:800}
.topbar .quien{margin-left:auto;font-size:12.5px;color:var(--texto2);display:flex;align-items:center;gap:10px}
.topbar .quien b{color:var(--texto)}
.topbar .quien button{margin:0;padding:7px 12px}
.cinta{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px;align-items:center}
.cinta .hoy{font-family:Consolas,monospace;font-size:12.5px;color:var(--texto2);margin-left:auto}
.pill{font-size:11.5px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;padding:5px 12px;border-radius:6px}
.pill.v{color:var(--verde);background:rgba(52,211,153,.12);border:1px solid rgba(52,211,153,.35)}
.pill.o{color:var(--oro);background:rgba(251,191,36,.1);border:1px solid rgba(251,191,36,.35)}
.pill.r{color:var(--rojo);background:rgba(248,113,113,.1);border:1px solid rgba(248,113,113,.35)}
.kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:14px}
.kpi{background:var(--panel);border:1px solid var(--borde);border-radius:12px;padding:16px 18px 13px}
.kpi .t{font-size:11px;color:var(--texto2);text-transform:uppercase;letter-spacing:.12em;font-weight:700}
/* La cifra nunca sale cortada a la mitad: el tamaño lo calcula tamanoCifra()
   segun el largo del valor, y si aun asi no cabe termina en puntos
   suspensivos, que avisa, en vez de comerse los ultimos digitos en silencio. */
.kpi .n{font-family:Consolas,monospace;font-size:38px;font-weight:700;line-height:1.15;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.kpi .d{font-size:12px;color:var(--texto2)}
.dos{display:grid;grid-template-columns:1fr 400px;gap:14px;align-items:start}
.avisin{border:1px solid rgba(251,191,36,.4);background:rgba(251,191,36,.06)}
.antesala{display:grid;grid-template-columns:1.1fr 1fr;min-height:100vh}
.caja:has(.antesala){max-width:none;margin:0;padding:0}
.lado-marca{position:relative;background:radial-gradient(700px 460px at 35% 42%,#1A1E30 0%,#0B0D14 62%);border-right:1px solid var(--borde);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:20px;padding:40px}
.lado-marca .emblema{width:118px;height:118px;border-radius:26px;background:linear-gradient(135deg,var(--oro),#B45309);display:grid;place-items:center;font-weight:900;color:#0B0D14;font-size:44px;box-shadow:0 0 90px rgba(251,191,36,.22)}
.lado-marca h1{font-size:30px;margin:0;text-align:center}
.lado-marca .sub{color:var(--texto2);font-size:13.5px;font-family:Consolas,monospace;text-align:center}
.lado-marca .raya{width:46px;height:3px;border-radius:2px;background:var(--oro);opacity:.85}
.lado-form{display:flex;align-items:center;justify-content:center;padding:40px 24px}
.form-tarjeta{width:100%;max-width:430px;background:var(--panel);border:1px solid var(--borde);border-radius:16px;padding:34px 36px 30px}
.form-tarjeta h1{font-size:22px;margin:0}
.form-tarjeta .sub{color:var(--texto2);font-size:13.5px;margin:6px 0 16px;line-height:1.6}
.campo{position:relative;margin-top:4px}
.campo .icono{position:absolute;left:13px;top:50%;transform:translateY(-50%);width:17px;height:17px;color:var(--texto2);pointer-events:none}
.campo input{padding:13px 46px 13px 40px;font-size:15px;border-radius:10px}
.campo .ojo{position:absolute;right:5px;top:50%;transform:translateY(-50%);margin:0;padding:8px 10px;background:transparent;border:0;color:var(--texto2)}
.campo .ojo:hover{color:var(--texto)}
.campo .ojo svg{width:18px;height:18px;display:block}
.form-tarjeta .entrar{width:100%;padding:12px;font-size:14.5px;margin-top:20px}
@media(max-width:1000px){.kpis{grid-template-columns:repeat(2,1fr)}.dos{grid-template-columns:1fr}}
@media(max-width:900px){.antesala{grid-template-columns:1fr}.lado-marca{display:none}}
</style></head><body><div class="caja">${cuerpo}</div></body></html>`);
}

function redirigir(res: ServerResponse, a: string, galleta?: string): void {
  const cab: Record<string, string> = { location: a };
  if (galleta) cab['set-cookie'] = galleta;
  res.writeHead(302, cab);
  res.end();
}

const escapar = (s: unknown): string =>
  String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c] as string);

async function leerCuerpo(req: IncomingMessage): Promise<Record<string, unknown>> {
  const trozos: Buffer[] = [];
  for await (const t of req) trozos.push(t as Buffer);
  if (!trozos.length) return {};
  try {
    return JSON.parse(Buffer.concat(trozos).toString('utf8')) as Record<string, unknown>;
  } catch {
    return {};
  }
}

async function leerFormulario(req: IncomingMessage): Promise<URLSearchParams> {
  const trozos: Buffer[] = [];
  for await (const t of req) trozos.push(t as Buffer);
  return new URLSearchParams(Buffer.concat(trozos).toString('utf8'));
}

function galletaDe(req: IncomingMessage): string | null {
  const crudas = req.headers.cookie ?? '';
  const mia = crudas
    .split(';')
    .map((c) => c.trim())
    .find((c) => c.startsWith(`${GALLETA}=`));
  return mia ? mia.slice(GALLETA.length + 1) : null;
}

export interface OpcionesServidor {
  base: string;
  puerto?: number;
  log?: (m: string) => void;
  /** Solo el programa instalado (.exe) puede tocar el arranque con Windows. */
  esExe?: boolean;
}

export async function iniciarServidor(op: OpcionesServidor): Promise<{ cerrar: () => void; puerto: number }> {
  const puerto = op.puerto ?? PUERTO;
  const log = op.log ?? console.log;
  const acceso = new Acceso(`${op.base}/estado/acceso.json`);
  const sesiones = new Sesiones();
  const avisador = crearAvisador({ base: op.base, esExe: !!op.esExe, log });
  let ultimaRevisionVersion = 0;
  let estacion: EstacionFiscal | null = null;

  const abrirEstacion = async (): Promise<EstacionFiscal | null> => {
    if (estacion) return estacion;
    try {
      estacion = await EstacionFiscal.abrir(op.base);
      return estacion;
    } catch {
      return null;
    }
  };
  await abrirEstacion();

  /** Quien esta del otro lado: una persona con sesion, o ninguno. */
  const quienEs = (req: IncomingMessage): Rol | null => sesiones.ver(galletaDe(req));

  /** ¿Viene con la llave de los programas del negocio? */
  const esMaquina = (req: IncomingMessage): boolean => {
    const llave = (req.headers.authorization ?? '').replace(/^Bearer\s+/i, '').trim();
    return llave !== '' && acceso.esLlaveMaquina(llave);
  };

  // Las secciones de la barra superior, cada una con el permiso que la abre.
  // El cajero ve solo lo suyo; el dueño lo ve todo.
  const SECCIONES: { ruta: string; nombre: string; accion: Accion }[] = [
    { ruta: '/', nombre: 'Inicio', accion: 'ver' },
    { ruta: '/documentos', nombre: 'Documentos', accion: 'ver' },
    { ruta: '/retenciones', nombre: 'Retenciones', accion: 'retenciones' },
    { ruta: '/contingencia', nombre: 'Contingencia', accion: 'configurar' },
    { ruta: '/respaldo', nombre: 'Respaldo', accion: 'configurar' },
    { ruta: '/claves', nombre: 'Claves', accion: 'claves' },
    { ruta: '/movimientos', nombre: 'Movimientos', accion: 'bitacora' },
    { ruta: '/configurar', nombre: 'Configuración', accion: 'configurar' },
  ];

  // El panel de marca de la ANTESALA (entrar y primer uso): split-screen
  // clasico, marca a la izquierda y formulario a la derecha.
  const ladoMarca = (): string => {
    const c = leerConfig(op.base);
    const sub = c?.emisor.rif
      ? conSerial(`${escapar(c.emisor.razon_social)} · ${escapar(c.emisor.rif)}`)
      : 'El modulo fiscal del negocio';
    return `<div class="lado-marca">
      <div class="emblema">EF</div>
      <div><h1>Estación Fiscal</h1><div class="sub">${sub}</div></div>
      <div class="raya"></div>
    </div>`;
  };

  // El serial y la VERSION en la sub-linea de identidad. La version se muestra
  // porque los articulos 9 al 11 de la 000121 obligan a homologar cada version
  // ANTES de instalarla: el funcionario tiene que poder ver, sin preguntar, cuál
  // está corriendo este equipo y cotejarla con la que se homologó.
  const conSerial = (texto: string): string => {
    const s = leerSerial(op.base);
    const c = leerConfig(op.base);
    const base = s
      ? `${texto} · ${escapar(s)}`
      : c && !c.nube?.url
        ? `${texto} · SIN SERIAL (practica)`
        : texto;
    return `${base} · v${escapar(VERSION_ESTACION)}`;
  };

  // LA PANTALLA DE LA IMPRENTA DIGITAL: aqui el negocio elige con cual emite y
  // pega las credenciales que le entrego su proveedor. Antes esto solo se podia
  // hacer editando el archivo de configuracion a mano, que para un cliente es
  // como no existir.
  const PROVEEDORES: { id: string; nombre: string; ayuda: string }[] = [
    { id: 'practica', nombre: 'Practica (no emite documentos validos)', ayuda: 'Para probar el circuito completo sin contrato ni documentos reales. Los numeros salen marcados como PRACTICA.' },
    { id: 'gustito', nombre: 'Imprenta Digital Gustito', ayuda: 'Pega la llave que te entrego Gustito al registrar tu estacion.' },
    { id: 'hka', nombre: 'The Factory HKA Venezuela', ayuda: 'Pega la direccion y el usuario y clave que te entrego HKA. Si te dieron un token fijo en vez de usuario y clave, ponlo en el ultimo campo.' },
  ];

  const camposImprenta = (i: ConfigImprenta | null | undefined, prefijo: string): string => {
    const v = (x?: string | null) => (x == null ? '' : escapar(x));
    const act = i?.adaptador ?? 'practica';
    return `
      <label>Proveedor</label>
      <select name="${prefijo}_adaptador">
        ${PROVEEDORES.map((p) => `<option value="${p.id}"${act === p.id ? ' selected' : ''}>${escapar(p.nombre)}</option>`).join('')}
      </select>
      <p class="pie" style="margin:6px 0 0">${escapar(PROVEEDORES.find((p) => p.id === act)?.ayuda ?? '')}</p>
      <label>Direccion de la imprenta (si te la dieron)</label>
      <input name="${prefijo}_url" value="${v(i?.url)}" placeholder="https://...">
      <label>Usuario</label><input name="${prefijo}_usuario" value="${v(i?.usuario)}" autocomplete="off">
      <label>Clave</label><input name="${prefijo}_clave" type="password" value="${v(i?.clave)}" autocomplete="off">
      <label>Llave o token fijo (si en vez de usuario y clave te dieron uno)</label>
      <input name="${prefijo}_token" value="${v(i?.token)}" autocomplete="off">`;
  };

  const imprentaHtml = (c: ConfigFiscal | null): string => {
    const usando = c?.usando ?? 'imprenta';
    return `
      <h2>Imprenta digital</h2>
      <div class="tarjeta">
        <p class="pie" style="margin-top:0">La imprenta es quien le pone el <b>numero de control</b> a cada documento: sin ella, la estacion sella pero el documento no queda emitido de verdad. Aqui se conecta con las credenciales que entrega el proveedor.</p>
        <div class="dato"><span>Emitiendo con</span><b>${escapar((usando === 'respaldo' ? c?.respaldo?.nombre : c?.imprenta?.nombre) ?? 'Practica')}</b></div>
        <form method="post" action="/imprenta" id="formImprenta">
          <label>¿Te mandaron las credenciales en un archivo o un correo? Pegalo aqui completo</label>
          <textarea id="pegado" rows="4" placeholder='Pega aqui tal cual lo que te mando la imprenta (por ejemplo: {"usuario":"...","clave":"...","url":"..."}). Tambien sirve si viene como texto suelto.'></textarea>
          <div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:8px">
            <button type="button" class="gris" onclick="leerPegado()">Leerlo y llenar los campos</button>
            <span id="leido" class="pie" style="margin:0;align-self:center"></span>
          </div>
          <hr style="border:0;border-top:1px solid var(--borde);margin:16px 0">
          ${camposImprenta(c?.imprenta, 'p')}
          <div style="display:flex;gap:10px;flex-wrap:wrap">
            <button type="submit">Guardar la imprenta</button>
            <button type="button" class="gris" onclick="probarImprenta('imprenta')">Probar la conexion</button>
          </div>
        </form>
        <p id="pruebaImprenta" class="pie" style="margin-bottom:0"></p>
      </div>
      <div class="tarjeta">
        <b>Proveedor de respaldo (continuidad del servicio)</b>
        <p class="pie" style="margin:6px 0">Una segunda imprenta digital <b>autorizada</b>, lista por si la principal deja de responder, para que el negocio no se quede sin poder facturar. <b>Solo una emite a la vez</b> y el cambio es manual: queda anotado quien lo hizo y cuando, y cada documento guarda con cual imprenta se numero.</p>
        <form method="post" action="/imprenta"><input type="hidden" name="cual" value="respaldo">
          ${camposImprenta(c?.respaldo, 'p')}
          <div style="display:flex;gap:10px;flex-wrap:wrap">
            <button type="submit" class="gris">Guardar el respaldo</button>
            <button type="button" class="gris" onclick="probarImprenta('respaldo')">Probar la conexion</button>
          </div>
        </form>
        <form method="post" action="/imprenta">
          <input type="hidden" name="cual" value="cambiar">
          <input type="hidden" name="usando" value="${usando === 'imprenta' ? 'respaldo' : 'imprenta'}">
          <button class="${usando === 'imprenta' ? 'gris' : ''}" style="margin-top:12px">
            ${usando === 'imprenta' ? 'Pasar al respaldo' : 'Volver a la imprenta principal'}
          </button>
        </form>
        <p id="pruebaRespaldo" class="pie" style="margin-bottom:0"></p>
      </div>
      <script>
      // LEE LO QUE MANDO LA IMPRENTA y llena los campos solo. Cada proveedor
      // le pone un nombre distinto a lo mismo (usuario/user/tokenEmpresa,
      // clave/password/tokenPassword...), y el negocio no tiene por que saber
      // cual va en cual casilla. Aguanta JSON anidado y tambien texto suelto
      // del tipo "Usuario: xxx".
      const SINONIMOS = {
        usuario: ['usuario','user','username','login','tokenempresa','token_empresa','empresa','cuenta'],
        clave: ['clave','password','pass','contrasena','contrasena','tokenpassword','token_password','secreto'],
        url: ['url','endpoint','direccion','host','servidor','ambiente','uri','baseurl','base_url'],
        token: ['token','jwt','apikey','api_key','llave','accesstoken','access_token','bearer'],
      };

      function buscarEnObjeto(obj, nombres, encontrado = {}) {
        if (!obj || typeof obj !== 'object') return encontrado;
        for (const [k, val] of Object.entries(obj)) {
          const llave = String(k).toLowerCase().replace(/[^a-z_]/g, '');
          if (val && typeof val === 'object') { buscarEnObjeto(val, nombres, encontrado); continue; }
          for (const [campo, alias] of Object.entries(nombres)) {
            if (!encontrado[campo] && alias.includes(llave) && String(val).trim()) {
              encontrado[campo] = String(val).trim();
            }
          }
        }
        return encontrado;
      }

      function buscarEnTexto(texto, nombres) {
        const encontrado = {};
        for (const linea of texto.split(/\\r?\\n/)) {
          const m = linea.match(/^\\s*([A-Za-z_ ]+)\\s*[:=]\\s*(.+?)\\s*$/);
          if (!m) continue;
          const llave = m[1].toLowerCase().replace(/[^a-z_]/g, '');
          for (const [campo, alias] of Object.entries(nombres)) {
            if (!encontrado[campo] && alias.includes(llave)) encontrado[campo] = m[2].replace(/^["']|["'],?$/g, '').trim();
          }
        }
        return encontrado;
      }

      function leerPegado() {
        const salida = document.getElementById('leido');
        const crudo = document.getElementById('pegado').value.trim();
        if (!crudo) { salida.className = 'mal'; salida.textContent = 'Pega primero lo que te mandaron.'; return; }
        let hallado = {};
        try {
          hallado = buscarEnObjeto(JSON.parse(crudo), SINONIMOS);
        } catch (e) {
          hallado = buscarEnTexto(crudo, SINONIMOS); // no era JSON: se lee como texto
        }
        const form = document.getElementById('formImprenta');
        const puestos = [];
        for (const campo of ['usuario','clave','url','token']) {
          if (!hallado[campo]) continue;
          const input = form.querySelector('[name="p_' + campo + '"]');
          if (input) { input.value = hallado[campo]; puestos.push(campo); }
        }
        // Si trae direccion de HKA, se preselecciona su proveedor.
        if ((hallado.url || '').includes('thefactoryhka')) {
          const sel = form.querySelector('[name="p_adaptador"]');
          if (sel) { sel.value = 'hka'; puestos.push('proveedor HKA'); }
        }
        if (!puestos.length) {
          salida.className = 'mal';
          salida.textContent = 'No reconoci credenciales ahi. Llena los campos a mano o pega el correo completo.';
          return;
        }
        salida.className = 'bien';
        salida.textContent = 'Listo: ' + puestos.join(', ') + '. Revisa que este bien y dale Guardar.';
      }

      async function probarImprenta(cual){
        const salida = document.getElementById(cual === 'respaldo' ? 'pruebaRespaldo' : 'pruebaImprenta');
        salida.className = 'pie'; salida.textContent = 'Comprobando la conexion con la imprenta...';
        const r = await fetch('/api/probar-imprenta', {method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({cual})});
        const d = await r.json();
        salida.className = d.ok ? 'bien' : 'mal';
        salida.textContent = d.detalle;
      }
      </script>`;
  };

  const cabecera = (rol: Rol, activa: string): string => {
    const c = leerConfig(op.base);
    const sub = c?.emisor.rif
      ? conSerial(`${escapar(c.emisor.razon_social)} · ${escapar(c.emisor.rif)} · Serie ${escapar(c.serie)}`)
      : 'Sin configurar todavía';
    const enlaces = SECCIONES.filter((s) => puede(rol, s.accion))
      .map((s) => `<a href="${s.ruta}"${s.nombre === activa ? ' class="activa"' : ''}>${s.nombre}</a>`)
      .join('');
    return `
    <div class="topbar">
      <div class="cubo">EF</div>
      <div class="nombre"><b>Estación Fiscal</b><span>${sub}</span></div>
      <nav>${enlaces}</nav>
      <div class="quien"><b>${NOMBRE_ROL[rol]}</b><form method="post" action="/salir"><button class="gris">Salir</button></form></div>
    </div>`;
  };

  const servidor = createServer(async (req, res) => {
    const url = new URL(req.url ?? '/', `http://127.0.0.1:${puerto}`);
    const ruta = url.pathname;
    const metodo = req.method ?? 'GET';
    try {
      // ===== La cara de aplicacion (icono y manifest, publicos) =====
      // Con esto la ventana en modo app (Edge --app) sale con su nombre, su
      // icono y su color, y el cliente jamas ve una direccion.
      if (ruta === '/manifest.webmanifest' && metodo === 'GET') {
        res.writeHead(200, { 'content-type': 'application/manifest+json', 'cache-control': 'no-store' });
        return res.end(JSON.stringify({
          name: 'Estación Fiscal',
          short_name: 'Estación Fiscal',
          start_url: '/',
          display: 'standalone',
          background_color: '#0B0D14',
          theme_color: '#0B0D14',
          icons: [{ src: '/icono.svg', sizes: 'any', type: 'image/svg+xml' }],
        }));
      }
      if (ruta === '/icono.svg' && metodo === 'GET') {
        res.writeHead(200, { 'content-type': 'image/svg+xml', 'cache-control': 'max-age=86400' });
        return res.end(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="14" fill="#0B0D14"/>
<rect x="6" y="6" width="52" height="52" rx="10" fill="none" stroke="#FBBF24" stroke-width="4"/>
<text x="32" y="42" text-anchor="middle" font-family="Segoe UI, sans-serif" font-size="26" font-weight="800" fill="#FBBF24">EF</text>
</svg>`);
      }
      // ===== PRIMER USO: la estacion nace sin portero y hay que ponerlo =====
      if (!acceso.yaTienePortero() && (ruta === '/primer-uso' || ruta === '/' || ruta === '/entrar')) {
        if (ruta === '/primer-uso' && metodo === 'POST') {
          const f = await leerFormulario(req);
          const clave = f.get('clave') ?? '';
          if (clave !== (f.get('repetir') ?? '')) {
            return html(res, `<h1>Las dos claves no son iguales</h1><a href="/primer-uso"><button>Volver</button></a>`, 400);
          }
          try {
            acceso.fijarClave('dueno', clave);
          } catch (e) {
            return html(res, `<h1>No se pudo</h1><p class="mal">${escapar((e as Error).message)}</p>
              <a href="/primer-uso"><button>Volver</button></a>`, 400);
          }
          log('Se puso la clave del dueño: la estacion ya tiene portero.');
          return redirigir(res, '/', `${GALLETA}=${sesiones.abrir('dueno')}; HttpOnly; SameSite=Strict; Path=/`);
        }
        return html(res, `<div class="antesala">
          ${ladoMarca()}
          <div class="lado-form">
            <form method="post" action="/primer-uso" class="form-tarjeta">
              <h1>Primer uso</h1>
              <p class="sub">Esta estacion todavia no tiene clave. Ponle la del dueño antes de nada: sin eso, cualquiera que se siente en esta computadora puede cambiar el RIF o anular una factura.</p>
              <label>Clave del dueño (minimo 8 caracteres)</label>
              <div class="campo">
                ${SVG_CANDADO}
                <input name="clave" id="clave" type="password" required autofocus autocomplete="new-password">
                <button type="button" class="ojo" onclick="verClave()" aria-label="Ver la clave">${SVG_OJO}</button>
              </div>
              <label>Repitela</label>
              <div class="campo">
                ${SVG_CANDADO}
                <input name="repetir" type="password" required autocomplete="new-password">
              </div>
              <button type="submit" class="entrar">Guardar y entrar</button>
              <p class="sub" style="margin:14px 0 0">Despues, desde Claves, le pones la suya al cajero y la de consulta al SENIAT.</p>
            </form>
          </div>
        </div>
        <script>function verClave(){const c=document.getElementById('clave');c.type=c.type==='password'?'text':'password';c.focus()}</script>`);
      }

      // ===== LA PUERTA: una sola. La clave dice quien entra. =====
      if (ruta === '/entrar') {
        if (metodo === 'POST') {
          const f = await leerFormulario(req);
          const mal = (m: string): void =>
            html(res, `<h1>No se pudo entrar</h1><p class="mal">${escapar(m)}</p>
              <a href="/entrar"><button>Volver</button></a>`, 401);
          let quien: Rol | null;
          try {
            quien = acceso.entrarConClave(f.get('clave') ?? '');
          } catch (e) {
            return mal((e as Error).message);
          }
          if (!quien) return mal('Clave incorrecta');
          log(`Entro el ${NOMBRE_ROL[quien]} a la estacion.`);
          return redirigir(res, '/', `${GALLETA}=${sesiones.abrir(quien)}; HttpOnly; SameSite=Strict; Path=/`);
        }
        return html(res, `<div class="antesala">
          ${ladoMarca()}
          <div class="lado-form">
            <form method="post" action="/entrar" class="form-tarjeta">
              <h1>Bienvenido</h1>
              <p class="sub">Cada venta, sellada y en regla.</p>
              <label>Clave</label>
              <div class="campo">
                ${SVG_CANDADO}
                <input name="clave" id="clave" type="password" required autofocus autocomplete="current-password">
                <button type="button" class="ojo" onclick="verClave()" aria-label="Ver la clave">${SVG_OJO}</button>
              </div>
              <button type="submit" class="entrar">Entrar</button>
            </form>
          </div>
        </div>
        <script>function verClave(){const c=document.getElementById('clave');c.type=c.type==='password'?'text':'password';c.focus()}</script>`);
      }

      if (ruta === '/salir') {
        sesiones.cerrar(galletaDe(req));
        return redirigir(res, '/entrar', `${GALLETA}=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0`);
      }

      // ===== LA PUERTA DEL FISCAL (tiene su propia clave, no usa sesion) =====
      // Mismo molde que el resto de la casa: antesala split-screen con la marca
      // y, adentro, tablero a lo ancho. La clave vive en memoria de la pagina y
      // viaja como Bearer; salir recarga y la suelta.
      if (ruta === '/fiscal' && metodo === 'GET') {
        return html(res, `<div class="antesala" id="antesala">
          ${ladoMarca()}
          <div class="lado-form">
            <form class="form-tarjeta" onsubmit="entrar(event)">
              <h1>Consulta fiscal</h1>
              <p class="sub">Acceso de solo lectura para el SENIAT. Toda consulta queda registrada.</p>
              <label>Clave de consulta</label>
              <div class="campo">
                ${SVG_CANDADO}
                <input id="clave" type="password" required autofocus autocomplete="off">
                <button type="button" class="ojo" onclick="verClave()" aria-label="Ver la clave">${SVG_OJO}</button>
              </div>
              <button type="submit" class="entrar">Entrar a la consulta</button>
              <p class="sub" id="avisoEntrar" style="margin:14px 0 0"></p>
            </form>
          </div>
        </div>
        <div id="tablero" style="display:none">
          <div class="topbar">
            <div class="cubo">EF</div>
            <div class="nombre"><b>Estación Fiscal</b><span>${conSerial(escapar(leerConfig(op.base)?.emisor.razon_social ?? 'Sin configurar') + (leerConfig(op.base)?.emisor.rif ? ' · ' + escapar(leerConfig(op.base)!.emisor.rif) : ''))}</span></div>
            <div class="quien"><b>SENIAT · Consulta</b><button class="gris" onclick="salir()">Salir</button></div>
          </div>
          <div class="cinta">
            <span class="pill o">Solo lectura</span>
            <span class="pill v" id="pillLibro" style="display:none">Libro íntegro</span>
            <span class="pill r" id="pillLibroMal" style="display:none">Libro: revisar</span>
            <span class="hoy" id="rangoDicho"></span>
          </div>
          <div class="tarjeta">
            <div style="display:flex;gap:14px;flex-wrap:wrap;align-items:flex-end">
              <div style="flex:1;min-width:170px"><label>Desde</label><input id="desde" type="date"></div>
              <div style="flex:1;min-width:170px"><label>Hasta</label><input id="hasta" type="date"></div>
              <button onclick="consultar()" style="margin-top:0">Consultar documentos</button>
              <button onclick="verEventos()" class="gris" style="margin-top:0">Registro de eventos</button>
              <button onclick="bajar('/api/fiscal/csv','documentos.csv')" class="gris" style="margin-top:0">Exportar documentos (CSV)</button>
              <button onclick="bajar('/api/fiscal/eventos_csv','eventos.csv')" class="gris" style="margin-top:0">Exportar eventos (CSV)</button>
            </div>
          </div>
          <div id="salida"></div>
        </div>
        <script>
        let CLAVE = '';
        const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c)=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
        function verClave(){const c=document.getElementById('clave');c.type=c.type==='password'?'text':'password';c.focus()}
        function salir(){location.reload()}
        async function pedir(camino){
          const q = new URLSearchParams({desde:desde.value||'', hasta:hasta.value||''});
          return fetch(camino+'?'+q.toString(), {headers:{authorization:'Bearer '+CLAVE}});
        }
        async function entrar(ev){
          ev.preventDefault();
          CLAVE = document.getElementById('clave').value;
          const hoy = new Date();
          const p = (n)=>String(n).padStart(2,'0');
          document.getElementById('hasta').value = hoy.getFullYear()+'-'+p(hoy.getMonth()+1)+'-'+p(hoy.getDate());
          document.getElementById('desde').value = hoy.getFullYear()+'-'+p(hoy.getMonth()+1)+'-01';
          const r = await pedir('/api/fiscal/documentos');
          if(!r.ok){
            const j = await r.json().catch(()=>({}));
            document.getElementById('avisoEntrar').innerHTML = '<span class="mal">'+esc(j.problema || 'La clave no es correcta')+'</span>';
            CLAVE = '';
            return;
          }
          document.getElementById('antesala').remove();
          document.getElementById('tablero').style.display = '';
          pintarDocumentos(await r.json());
        }
        function pintarDocumentos(d){
          document.getElementById('pillLibro').style.display = d.integridad.integro ? '' : 'none';
          document.getElementById('pillLibroMal').style.display = d.integridad.integro ? 'none' : '';
          document.getElementById('rangoDicho').textContent = 'Período consultado: '+(desde.value||'inicio')+' a '+(hasta.value||'hoy');
          const kpi = (t,n,d2)=>'<div class="kpi"><div class="t">'+t+'</div><div class="n">'+n+'</div><div class="d">'+d2+'</div></div>';
          salida.innerHTML =
            '<div class="kpis">'+
              kpi('Documentos', d.totales.documentos, 'del período')+
              kpi('Base imponible', d.totales.base_usd.toFixed(2), 'USD')+
              kpi('Impuesto (IVA)', d.totales.iva_usd.toFixed(2), 'USD')+
              kpi('Total', d.totales.total_usd.toFixed(2), 'USD')+
            '</div>'+
            (d.totales.por_alicuota.length ? '<div class="tarjeta">'+d.totales.por_alicuota.map(a=>
              '<div class="dato"><span>Alícuota '+esc(a.alicuota)+'%</span><b>base '+a.base_usd.toFixed(2)+' · IVA '+a.iva_usd.toFixed(2)+' USD</b></div>').join('')+'</div>' : '')+
            '<div class="tarjeta"><table><tr><th>Tipo</th><th>Documento</th><th>N.º de control</th><th>Fecha</th><th>Cliente</th><th class="num">Total USD</th><th class="num">Total Bs</th><th>Transmisión</th></tr>'+
            d.filas.map(f=>'<tr><td>'+esc(f.tipo)+'</td><td class="doc">'+esc(f.serie)+'-'+esc(f.numero)+'</td><td class="doc">'+esc(f.numero_control_imprenta||'—')+'</td><td>'+new Date(f.fecha).toLocaleString('es-VE')+'</td><td>'+esc(f.cliente)+'</td><td class="num">'+f.total_usd.toFixed(2)+'</td><td class="num">'+(f.total_bs==null?'—':f.total_bs.toFixed(2))+'</td><td>'+esc(f.transmision)+'</td></tr>').join('')+
            '</table></div>';
        }
        async function consultar(){
          const r = await pedir('/api/fiscal/documentos');
          if(!r.ok){ salida.innerHTML='<div class="tarjeta"><p class="mal">No se pudo consultar. Verifique la clave o intente de nuevo.</p></div>'; return; }
          pintarDocumentos(await r.json());
        }
        async function verEventos(){
          const r = await pedir('/api/fiscal/eventos');
          if(!r.ok){ salida.innerHTML='<div class="tarjeta"><p class="mal">No se pudo consultar. Verifique la clave o intente de nuevo.</p></div>'; return; }
          const d = await r.json();
          document.getElementById('rangoDicho').textContent = 'Período consultado: '+(desde.value||'inicio')+' a '+(hasta.value||'hoy');
          salida.innerHTML =
            '<div class="kpis" style="grid-template-columns:repeat(2,1fr)">'+
              '<div class="kpi"><div class="t">Eventos del período</div><div class="n">'+d.eventos.length+'</div><div class="d">registro automático</div></div>'+
              '<div class="kpi"><div class="t">Cadena de eventos</div><div class="n '+(d.integridad.integro?'bien':'mal')+'" style="font-size:22px;padding-top:10px">'+(d.integridad.integro?'ÍNTEGRA':'REVISAR')+'</div><div class="d">verificación de hashes</div></div>'+
            '</div>'+
            '<div class="tarjeta"><table><tr><th class="num">N.º</th><th>Fecha y hora</th><th>Quién</th><th>Desde</th><th>Acción</th></tr>'+
            d.eventos.map(e=>'<tr><td class="num">'+e.seq+'</td><td>'+new Date(e.en).toLocaleString('es-VE')+'</td><td>'+esc(e.quien)+'</td><td>'+esc(e.desde)+'</td><td>'+esc(e.accion)+'</td></tr>').join('')+
            '</table></div>';
        }
        async function bajar(camino, nombre){
          const r = await pedir(camino);
          if(!r.ok){ salida.innerHTML='<div class="tarjeta"><p class="mal">No se pudo exportar. Verifique la clave o intente de nuevo.</p></div>'; return; }
          const t = await r.text();
          const a = document.createElement('a');
          a.href = URL.createObjectURL(new Blob([t],{type:'text/csv'}));
          a.download = nombre; a.click();
        }
        </script>`);
      }

      if (ruta.startsWith('/api/fiscal/') && metodo === 'GET') {
        const est = await abrirEstacion();
        if (!est) return json(res, { problema: 'La estacion no esta lista' }, 503);
        // La clave puede venir por query (la pagina /fiscal) o como Bearer en la
        // cabecera (la "interfaz de programacion" del art. 3, num. 8: un sistema
        // del SENIAT consulta sin pasar por la pagina).
        const clave =
          url.searchParams.get('clave') ??
          (req.headers.authorization ?? '').replace(/^Bearer\s+/i, '');
        try {
          if (!acceso.tieneClave('auditor') || !acceso.entrar('auditor', clave)) {
            return json(res, { problema: 'Clave incorrecta' }, 401);
          }
        } catch (e) {
          return json(res, { problema: (e as Error).message }, 429);
        }
        const desde = new Date(url.searchParams.get('desde') || '2000-01-01');
        const hasta = new Date(`${url.searchParams.get('hasta') || '2999-12-31'}T23:59:59`);
        const aud = est.auditor('fiscal', req.socket.remoteAddress ?? 'local');
        if (ruta === '/api/fiscal/documentos') {
          return json(res, { filas: aud.reporte(desde, hasta), totales: aud.totales(desde, hasta), integridad: aud.comprobarIntegridad() });
        }
        if (ruta === '/api/fiscal/csv') {
          res.writeHead(200, { 'content-type': 'text/csv; charset=utf-8' });
          return res.end(aud.exportarCsv(desde, hasta));
        }
        // El registro de EVENTOS (art. 3, num. 8: registros fiscales Y de eventos).
        if (ruta === '/api/fiscal/eventos') {
          return json(res, { eventos: aud.eventos(desde, hasta), integridad: aud.comprobarIntegridadEventos() });
        }
        if (ruta === '/api/fiscal/eventos_csv') {
          res.writeHead(200, { 'content-type': 'text/csv; charset=utf-8' });
          return res.end(aud.exportarEventosCsv(desde, hasta));
        }
        // Un documento puntual con su detalle completo (para el fiscal si; el
        // publico solo ve la comprobacion del sello, sin detalle).
        if (ruta === '/api/fiscal/documento') {
          const tipo = url.searchParams.get('tipo') ?? '';
          const numero = Number(url.searchParams.get('numero'));
          if (!['factura', 'nota_credito', 'nota_debito'].includes(tipo) || !Number.isInteger(numero)) {
            return json(res, { problema: 'Indica tipo (factura, nota_credito o nota_debito) y numero' }, 400);
          }
          const doc = aud.verDocumento(tipo as 'factura' | 'nota_credito' | 'nota_debito', numero);
          if (!doc) return json(res, { problema: 'No existe ese documento' }, 404);
          return json(res, doc);
        }
      }

      // ===== LA PUERTA DE LOS PROGRAMAS DEL NEGOCIO =====
      // Esta escribe en el libro fiscal, asi que pide la llave de maquina. Es
      // local, si, pero un programa cualquiera de esa computadora no tiene por
      // que poder quemar numeracion fiscal.
      if (ruta === '/api/venta' && metodo === 'POST') {
        if (!esMaquina(req)) return json(res, { sellada: false, problema: 'Falta la llave de la estacion' }, 401);
        const est = await abrirEstacion();
        if (!est) return json(res, { sellada: false, problema: 'La estacion fiscal no esta configurada' }, 503);
        const cuerpo = await leerCuerpo(req);
        // Dos formas de entregar por la misma puerta:
        //   - la CRUDA de Gustito/Cerebrito ({tipo, origen_id, venta:{...}}, la
        //     misma forma que viaja por la cola de la nube): se convierte aca
        //     adentro, porque la alicuota es cosa de la Estacion, y
        //   - la VentaEntrante ya armada (renglones con alicuota), para los
        //     programas administrativos de terceros que siguen la guia.
        const esCruda = typeof cuerpo.tipo === 'string' && typeof cuerpo.origen_id === 'string' && !!cuerpo.venta;
        const r = esCruda
          ? await est.recibirVentaDeGustito(cuerpo as unknown as Omit<VentaDeNube, 'cola_id'>, 'cerebrito')
          : await est.recibirVenta(cuerpo as unknown as VentaEntrante, 'gustito');
        log(`Venta ${String(cuerpo.origen_id)}: ${r.sellada ? `${r.serie}-${r.numero}` : r.problema}`);
        return json(res, r, r.sellada ? 200 : 400);
      }

      // El apreton de manos del puente: el programa del negocio (el Cerebrito)
      // comprueba que su llave sirve y a que negocio le esta hablando, sin
      // escribir nada en el libro.
      if (ruta === '/api/puente' && metodo === 'GET') {
        if (!esMaquina(req)) return json(res, { ok: false, problema: 'Esa llave no es de esta estacion' }, 401);
        const est = await abrirEstacion();
        if (!est) return json(res, { ok: false, problema: 'La estacion fiscal no esta configurada' }, 503);
        const e = est.estado();
        // interfaz: la version del contrato de GUIA_INTEGRACION.md que habla esta
        // Estacion. Sube SOLO con un cambio incompatible; lo aditivo no la mueve.
        return json(res, { ok: true, negocio: e.negocio, rif: e.rif, serial: est.serial(), documentos: e.documentos, puede_sellar: e.puede_sellar, interfaz: 1 });
      }

      // ===== DE AQUI PARA ABAJO, TODO PIDE QUIEN SOS =====
      const rol = quienEs(req);
      const maquina = esMaquina(req);
      const esApi = ruta.startsWith('/api/');

      /** Corta si no tiene permiso. Devuelve true cuando ya respondio. */
      const cortado = (accion: Accion, dejarMaquina = false): boolean => {
        if (dejarMaquina && maquina) return false;
        if (!rol) {
          if (esApi) json(res, { problema: 'Hay que entrar con clave' }, 401);
          else redirigir(res, '/entrar');
          return true;
        }
        if (!puede(rol, accion)) {
          const m = `El ${NOMBRE_ROL[rol]} no puede hacer eso`;
          if (esApi) json(res, { problema: m }, 403);
          else html(res, `<h1>Sin permiso</h1><p class="mal">${m}.</p><a href="/"><button>Volver</button></a>`, 403);
          return true;
        }
        return false;
      };

      // ===== Estado (el dia a dia): el tablero =====
      if (ruta === '/' && metodo === 'GET') {
        if (cortado('ver')) return;
        const yo = rol as Rol;
        const est = await abrirEstacion();
        if (!est) {
          const faltan = revisar(leerConfig(op.base)).map((f) => `<li>${escapar(f.problema)}</li>`).join('');
          return html(res, `${cabecera(yo, 'Inicio')}
            <p class="aviso" style="font-size:16px;font-weight:700">Todavia no esta lista para sellar documentos.</p>
            <div class="dos" style="margin-top:14px"><div class="tarjeta" style="margin:0"><b>Falta:</b><ul style="margin:8px 0 0;padding-left:20px;line-height:1.9">${faltan}</ul></div></div>
            ${puede(yo, 'configurar')
              ? '<a href="/configurar"><button style="margin-top:16px">Configurar la estacion</button></a>'
              : '<p class="pie">Esto lo configura el responsable con su clave.</p>'}`);
        }
        const e = est.estado();
        const resp = est.estadoRespaldo();
        const docs = est.documentos();
        const hoyTexto = new Date().toDateString();
        const deHoy = docs.filter((a) => new Date(a.sellado_en).toDateString() === hoyTexto).length;
        const TIPO: Record<string, string> = { factura: 'FAC', nota_credito: 'NC', nota_debito: 'ND', factura_manual: 'TALON' };
        const filas = docs.slice(-10).reverse().map((a) => {
          const esNota = a.tipo === 'nota_credito';
          const quien = esNota && a.ref
            ? `corrige la ${TIPO[a.ref.tipo] ?? a.ref.tipo} ${a.ref.serie}-${a.ref.numero}`
            : escapar(a.documento.cliente?.nombre ?? 'Consumidor Final');
          return `<tr><td class="doc${esNota ? ' mal' : ''}">${TIPO[a.tipo] ?? a.tipo} ${a.serie}-${a.numero}</td>
            <td>${new Date(a.sellado_en).toLocaleString('es-VE')}</td><td>${quien}</td>
            <td class="num${esNota ? ' mal' : ''}">${esNota ? '&minus;' : ''}${a.documento.total_usd.toFixed(2)}</td></tr>`;
        }).join('') || '<tr><td colspan="4">Todavia no hay documentos</td></tr>';
        const arranqueWin = op.esExe ? await autoarranqueActivo() : null;
        // Revisar el canal de versiones al abrir Inicio, con freno de 5 minutos.
        if (Date.now() - ultimaRevisionVersion > 300_000) {
          ultimaRevisionVersion = Date.now();
          void avisador.verificar();
        }
        const ver = avisador.estado();
        const avisoVersion = ver.disponible
          ? `<div class="tarjeta avisin"><div class="barra"><div>
              <b>Versión ${escapar(ver.disponible)} disponible</b>
              <p class="pie" style="margin:4px 0 0">Esta instalación tiene la ${escapar(ver.actual)}. La actualización no se aplica sola: la decide el dueño.</p>
            </div>${puede(yo, 'configurar') ? `<form method="post" action="/actualizar"><button${ver.descargando ? ' disabled' : ''}>${ver.descargando ? 'Descargando…' : 'Aplicar ahora'}</button></form>` : ''}</div></div>`
          : '';
        return html(res, `${cabecera(yo, 'Inicio')}
          ${avisoVersion}
          <div class="cinta">
            <span class="pill ${e.puede_sellar ? 'v' : 'r'}">${e.puede_sellar ? 'Sellando documentos' : 'No esta sellando'}</span>
            <span class="pill ${e.reloj_bien ? 'v' : 'r'}">Hora ${e.reloj_bien ? 'en orden' : 'corrida'}</span>
            <span class="pill o">Imprenta: ${escapar(e.imprenta)}${e.imprenta_real ? '' : ' (practica)'}</span>
            <span class="hoy">${new Date().toLocaleString('es-VE', { weekday: 'long', day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })}</span>
          </div>
          <div class="kpis">
            <div class="kpi"><div class="t">Documentos emitidos</div><div class="n" style="font-size:${tamanoCifra(e.documentos)}px">${e.documentos}</div><div class="d">hoy: ${deHoy}</div></div>
            <div class="kpi"><div class="t">Por transmitir</div><div class="n ${e.pendientes ? 'aviso' : 'bien'}">${e.pendientes}</div><div class="d">${e.pendientes ? 'esperando internet o imprenta' : 'todo al dia'}</div></div>
            <div class="kpi"><div class="t">Rechazados</div><div class="n ${e.rechazados ? 'mal' : 'bien'}">${e.rechazados}</div><div class="d">${e.rechazados ? 'REVISAR con la imprenta' : 'nada que revisar'}</div></div>
            <div class="kpi"><div class="t">Libro fiscal</div><div class="n ${e.libro_integro ? 'bien' : 'mal'}" style="font-size:27px;padding-top:9px">${e.libro_integro ? 'ÍNTEGRO' : 'REVISAR'}</div><div class="d">${e.libro_integro ? 'cadena verificada' : 'el libro no cuadra'}</div></div>
          </div>
          <div class="dos">
            <div class="tarjeta" style="margin:0">
              <h2 style="margin-top:0">Ultimos documentos</h2>
              <table><tr><th>Documento</th><th>Fecha</th><th>Cliente</th><th class="num">Total USD</th></tr>${filas}</table>
              <div style="display:flex;gap:10px;flex-wrap:wrap">
                <a href="/documentos"><button class="gris">Ver todos</button></a>
                ${puede(yo, 'reintentar') && e.pendientes ? '<button class="gris" onclick="empujar(this)">Enviar lo pendiente</button>' : ''}
                <form method="post" action="/reloj" style="display:inline"><button class="gris">Revisar la hora</button></form>
              </div>
            </div>
            <div style="display:grid;gap:14px">
              <div class="tarjeta" style="margin:0">
                <h2 style="margin-top:0">El libro, resguardado</h2>
                <div class="dato"><span>Respaldo fuera del equipo</span><b class="${resp.al_dia ? 'bien' : 'aviso'}">${resp.afuera} de ${resp.aqui}</b></div>
                <div class="dato"><span>Ultima salida</span><b>${resp.subido_en ? new Date(resp.subido_en).toLocaleString('es-VE') : 'nunca'}</b></div>
                <div class="dato"><span>Hora del equipo</span><b class="${e.reloj_bien ? 'bien' : 'mal'}">${escapar(e.reloj)}</b></div>
                <div class="dato"><span>Serial de la estacion</span><b class="${est.serial() ? '' : 'aviso'}">${escapar(est.serial() ?? 'sin serial (practica)')}</b></div>
                <div class="dato"><span>Version del sistema</span><b>${escapar(VERSION_ESTACION)}</b></div>
                ${arranqueWin === null ? '' : `<div class="dato"><span>Arranca con Windows</span><b class="${arranqueWin ? 'bien' : 'mal'}">${arranqueWin ? 'Activado' : 'Apagado'}</b></div>`}
              </div>
              ${e.puede_sellar ? '' : `<div class="tarjeta" style="margin:0;border-color:rgba(248,113,113,.5)"><p class="mal" style="margin:0 0 8px"><b>La estacion no esta sellando documentos.</b></p>
                <p class="pie" style="margin:0">La hora de este equipo no coincide con la hora de referencia. Corrige la fecha y la hora de Windows (lo mas sencillo es dejarla en automatico) y usa Revisar la hora. Las ventas no se pierden, se sellan apenas se arregle.</p></div>`}
              ${acceso.tieneClave('auditor') || !puede(yo, 'claves') ? '' : `<div class="tarjeta avisin" style="margin:0">
                <h2 style="margin-top:0;color:var(--oro)">Pendiente del negocio</h2>
                <p class="pie" style="margin:0"><b style="color:var(--texto)">Falta la clave de consulta del SENIAT.</b> La Providencia 000121 (articulo 3, numeral 8) obliga a tenerla lista para cuando el funcionario la pida. Se pone en <a href="/claves">Claves</a> y no puede ser la misma tuya.</p></div>`}
              ${puede(yo, 'anular') ? `<div class="tarjeta" style="margin:0">
                <h2 style="margin-top:0">Anular una factura</h2>
                <p class="pie" style="margin:0">Una factura sellada no se borra nunca: se corrige con una nota de credito, y queda escrito quien la pidio.</p>
                <label>Numero de la factura</label><input id="anNumero" type="number" min="1">
                <label>Motivo</label><input id="anMotivo" placeholder="Por que se anula">
                <button onclick="anular()">Anular con nota de credito</button>
                <p id="anSalida"></p>
              </div>` : ''}
            </div>
          </div>
          <p class="pie">Para el SENIAT: la consulta del fiscal esta en <b>/fiscal</b>, con su propia clave.</p>
          <script>
          async function anular(){
            const r = await fetch('/api/anular',{method:'POST',headers:{'content-type':'application/json'},
              body:JSON.stringify({numero:Number(anNumero.value),motivo:anMotivo.value})});
            const d = await r.json();
            anSalida.className = d.sellada ? 'bien' : 'mal';
            anSalida.textContent = d.sellada ? ('Lista la nota de credito '+d.serie+'-'+d.numero) : d.problema;
          }
          async function empujar(b){
            b.disabled = true; b.textContent = 'Enviando...';
            await fetch('/api/reintentar',{method:'POST'});
            location.reload();
          }
          </script>`);
      }

      // ===== Todos los documentos =====
      if (ruta === '/documentos' && metodo === 'GET') {
        if (cortado('ver')) return;
        const est = await abrirEstacion();
        if (!est) return redirigir(res, '/');
        const docs = est.documentos();
        const TIPO: Record<string, string> = { factura: 'FAC', nota_credito: 'NC', nota_debito: 'ND', factura_manual: 'TALON' };
        const filas = docs.slice(-500).reverse().map((a) => {
          const esNota = a.tipo === 'nota_credito';
          const quien = esNota && a.ref
            ? `corrige la ${TIPO[a.ref.tipo] ?? a.ref.tipo} ${a.ref.serie}-${a.ref.numero}`
            : escapar(a.documento.cliente?.nombre ?? 'Consumidor Final');
          return `<tr><td class="doc${esNota ? ' mal' : ''}">${TIPO[a.tipo] ?? a.tipo} ${a.serie}-${a.numero}</td>
            <td class="num">${a.control}</td>
            <td>${new Date(a.sellado_en).toLocaleString('es-VE')}</td><td>${quien}</td>
            <td class="num${esNota ? ' mal' : ''}">${esNota ? '&minus;' : ''}${a.documento.total_usd.toFixed(2)}</td></tr>`;
        }).join('') || '<tr><td colspan="5">Todavia no hay documentos</td></tr>';
        const hoy = new Date();
        const primeroDelMes = new Date(hoy.getFullYear(), hoy.getMonth(), 1).toISOString().slice(0, 10);
        const puedeExportar = puede(rol as Rol, 'exportar');
        return html(res, `${cabecera(rol as Rol, 'Documentos')}
          ${!puedeExportar ? '' : `<div class="tarjeta">
            <h2 style="margin-top:0">Entregarle los documentos al contador</h2>
            <p class="pie" style="margin-top:0">Baja el archivo del periodo y mandaselo a tu contador. Se abre en Excel.
            <b>No le entregues la clave de consulta del SENIAT</b>: esa es para el funcionario, y con este archivo el contador tiene lo que necesita.</p>
            <div style="display:flex;gap:10px;flex-wrap:wrap;align-items:end">
              <div style="flex:1;min-width:150px"><label>Desde</label><input type="date" id="expDesde" value="${primeroDelMes}"></div>
              <div style="flex:1;min-width:150px"><label>Hasta</label><input type="date" id="expHasta" value="${hoy.toISOString().slice(0, 10)}"></div>
              <button onclick="bajarDocumentos()">Bajar el archivo</button>
            </div>
            <p id="expSalida" class="pie" style="margin-bottom:0"></p>
          </div>`}
          <div class="tarjeta">
            <h2 style="margin-top:0">Todos los documentos (${docs.length})</h2>
            <table><tr><th>Documento</th><th class="num">Control</th><th>Fecha</th><th>Cliente</th><th class="num">Total USD</th></tr>${filas}</table>
            ${docs.length > 500 ? `<p class="pie">En pantalla se muestran los ultimos 500${puedeExportar ? '. El archivo de arriba trae el periodo completo' : ''}.</p>` : ''}
          </div>
          ${!puedeExportar ? '' : `<script>
          async function bajarDocumentos(){
            const salida = document.getElementById('expSalida');
            const desde = document.getElementById('expDesde').value;
            const hasta = document.getElementById('expHasta').value;
            if (!desde || !hasta || desde > hasta) {
              salida.className = 'mal';
              salida.textContent = 'Revisa las fechas: la de inicio no puede ser posterior a la del final.';
              return;
            }
            salida.className = 'pie'; salida.textContent = 'Preparando el archivo...';
            const r = await fetch('/api/documentos.csv?' + new URLSearchParams({ desde, hasta }).toString());
            if (!r.ok) { salida.className = 'mal'; salida.textContent = 'No se pudo preparar el archivo.'; return; }
            const a = document.createElement('a');
            a.href = URL.createObjectURL(new Blob([await r.text()], { type: 'text/csv;charset=utf-8' }));
            a.download = 'documentos-' + desde + '-al-' + hasta + '.csv';
            a.click();
            salida.className = 'bien';
            salida.textContent = 'Listo. El archivo quedo en tus descargas.';
          }
          </script>`}`);
      }

      /**
       * El negocio se lleva SUS documentos.
       *
       * Antes esto solo salia por la puerta del fiscal, o sea que para darle el
       * periodo a su contador el dueño tenia que entregarle la clave del
       * SENIAT: una clave que es para el funcionario y de un solo proposito.
       * Ahora el dueño baja lo suyo con su propia sesion y no comparte nada.
       */
      if (ruta === '/api/documentos.csv' && metodo === 'GET') {
        if (cortado('exportar')) return;
        const est = await abrirEstacion();
        if (!est) return json(res, { problema: 'La estacion no esta lista' }, 503);
        const desde = new Date(url.searchParams.get('desde') || '2000-01-01');
        const hasta = new Date(`${url.searchParams.get('hasta') || '2999-12-31'}T23:59:59`);
        // Se usa el mismo auditor que atiende al fiscal: mismo contenido, misma
        // verdad, sin una segunda version del reporte que se pueda desviar.
        const aud = est.auditor(NOMBRE_ROL[rol as Rol], req.socket.remoteAddress ?? 'local');
        res.writeHead(200, { 'content-type': 'text/csv; charset=utf-8', 'cache-control': 'no-store' });
        return res.end(aud.exportarCsv(desde, hasta));
      }

      // ===== Configuracion (solo el dueño) =====
      if (ruta === '/configurar' && metodo === 'GET') {
        if (cortado('configurar')) return;
        const c = leerConfig(op.base);
        const v = (x?: string | number | null) => (x == null ? '' : escapar(x));
        // El arranque con Windows solo existe en el programa instalado.
        const arranqueHtml = op.esExe
          ? await (async () => {
              const activo = await autoarranqueActivo();
              return `<h2>Arranque con Windows</h2>
          <div class="tarjeta">
            <p class="pie" style="margin-top:0">Una estacion fiscal no puede depender de que alguien se acuerde de abrirla: con esto arranca sola (y oculta) al prender la computadora. Se asegura solo en cada arranque, salvo que lo apagues aqui.</p>
            <div class="dato"><span>Ahora mismo</span><b class="${activo ? 'bien' : 'mal'}">${activo ? 'Activado' : 'Apagado'}</b></div>
            <form method="post" action="/arranque"><input type="hidden" name="activar" value="${activo ? 'no' : 'si'}">
            <button class="${activo ? 'gris' : ''}">${activo ? 'Apagarlo' : 'Activarlo'}</button></form>
          </div>`;
            })()
          : '';
        return html(res, `${cabecera(rol as Rol, 'Configuración')}
          <form method="post" action="/configurar">
          <div class="tarjeta">
            <label>RIF del negocio</label><input name="rif" value="${v(c?.emisor.rif)}" placeholder="J-12345678-9" required>
            <label>Razon social</label><input name="razon_social" value="${v(c?.emisor.razon_social)}" required>
            <label>Domicilio fiscal</label><input name="domicilio_fiscal" value="${v(c?.emisor.domicilio_fiscal)}" required>
            <label>IVA general</label><select name="alicuota_general">
              <option value="16"${c?.emisor.alicuota_general === 16 ? ' selected' : ''}>16%</option>
              <option value="8"${c?.emisor.alicuota_general === 8 ? ' selected' : ''}>8%</option>
              <option value="0"${c?.emisor.alicuota_general === 0 ? ' selected' : ''}>Exento</option>
            </select>
            <label>Serie</label><input name="serie" value="${v(c?.serie) || 'A'}" required>
            <label>Nombre de este equipo</label><input name="equipo" value="${v(c?.equipo) || 'FISCAL-01'}" required>
          </div>
          <button type="submit">Guardar</button>
          </form>
          ${imprentaHtml(c)}
          <h2>Modelo de factura</h2>
          <div class="tarjeta">
            <p class="pie" style="margin-top:0">La cara de la factura ante el cliente del negocio. Los datos que exige el articulo 7 de la Providencia 000102 van siempre y no se pueden quitar ni mover; esto agrega la marca del negocio en la zona libre del documento. Cada cambio queda anotado en el registro de eventos.</p>
            <form method="post" action="/modelo-factura">
              <label>Mensaje del pie (se imprime en el papel, opcional)</label>
              <input name="mensaje_pie" maxlength="240" value="${v(c?.modelo_factura?.mensaje_pie)}" placeholder="Gracias por su compra">
              <label>Logo para la factura del telefono (direccion https, opcional)</label>
              <input name="logo_url" value="${v(c?.modelo_factura?.logo_url)}" placeholder="https://...">
              <label>Color de acento de la version digital (#RRGGBB, opcional)</label>
              <input name="color" value="${v(c?.modelo_factura?.color)}" placeholder="#C9A227">
              <div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:12px">
                <button name="accion" value="probar" class="gris">Ver la muestra antes de guardar</button>
                <button name="accion" value="guardar">Guardar el modelo</button>
              </div>
            </form>
          </div>
          <h2>Llave para los programas del negocio</h2>
          <div class="tarjeta">
            <p class="pie" style="margin-top:0">Esta es la que se le pone a Gustito o al Cerebrito para que entreguen la venta a la estacion. No es de ninguna persona: es del equipo.</p>
            <code>${escapar(acceso.llaveMaquina())}</code>
            <form method="post" action="/claves"><input type="hidden" name="que" value="rotar_llave">
            <button class="gris">Cambiarla por una nueva</button></form>
          </div>
          ${arranqueHtml}
          <h2>Esta instalación</h2>
          <div class="tarjeta">
            <div class="dato"><span>Versión del programa</span><b>${escapar(avisador.estado().actual)}</b></div>
            <p class="pie" style="margin:8px 0 0">Las versiones nuevas se avisan en Inicio y las aplica el dueño. Conforme a los artículos 9 al 11 de la Providencia SNAT/2024/000121, cada versión del sistema se homologa antes de instalarse.</p>
          </div>
          <h2>Retirar la Estación de este equipo</h2>
          <div class="tarjeta">
            <p class="pie" style="margin-top:0">Quita el arranque con Windows y los accesos del escritorio, y detiene el programa. <b>El libro fiscal no se elimina:</b> la carpeta del programa queda en su sitio y debe conservarse junto con su respaldo (los registros se conservan por diez años).</p>
            <button class="gris" onclick="document.getElementById('modalRetirar').style.display='flex'">Retirar la Estación…</button>
          </div>
          <div id="modalRetirar" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:50;align-items:center;justify-content:center;padding:20px">
            <div class="tarjeta" style="max-width:470px;margin:0">
              <b>¿Retirar la Estación de este equipo?</b>
              <p class="pie">El programa se detiene y deja de arrancar con Windows. La carpeta con el libro fiscal permanece intacta en su sitio.</p>
              <div style="display:flex;gap:10px;flex-wrap:wrap">
                <button class="gris" onclick="document.getElementById('modalRetirar').style.display='none'">Quedarse</button>
                <form method="post" action="/desinstalar"><button>Sí, retirar</button></form>
              </div>
            </div>
          </div>
          <p class="pie">Los precios de la carta se manejan con el IVA incluido: el cliente paga lo mismo de siempre.</p>`);
      }

      // ===== Aplicar una version nueva: decision del dueño, nunca sola =====
      if (ruta === '/actualizar' && metodo === 'POST') {
        if (cortado('configurar')) return;
        const err = await avisador.aplicar();
        if (err) {
          return html(res, `<h1>No se pudo aplicar</h1><p class="mal">${escapar(err)}</p><a href="/"><button>Volver</button></a>`, 400);
        }
        return html(res, `<h1>Aplicando la versión nueva</h1><p class="pie">La Estación se reinicia sola en unos segundos. Esta pestaña puede cerrarse.</p>`);
      }

      // ===== Retirar la Estacion del equipo (el libro fiscal NO se toca) =====
      if (ruta === '/desinstalar' && metodo === 'POST') {
        if (cortado('configurar')) return;
        if (!op.esExe) {
          return html(res, `<h1>Solo en el programa instalado</h1><p class="pie">En desarrollo no se toca el registro de Windows.</p><a href="/configurar"><button>Volver</button></a>`, 400);
        }
        const est = await abrirEstacion();
        est?.anotarEvento(NOMBRE_ROL[rol as Rol], 'estacion_retirada');
        await retirarDelEquipo(op.base);
        log('La Estacion fue retirada de este equipo por decision del dueño.');
        setTimeout(() => process.exit(0), 1500);
        return html(res, `<h1>La Estación quedó retirada</h1>
          <div class="tarjeta"><p class="pie" style="margin:0">El programa se detuvo y ya no arranca con Windows. La carpeta con el libro fiscal y su configuración permanece en su sitio: consérvela junto con su respaldo (los registros se conservan por diez años). Para volver a usar la Estación, abra de nuevo el ejecutable de esa carpeta.</p></div>`);
      }

      // ===== La imprenta digital: guardar credenciales y cambiar al respaldo =====
      if (ruta === '/imprenta' && metodo === 'POST') {
        if (cortado('configurar')) return;
        const datos = await leerFormulario(req);
        const previa = leerConfig(op.base);
        if (!previa) {
          return html(res, `<h1>Primero los datos del negocio</h1><p class="pie">Configura el RIF y la razon social antes de conectar la imprenta.</p><a href="/configurar"><button>Volver</button></a>`, 400);
        }
        const cual = datos.get('cual') ?? 'imprenta';
        const nombreDe = (ad: string): string =>
          PROVEEDORES.find((p) => p.id === ad)?.nombre ?? 'Imprenta';
        try {
          let nueva: ConfigFiscal;
          if (cual === 'cambiar') {
            // El cambio al salvavidas es MANUAL y queda anotado: dos fuentes de
            // numeracion a la vez si es un problema fiscal.
            const aCual = datos.get('usando') === 'respaldo' ? 'respaldo' : 'imprenta';
            if (aCual === 'respaldo' && !previa.respaldo) {
              return html(res, `<h1>No hay respaldo configurado</h1><p class="pie">Configura primero la imprenta de respaldo.</p><a href="/configurar"><button>Volver</button></a>`, 400);
            }
            nueva = { ...previa, usando: aCual };
          } else {
            const ad = (datos.get('p_adaptador') ?? 'practica') as ConfigImprenta['adaptador'];
            const armada: ConfigImprenta = {
              adaptador: ad,
              nombre: nombreDe(ad),
              url: datos.get('p_url')?.trim() || undefined,
              usuario: datos.get('p_usuario')?.trim() || undefined,
              clave: datos.get('p_clave')?.trim() || undefined,
              token: datos.get('p_token')?.trim() || undefined,
            };
            nueva = cual === 'respaldo' ? { ...previa, respaldo: armada } : { ...previa, imprenta: armada };
          }
          guardarConfig(op.base, nueva);
          estacion = null; // se reabre con la imprenta nueva
          const est = await abrirEstacion();
          est?.anotarEvento(NOMBRE_ROL[rol as Rol], cual === 'cambiar' ? 'imprenta_cambiada' : 'imprenta_configurada', {
            usando: nueva.usando,
            proveedor: (nueva.usando === 'respaldo' ? nueva.respaldo : nueva.imprenta)?.adaptador,
          });
          log(`Imprenta ${cual === 'cambiar' ? 'cambiada a ' + nueva.usando : 'configurada'}.`);
          return redirigir(res, '/configurar');
        } catch (e) {
          return html(res, `<h1>No se pudo guardar</h1><p class="mal">${escapar((e as Error).message)}</p><a href="/configurar"><button>Volver</button></a>`, 400);
        }
      }

      // Tocar la puerta de la imprenta SIN emitir: comprobar credenciales.
      if (ruta === '/api/probar-imprenta' && metodo === 'POST') {
        if (cortado('configurar')) return;
        const cuerpo = await leerCuerpo(req);
        const c = leerConfig(op.base);
        const cual = cuerpo.cual === 'respaldo' ? 'respaldo' : 'imprenta';
        const cfgImp = cual === 'respaldo' ? c?.respaldo : c?.imprenta;
        if (!cfgImp) return json(res, { ok: false, detalle: 'Todavia no hay imprenta configurada de ese lado.' });
        try {
          const imp = await crearImprenta(cfgImp);
          if (!imp.probar) return json(res, { ok: true, detalle: `${imp.nombre}: configurada.` });
          const r = await imp.probar();
          const est = await abrirEstacion();
          est?.anotarEvento(NOMBRE_ROL[rol as Rol], 'imprenta_probada', { cual, ok: r.ok });
          return json(res, r);
        } catch (e) {
          return json(res, { ok: false, detalle: (e as Error).message });
        }
      }

      // El interruptor del arranque con Windows (solo el dueño, solo el .exe).
      if (ruta === '/arranque' && metodo === 'POST') {
        if (cortado('configurar')) return;
        if (!op.esExe) {
          return html(res, `<h1>Solo en el programa instalado</h1><p class="pie">En desarrollo no se toca el registro de Windows.</p><a href="/configurar"><button>Volver</button></a>`, 400);
        }
        const datos = await leerFormulario(req);
        const activar = datos.get('activar') === 'si';
        await ponerAutoarranque(op.base, activar);
        // El apagado queda anotado a proposito: el proximo arranque del
        // programa NO lo vuelve a prender por la espalda.
        marcarApagado(op.base, !activar);
        const est = await abrirEstacion();
        est?.anotarEvento(NOMBRE_ROL[rol as Rol], activar ? 'arranque_windows_activado' : 'arranque_windows_apagado');
        return redirigir(res, '/configurar');
      }

      // ===== El modelo de factura (la zona libre del documento) =====
      if (ruta === '/modelo-factura' && metodo === 'POST') {
        if (cortado('configurar')) return;
        const datos = await leerFormulario(req);
        const previa = leerConfig(op.base);
        if (!previa) {
          return html(res, `<h1>Primero los datos del negocio</h1><p class="pie">Configura el RIF y la razon social antes del modelo de factura.</p><a href="/configurar"><button>Volver</button></a>`, 400);
        }
        const modelo: ModeloFactura = {
          mensaje_pie: (datos.get('mensaje_pie') ?? '').trim() || null,
          logo_url: (datos.get('logo_url') ?? '').trim() || null,
          color: (datos.get('color') ?? '').trim() || null,
        };
        const hayAlgo = !!(modelo.mensaje_pie || modelo.logo_url || modelo.color);
        const nueva: ConfigFiscal = { ...previa, modelo_factura: hayAlgo ? modelo : null };
        // La muestra usa el validador real: un color malo se descubre aqui, no
        // en el papel del cliente.
        const fallas = revisar(nueva);
        if (fallas.length) {
          return html(res, `<h1>Revisa el modelo</h1><p class="mal">${escapar(fallas.map((f) => f.problema).join('; '))}</p><a href="/configurar"><button>Volver</button></a>`, 400);
        }
        if (datos.get('accion') === 'probar') {
          // El papel de muestra, tal cual saldria por la termica, SIN guardar.
          const muestra = lineasFactura(asientoMuestra(previa), {
            numeroControl: '00-00000000',
            urlSello: 'https://sello.gustitoxpress.app/v/muestra',
            serial: leerSerial(op.base) ?? undefined,
            fechaAsignacion: new Date().toISOString(),
          }, hayAlgo ? modelo : null).join('\n');
          return html(res, `<h1>Asi se veria el papel</h1>
            <p class="pie">Es una muestra con datos de ejemplo, sin valor y sin guardar. Los datos del articulo 7 salen siempre; el modelo solo agrega lo del negocio.</p>
            <div class="tarjeta"><pre style="font-family:Consolas,monospace;font-size:12.5px;line-height:1.45;overflow-x:auto;margin:0">${escapar(muestra)}</pre></div>
            <form method="post" action="/modelo-factura" style="display:inline">
              <input type="hidden" name="mensaje_pie" value="${escapar(modelo.mensaje_pie ?? '')}">
              <input type="hidden" name="logo_url" value="${escapar(modelo.logo_url ?? '')}">
              <input type="hidden" name="color" value="${escapar(modelo.color ?? '')}">
              <button name="accion" value="guardar">Guardar este modelo</button>
            </form>
            <a href="/configurar"><button class="gris">Volver a editar</button></a>`);
        }
        try {
          guardarConfig(op.base, nueva);
          estacion = null;
          const est = await abrirEstacion();
          // El cambio de la cara del documento queda en el registro de eventos:
          // que se puso y quien lo puso.
          est?.anotarEvento(NOMBRE_ROL[rol as Rol], 'modelo_factura_guardado', {
            mensaje_pie: modelo.mensaje_pie,
            logo_url: modelo.logo_url,
            color: modelo.color,
          });
          return redirigir(res, '/configurar');
        } catch (e) {
          return html(res, `<h1>No se pudo guardar</h1><p class="mal">${escapar((e as Error).message)}</p><a href="/configurar"><button>Volver</button></a>`, 400);
        }
      }

      if (ruta === '/configurar' && metodo === 'POST') {
        if (cortado('configurar')) return;
        const datos = await leerFormulario(req);
        const previa = leerConfig(op.base);
        const nueva: ConfigFiscal = {
          emisor: {
            rif: datos.get('rif') ?? '',
            razon_social: datos.get('razon_social') ?? '',
            domicilio_fiscal: datos.get('domicilio_fiscal') ?? '',
            precios_con_iva: true,
            alicuota_general: Number(datos.get('alicuota_general') ?? 16),
            // Lo que esta pantalla no toca, se conserva: re-guardar los datos
            // del negocio no puede apagar ni encender el IGTF.
            ...(previa?.emisor.igtf_activo != null ? { igtf_activo: previa.emisor.igtf_activo } : {}),
          },
          serie: datos.get('serie') ?? 'A',
          sucursal: previa?.sucursal ?? '1',
          equipo: datos.get('equipo') ?? 'FISCAL-01',
          imprenta: previa?.imprenta ?? { adaptador: 'practica', nombre: 'Practica' },
          respaldo: previa?.respaldo ?? null,
          // Si el negocio estaba en el respaldo, re-guardar sus datos NO lo
          // devuelve a la principal en silencio: ese cambio es siempre un acto
          // deliberado con su propio boton y su anotacion.
          usando: previa?.usando ?? 'imprenta',
          // El enlace con la nube, la referencia de hora y el modelo de factura
          // tampoco son de esta pantalla: se conservan tal cual.
          nube: previa?.nube ?? null,
          ...(previa?.hora_url ? { hora_url: previa.hora_url } : {}),
          modelo_factura: previa?.modelo_factura ?? null,
        };
        try {
          guardarConfig(op.base, nueva);
          estacion = null;
          const est = await abrirEstacion();
          est?.anotarEvento(NOMBRE_ROL[rol as Rol], 'configuracion_guardada', {
            rif: nueva.emisor.rif,
            razon_social: nueva.emisor.razon_social,
          });
          return redirigir(res, '/');
        } catch (e) {
          return html(res, `<h1>No se pudo guardar</h1><p class="mal">${escapar((e as Error).message)}</p><a href="/configurar"><button>Volver</button></a>`, 400);
        }
      }

      // ===== Claves de los demas (solo el dueño) =====
      if (ruta === '/claves' && metodo === 'GET') {
        if (cortado('claves')) return;
        const cuando = (r: Rol): string => {
          const c = acceso.cambiadaEn(r);
          return c ? `puesta el ${new Date(c).toLocaleDateString('es-VE')}` : 'todavia sin poner';
        };
        return html(res, `${cabecera(rol as Rol, 'Claves')}
          <form method="post" action="/claves">
          <input type="hidden" name="que" value="cajero">
          <div class="tarjeta">
            <b>Clave del cajero</b> <span class="pie">(${cuando('cajero')})</span>
            <p class="pie" style="margin:6px 0">Consulta el estado de la estacion y reintenta las transmisiones pendientes. No configura ni anula documentos.</p>
            <label>Clave nueva</label><input name="clave" type="password" required>
          </div>
          <button type="submit">Guardar la del cajero</button>
          </form>
          <form method="post" action="/claves">
          <input type="hidden" name="que" value="auditor">
          <div class="tarjeta">
            <b>Clave de consulta del SENIAT</b> <span class="pie">(${cuando('auditor')})</span>
            <p class="pie" style="margin:6px 0">La Providencia 000121 (articulo 3, numeral 8) obliga a entregarsela. Solo consulta y exporta, por /fiscal.</p>
            <label>Clave nueva</label><input name="clave" type="password" required>
          </div>
          <button type="submit">Guardar la del SENIAT</button>
          </form>
          <form method="post" action="/claves">
          <input type="hidden" name="que" value="dueno">
          <div class="tarjeta">
            <b>Tu clave</b> <span class="pie">(${cuando('dueno')})</span>
            <label>Clave nueva</label><input name="clave" type="password" required>
          </div>
          <button type="submit">Cambiar la mia</button>
          </form>
          <p class="pie">Al cambiar una clave se cierran las sesiones abiertas.</p>`);
      }

      if (ruta === '/claves' && metodo === 'POST') {
        if (cortado('claves')) return;
        const f = await leerFormulario(req);
        const que = f.get('que') ?? '';
        const est = await abrirEstacion();
        try {
          if (que === 'rotar_llave') {
            acceso.rotarLlaveMaquina();
            est?.anotarEvento(NOMBRE_ROL[rol as Rol], 'llave_maquina_rotada');
            return redirigir(res, '/configurar');
          }
          if (que !== 'cajero' && que !== 'auditor' && que !== 'dueno') {
            return html(res, `<h1>No se pudo</h1><p class="mal">No se cual clave es esa.</p><a href="/claves"><button>Volver</button></a>`, 400);
          }
          const clave = f.get('clave') ?? '';
          // Cada quien la suya. La del SENIAT sobre todo: la norma pide una
          // clave PARA EL SENIAT, y si es la misma del dueño, entregarsela es
          // entregarle la estacion completa.
          const otros: Rol[] = (['dueno', 'cajero', 'auditor'] as Rol[]).filter((r) => r !== que);
          const repetida = otros.find((r) => acceso.esLaMismaClaveDe(r, clave));
          if (repetida) {
            return html(res, `<h1>Esa clave ya esta en uso</h1>
              <p class="mal">Es la misma que tiene ${NOMBRE_ROL[repetida]}. Cada quien tiene que tener la suya: si no, darle una es darle la del otro.</p>
              <a href="/claves"><button>Volver</button></a>`, 400);
          }
          acceso.fijarClave(que, clave);
          est?.anotarEvento(NOMBRE_ROL[rol as Rol], 'clave_cambiada', { rol: que });
          log(`Se cambio la clave de ${NOMBRE_ROL[que]}.`);
          // Cambiarle la clave a alguien tumba las sesiones que ese alguien haya
          // dejado abiertas por ahi. Al que la cambio no se le tumba la suya,
          // salvo que la cambiada sea la propia: ahi se le abre una nueva.
          sesiones.cerrarRol(que);
          if (que !== rol) return redirigir(res, '/claves');
          return redirigir(res, '/claves', `${GALLETA}=${sesiones.abrir(rol as Rol)}; HttpOnly; SameSite=Strict; Path=/`);
        } catch (e) {
          return html(res, `<h1>No se pudo</h1><p class="mal">${escapar((e as Error).message)}</p><a href="/claves"><button>Volver</button></a>`, 400);
        }
      }

      // ===== El respaldo del libro, fuera del equipo (solo el dueño) =====
      if (ruta === '/respaldo' && metodo === 'GET') {
        if (cortado('configurar')) return;
        const est = await abrirEstacion();
        if (!est) return redirigir(res, '/');
        const r = est.estadoRespaldo();
        const vacio = est.documentos().length === 0;
        return html(res, `${cabecera(rol as Rol, 'Respaldo')}
          <div class="tarjeta">
            <div class="dato"><span>Documentos en este equipo</span><b>${r.aqui}</b></div>
            <div class="dato"><span>Guardados fuera del equipo</span><b class="${r.al_dia ? 'bien' : 'aviso'}">${r.afuera}</b></div>
            <div class="dato"><span>Ultima vez que salio</span><b>${r.subido_en ? new Date(r.subido_en).toLocaleString('es-VE') : 'nunca'}</b></div>
            ${r.problema ? `<p class="mal" style="margin:8px 0 0">${escapar(r.problema)}</p>` : ''}
            <form method="post" action="/respaldo"><input type="hidden" name="que" value="subir">
            <button>Enviar el respaldo ahora</button></form>
          </div>
          <h2>Tu frase de recuperacion</h2>
          <div class="tarjeta">
            <p class="pie" style="margin:0 0 8px"><b>Anotala fuera de este equipo</b> (en el telefono, en un papel en la caja fuerte, u otro lugar seguro). El respaldo sale CIFRADO con esta frase: ni la imprenta ni nosotros podemos abrirlo. Si se quema esta laptop y no tienes la frase, el respaldo no sirve para nada.</p>
            <code>${escapar(est.fraseDelRespaldo())}</code>
          </div>
          ${vacio ? `
          <h2>Restaurar el libro en un equipo nuevo</h2>
          <div class="tarjeta">
            <p class="pie" style="margin:0 0 8px">Para una instalacion nueva: descarga el libro resguardado en la imprenta y lo restaura en este equipo. Solo funciona con el libro vacio, que es el caso de esta estacion ahorita.</p>
            <form method="post" action="/respaldo">
              <input type="hidden" name="que" value="restaurar">
              <label>La frase de recuperacion que anotaste</label>
              <input name="frase" required>
              <button>Restaurar el libro</button>
            </form>
          </div>` : `
          <p class="pie">Para restaurar un libro hace falta una instalacion nueva y vacia: en esta ya hay ${r.aqui} documento(s), y un libro con documentos no se sobreescribe nunca.</p>`}`);
      }

      if (ruta === '/respaldo' && metodo === 'POST') {
        if (cortado('configurar')) return;
        const est = await abrirEstacion();
        if (!est) return redirigir(res, '/');
        const f = await leerFormulario(req);
        if (f.get('que') === 'restaurar') {
          try {
            const r = await est.traerRespaldoDeLaNube(f.get('frase') ?? '');
            estacion = null;
            await abrirEstacion();
            log(`Libro restaurado desde el respaldo: ${r.documentos} documento(s).`);
            return html(res, `<h1>Restauracion completada</h1><p class="bien">Se levantaron ${r.documentos} documento(s) del respaldo.</p>
              <a href="/"><button>Ver la estacion</button></a>`);
          } catch (e) {
            return html(res, `<h1>No se pudo traer el respaldo</h1><p class="mal">${escapar((e as Error).message)}</p>
              <a href="/respaldo"><button>Volver</button></a>`, 400);
          }
        }
        const r = await est.subirRespaldo();
        log(`Respaldo: ${r.subidas} linea(s) mandadas${r.problema ? `, ${r.problema}` : ''}`);
        return redirigir(res, '/respaldo');
      }

      if (ruta === '/api/respaldo' && metodo === 'POST') {
        if (cortado('ver', true)) return;
        const est = await abrirEstacion();
        if (!est) return json(res, { problema: 'La estacion no esta lista' }, 503);
        return json(res, await est.subirRespaldo());
      }

      // ===== Contingencia: cuando se cae todo (solo el dueño) =====
      if (ruta === '/contingencia' && metodo === 'GET') {
        if (cortado('configurar')) return;
        const est = await abrirEstacion();
        if (!est) return redirigir(res, '/');
        const cargadas = est.talonarioCargado();
        const hoy = new Date().toISOString().slice(0, 16);
        return html(res, `${cabecera(rol as Rol, 'Contingencia')}
          <p class="pie" style="margin:0 0 12px">Contingencia: que hacer cuando no hay electricidad, no hay internet o falla este equipo.</p>
          <div class="tarjeta">
            <p class="pie" style="margin:0 0 10px"><b>Que hacer cuando no hay electricidad, no hay internet o el equipo no enciende:</b></p>
            <ol class="pie" style="margin:0;padding-left:18px;line-height:1.9">
              <li>Se factura a mano en el talonario. Esa es la contingencia que establece la norma.</li>
              <li>Se guardan las copias en orden, sin saltarse numeros.</li>
              <li>Cuando todo vuelva, se cargan aqui abajo una por una.</li>
            </ol>
            <p class="pie" style="margin:10px 0 0">Si no se cargan, el reporte del mes queda incompleto justo en el periodo de la interrupcion, y esa diferencia es lo primero que revisa una fiscalizacion. Estas facturas NO se le mandan a la imprenta: el papel ya trae su numero impreso. Se anotan para que las cuentas cuadren con lo que de verdad se vendio.</p>
          </div>
          <h2>Cargar una factura del talonario</h2>
          <form method="post" action="/contingencia">
          <div class="tarjeta">
            <label>Numero impreso en el papel</label><input name="numero" placeholder="00001234" required>
            <label>Cuando se emitio</label><input name="emitido_en" type="datetime-local" value="${hoy}" required>
            <label>A nombre de quien</label><input name="cliente" value="Consumidor final" required>
            <label>RIF o cedula (si la pidio con datos)</label><input name="identificacion">
            <label>Total cobrado en dolares</label><input name="total_usd" type="number" step="0.01" min="0.01" required>
            <label>IVA</label><select name="alicuota">
              <option value="16">16%</option><option value="8">8%</option><option value="0">Exento</option>
            </select>
            <label>Tasa del dia (opcional)</label><input name="tasa_bs" type="number" step="0.01">
            <label>Nota (opcional)</label><input name="nota" placeholder="que se vendio, si hace falta">
          </div>
          <button type="submit">Cargar esta factura</button>
          </form>
          <h2>Ya cargadas (${cargadas.length})</h2>
          <div class="tarjeta"><table><tr><th>Numero</th><th>Cuando se emitio</th><th>Total USD</th></tr>
          ${cargadas.slice(0, 30).map((c) => `<tr><td>${escapar(c.numero)}</td><td>${new Date(c.emitido_en).toLocaleString('es-VE')}</td><td>${c.total_usd.toFixed(2)}</td></tr>`).join('') || '<tr><td colspan="3">Ninguna cargada</td></tr>'}
          </table></div>`);
      }

      if (ruta === '/contingencia' && metodo === 'POST') {
        if (cortado('configurar')) return;
        const est = await abrirEstacion();
        if (!est) return redirigir(res, '/');
        const f = await leerFormulario(req);
        const ident = (f.get('identificacion') ?? '').trim();
        const r = await est.cargarTalonario(
          {
            numero: f.get('numero') ?? '',
            // El campo del navegador viene sin zona horaria: se toma como hora
            // de este equipo, que es donde se emitio el papel.
            emitido_en: new Date(f.get('emitido_en') ?? '').toISOString(),
            cliente: {
              nombre: f.get('cliente') ?? '',
              rif: /^[JGVEP]/i.test(ident) ? ident : null,
              cedula: ident && !/^[JGVEP]/i.test(ident) ? ident : null,
            },
            total_usd: Number(f.get('total_usd') ?? 0),
            alicuota: Number(f.get('alicuota') ?? 16),
            tasa_bs: f.get('tasa_bs') ? Number(f.get('tasa_bs')) : null,
            nota: f.get('nota'),
          },
          NOMBRE_ROL[rol as Rol],
        );
        if (!r.sellada) {
          return html(res, `<h1>No se pudo cargar</h1><p class="mal">${escapar(r.problema)}</p>
            <a href="/contingencia"><button>Volver</button></a>`, 400);
        }
        log(`Cargada del talonario la factura ${String(f.get('numero'))}.`);
        return redirigir(res, '/contingencia');
      }

      // ===== Retenciones recibidas: el credito del periodo (Providencia 000054) =====
      if (ruta === '/retenciones' && metodo === 'GET') {
        if (cortado('retenciones')) return;
        const est = await abrirEstacion();
        if (!est) return redirigir(res, '/');
        const pedido = url.searchParams.get('periodo') ?? '';
        const periodo = /^\d{4}-\d{2}$/.test(pedido) ? pedido : new Date().toISOString().slice(0, 7);
        const del = est.retencionesRecibidas(periodo);
        const resumen = est.resumenRetenciones(periodo);
        const hoy = new Date().toISOString().slice(0, 10);
        return html(res, `${cabecera(rol as Rol, 'Retenciones')}
          <p class="pie" style="margin:0 0 12px">Comprobantes de retencion de IVA recibidos (Providencia SNAT/2025/000054). Cuando un agente de retencion paga una factura del negocio, retiene el 75% o el 100% del IVA y entrega un comprobante. Ese comprobante es credito contra la cuota del periodo en la declaracion. Este sistema no lo emite (lo emite el agente): aqui se registra para que el periodo cuadre y el credito no se pierda.</p>
          <div class="kpis" style="grid-template-columns:repeat(3,1fr)">
            <div class="kpi"><div class="t">Periodo</div><div class="n" style="font-size:30px">${escapar(resumen.periodo)}</div><div class="d">al que aporta el credito</div></div>
            <div class="kpi"><div class="t">Comprobantes vigentes</div><div class="n">${resumen.comprobantes}</div><div class="d">registrados y sin anular</div></div>
            <div class="kpi"><div class="t">IVA retenido</div><div class="n" style="font-size:${Math.min(30, tamanoCifra(resumen.iva_retenido_bs.toFixed(2)))}px">${resumen.iva_retenido_bs.toFixed(2)}</div><div class="d">Bs, credito del periodo</div></div>
          </div>
          <form method="get" action="/retenciones" class="barra" style="margin-bottom:14px;justify-content:flex-start">
            <label style="margin:0">Ver otro periodo</label>
            <input type="month" name="periodo" value="${escapar(periodo)}" style="max-width:190px">
            <button type="submit" class="gris" style="margin:0">Ver</button>
            <span style="flex:1"></span>
            <a href="/retenciones.csv?periodo=${escapar(periodo)}"><button type="button" class="gris" style="margin:0">Bajar el archivo del periodo</button></a>
          </form>
          <div class="dos">
            <div>
              <h2 style="margin-top:0">Comprobantes del periodo (${del.length})</h2>
              <div class="tarjeta"><table>
                <tr><th>Numero</th><th>Fecha</th><th>Agente de retencion</th><th>Facturas</th><th class="num">%</th><th class="num">IVA retenido Bs</th></tr>
                ${del.map((r) => `<tr><td class="doc">${escapar(r.numero)}</td><td>${escapar(r.fecha)}</td><td>${escapar(r.agente.razon_social)}<span class="pie" style="display:block;margin:0">${escapar(r.agente.rif)}</span></td><td>${escapar(r.facturas)}</td><td class="num">${r.porcentaje}</td><td class="num">${r.iva_retenido_bs.toFixed(2)}</td></tr>`).join('') || '<tr><td colspan="6">Ninguno registrado en este periodo</td></tr>'}
              </table></div>
              <h2>Anular un comprobante mal cargado</h2>
              <div class="tarjeta">
                <p class="pie" style="margin:0 0 6px">El registro original no se borra: queda con la contramarca de quien lo anulo y por que.</p>
                <form method="post" action="/retenciones/anular">
                  <label>Numero del comprobante</label><input name="numero" required>
                  <label>Motivo de la anulacion</label><input name="motivo" required>
                  <button type="submit">Anular con contramarca</button>
                </form>
              </div>
            </div>
            <div>
              <h2 style="margin-top:0">Registrar un comprobante</h2>
              <form method="post" action="/retenciones">
              <div class="tarjeta" style="margin:0">
                <label>Numero del comprobante, tal como lo entrego el agente</label><input name="numero" required>
                <label>Fecha del comprobante</label><input name="fecha" type="date" value="${hoy}" required>
                <label>RIF del agente de retencion</label><input name="agente_rif" placeholder="J-12345678-9" required>
                <label>Razon social del agente</label><input name="agente_razon_social" required>
                <label>Factura o facturas del negocio a las que corresponde</label><input name="facturas" placeholder="A-00000012" required>
                <label>Porcentaje retenido</label><select name="porcentaje"><option value="75">75%</option><option value="100">100%</option></select>
                <label>IVA retenido, en bolivares</label><input name="iva_retenido_bs" type="number" step="0.01" min="0.01" required>
                <label>Nota (opcional)</label><input name="nota">
                <button type="submit">Registrar el comprobante</button>
              </div>
              </form>
            </div>
          </div>`);
      }

      if (ruta === '/retenciones' && metodo === 'POST') {
        if (cortado('retenciones')) return;
        const est = await abrirEstacion();
        if (!est) return redirigir(res, '/');
        const f = await leerFormulario(req);
        try {
          const r = est.registrarRetencion(
            {
              numero: f.get('numero') ?? '',
              fecha: f.get('fecha') ?? '',
              agente_rif: f.get('agente_rif') ?? '',
              agente_razon_social: f.get('agente_razon_social') ?? '',
              facturas: f.get('facturas') ?? '',
              iva_retenido_bs: Number(f.get('iva_retenido_bs') ?? 0),
              porcentaje: Number(f.get('porcentaje') ?? 0),
              nota: f.get('nota'),
            },
            NOMBRE_ROL[rol as Rol],
          );
          log(`Registrado el comprobante de retencion ${r.numero} (${r.iva_retenido_bs.toFixed(2)} Bs).`);
          return redirigir(res, `/retenciones?periodo=${r.periodo}`);
        } catch (e) {
          return html(res, `<h1>No se pudo registrar</h1><p class="mal">${escapar((e as Error).message)}.</p>
            <a href="/retenciones"><button>Volver</button></a>`, 400);
        }
      }

      if (ruta === '/retenciones/anular' && metodo === 'POST') {
        if (cortado('retenciones')) return;
        const est = await abrirEstacion();
        if (!est) return redirigir(res, '/');
        const f = await leerFormulario(req);
        try {
          est.anularRetencion(f.get('numero') ?? '', f.get('motivo') ?? '', NOMBRE_ROL[rol as Rol]);
          log(`Anulado con contramarca el comprobante de retencion ${String(f.get('numero'))}.`);
          return redirigir(res, '/retenciones');
        } catch (e) {
          return html(res, `<h1>No se pudo anular</h1><p class="mal">${escapar((e as Error).message)}.</p>
            <a href="/retenciones"><button>Volver</button></a>`, 400);
        }
      }

      // El dueño se lleva las retenciones del periodo para su contador, con su
      // propia sesion: mismo criterio que el archivo de documentos (exportar).
      if (ruta === '/retenciones.csv' && metodo === 'GET') {
        if (cortado('exportar')) return;
        const est = await abrirEstacion();
        if (!est) return redirigir(res, '/');
        const pedido = url.searchParams.get('periodo') ?? '';
        const periodo = /^\d{4}-\d{2}$/.test(pedido) ? pedido : new Date().toISOString().slice(0, 7);
        const filas = est.retencionesRecibidas(periodo);
        const lineas = ['numero;fecha;periodo;agente_rif;agente_razon_social;facturas;porcentaje;iva_retenido_bs;registrado_en;registrado_por'];
        for (const r of filas) {
          lineas.push([r.numero, r.fecha, r.periodo, r.agente.rif, r.agente.razon_social, r.facturas, r.porcentaje, r.iva_retenido_bs, r.registrado_en, r.quien].map(celdaCsv).join(';'));
        }
        res.writeHead(200, {
          'content-type': 'text/csv; charset=utf-8',
          'content-disposition': `attachment; filename="retenciones-${periodo}.csv"`,
          'cache-control': 'no-store',
        });
        return res.end(lineas.join('\n') + '\n');
      }

      // ===== Movimientos: quien hizo que (solo el dueño) =====
      if (ruta === '/movimientos' && metodo === 'GET') {
        if (cortado('bitacora')) return;
        const est = await abrirEstacion();
        const eventos = est ? est.movimientos(80) : [];
        return html(res, `${cabecera(rol as Rol, 'Movimientos')}
          <p class="pie" style="margin-top:2px">Cada linea esta encadenada con la anterior: si alguien edita el archivo, se nota.</p>
          <div class="tarjeta"><table><tr><th>Cuando</th><th>Quien</th><th>Que</th><th>Detalle</th></tr>
          ${eventos.reverse().map((v) => `<tr><td>${new Date(v.en).toLocaleString('es-VE')}</td><td>${escapar(v.quien)}</td><td>${escapar(v.accion)}</td><td class="pie" style="margin:0">${escapar(JSON.stringify(v.detalle ?? {}))}</td></tr>`).join('') || '<tr><td colspan="4">Todavia no hay movimientos</td></tr>'}
          </table></div>`);
      }

      // ===== API del dia a dia =====
      if (ruta === '/api/anular' && metodo === 'POST') {
        if (cortado('anular')) return;
        const est = await abrirEstacion();
        if (!est) return json(res, { sellada: false, problema: 'La estacion no esta lista' }, 503);
        const c = await leerCuerpo(req);
        // Quien anula queda escrito. Ese era el hueco: se podia anular sin clave
        // y sin dejar rastro de quien fue.
        return json(res, await est.anular(Number(c.numero), String(c.motivo ?? ''), NOMBRE_ROL[rol as Rol]));
      }

      if (ruta === '/api/estado' && metodo === 'GET') {
        if (cortado('ver', true)) return;
        const est = await abrirEstacion();
        const base = est ? est.estado() : { listo: false, falta: revisar(leerConfig(op.base)) };
        return json(res, { ...base, version: avisador.estado() });
      }

      // Revisar la hora: cualquiera de los dos que este atendiendo, y tambien
      // los programas del negocio (asi el Cerebrito puede pedirla al arrancar).
      if (ruta === '/reloj' && metodo === 'POST') {
        if (cortado('ver')) return;
        const est = await abrirEstacion();
        if (est) {
          const r = await est.revisarReloj(NOMBRE_ROL[rol as Rol]);
          log(`Revisada la hora del equipo: ${r.texto}`);
        }
        return redirigir(res, '/');
      }

      if (ruta === '/api/reloj' && metodo === 'POST') {
        if (cortado('ver', true)) return;
        const est = await abrirEstacion();
        if (!est) return json(res, { problema: 'La estacion no esta lista' }, 503);
        return json(res, await est.revisarReloj(rol ? NOMBRE_ROL[rol] : 'programa'));
      }

      if (ruta === '/api/reintentar' && metodo === 'POST') {
        if (cortado('reintentar', true)) return;
        const est = await abrirEstacion();
        if (!est) return json(res, { problema: 'La estacion no esta lista' }, 503);
        return json(res, await est.reintentar());
      }

      json(res, { problema: 'No existe esa direccion' }, 404);
    } catch (e) {
      log(`Error atendiendo ${ruta}: ${(e as Error).message}`);
      json(res, { problema: (e as Error).message }, 500);
    }
  });

  await new Promise<void>((listo) => servidor.listen(puerto, '127.0.0.1', listo));
  log(`Estacion Fiscal escuchando en http://127.0.0.1:${puerto}`);
  if (!acceso.yaTienePortero()) log('Atencion: la estacion todavia no tiene clave. Abre la direccion de arriba y ponle la del dueño.');
  return {
    // Cerrar es cerrar TODO lo que este servidor arranco: el temporizador del
    // avisador (12 h) y las conexiones keep-alive abiertas tambien mantienen
    // vivo el proceso, no solo el puerto.
    cerrar: () => {
      avisador.detener();
      servidor.closeAllConnections();
      servidor.close();
    },
    puerto,
  };
}
