← Tablero

Manual del tablero

qué mide cada cosa y cómo se arregla

Cómo funciona esta web

El tablero es una página estática. No tiene backend, no tiene login y no consulta nada en vivo: lee un solo archivo, data.json, que alguien más dejó escrito. Todo lo interesante pasa antes de que abras el navegador.

1

El colector

~/panel-data.py, disparado por un timer de systemd cada 2 minutos. Es el único que trabaja de verdad.

2

Las cuatro fuentes

  • git — recorre cada repo de /var/www/kxklod/: commits, rama, últimos 8 mensajes, archivos sin commitear.
  • Zabbix — API del server (host kostik-dev, que es esta misma VM): CPU, RAM, disco, steal, uptime y si nginx / ssh / zabbix / los snapshots están vivos.
  • Cupo de Claude — cuánto queda de la ventana de 5 h y de la semanal.
  • Cloudflare DNS — para saber si el sitio en prod lo sirve Pages, la VM o un hosting ajeno.
3

El archivo

Todo eso se escribe junto en /var/www/kxklod/panel/data.json, de forma atómica (se escribe un .tmp y se renombra: nunca vas a leer medio archivo).

4

La página

app.js pide data.json al abrir y vuelve a pedirlo cada 60 s. Dibuja las señales, el tablero de repos y los badges. Nada más.

Las dos cosas que no salen de data.json

Los badges de test
Los escribe ~/pages-deploy/run-tests.sh <sitio>, que corre la auditoría (~/validar.sh) y Lighthouse, y guarda el resultado en test-results.json. No corre solo: se dispara después de un deploy. Por eso los badges dicen “última corrida” y guardan el commit contra el que se midieron — pueden estar atrasados respecto de la punta del repo.
Los snapshots
Cada hash del historial es un link. La primera vez que se pide, un servidorcito extrae ese commit con git archive y te muestra el sitio como estaba en ese momento. Lo único del panel que no es un archivo estático.

El panel es público y sin auth. Cualquiera que sepa la URL ve los mensajes de commit y puede navegar los snapshots de cualquier commit. Nada de secretos en los mensajes de commit (guía §8.1).

El pulso y el reloj

La barra fina que late abajo del encabezado es el latido del dato, no un adorno: si el colector se muere, el dato envejece y el encabezado cambia de estado — fresco hasta 5 minutos, sospechoso hasta 15, viejo pasado eso. El reloj es tu reloj local, sirve para comparar de un vistazo.

Las tres señales

Están copiadas de las señales de vía del ferrocarril: cada una muestra el aspecto escrito además de encendido, así el estado nunca depende solo del color. Se clickean para ver el detalle.

Claude

Cuánto cupo queda. Mira el peor de los dos: ventana de 5 h y semanal.

  • Vía libre — 50% o más libre
  • Precaución — menos del 50%
  • Alto — menos del 25%

Al abrirla también ves cuántas sesiones corren y cuántas más entran con la RAM libre.

Ojo: las sesiones y la RAM son de toda la VM, no de este usuario: si kostik tiene sesiones abiertas, se cuentan acá. El cupo de 5 h y el semanal también son de la cuenta, compartidos con kostik.

Máquina

Salud de la VM, vía Zabbix.

  • Alto — un servicio caído, disco >85%, RAM libre <800 MB o steal ≥25%
  • Precaución — disco >75%, steal ≥18% o CPU ≥90%
  • Vía libre — todo lo demás

Steal = el porcentaje de CPU que el host le roba a la VM. Esta máquina idlea entre 6 y 14% (vCPU compartida): eso es lo normal, recién arriba de 18 hay algo que mirar.

Actividad

Si el taller se está moviendo.

  • Alto — hace más de 10 días que no se commitea nada
  • Precaución — hay repos sin commitear, o el último commit tiene más de 3 días
  • Vía libre — todo lo demás

Adentro hay barras por repo y un selector hora / día / semana / mes. Si no la tocás, la ventana baja sola hasta la primera que tenga commits.

Despacho de repos

Una fila por repo, ordenadas por frescura. La franja de color de la izquierda es el calor: ámbar pleno = commiteado hoy, apagándose hasta gris a los 21 días.

al día / N sin commitear
Archivos modificados que todavía no están en git. Es lo que más rápido se olvida.
Pages VM Externo
Quién sirve la producción de ese sitio: Cloudflare Pages, el nginx de esta VM, o un hosting ajeno. Se deduce del DNS real, no de una lista escrita a mano.
prod ↗
Abre el sitio en producción. Ojo: esta VM es DEV. Lo que ves acá no es lo que ve el mundo.
El triangulito
Despliega los últimos 8 commits. Cada hash abre el sitio congelado en ese commit.

Los ítems de la auditoría

El badge Guía ✓ de cada fila resume la corrida de ~/validar.sh: la checklist de la guía de sitios. ✗ es una falla (algo está mal y hay que arreglarlo), ⚠ es un aviso (se puede vivir con eso, pero cuesta plata en bytes o en posiciones). Acá está cada ítem que puede aparecer, aunque hoy esté en verde.

✗ falla

HTML falta: lang, title, viewport, meta-description, favicon, og-tags

Revisa el <head> del index.html y lista lo que no encontró.

Por qué importa: sin viewport el sitio no es usable en celular; sin description y og-tags, Google y cualquier red social inventan el resumen y la miniatura; sin lang los lectores de pantalla leen con el acento equivocado.

Cómo se arregla: copiar el <head> estándar de la guía (§3) desde un sitio que ya esté en verde y adaptar los textos.

✗ malformado
⚠ ausente

robots.txt

Que exista y que su primera línea sea una directiva de verdad (User-agent, Allow, Disallow, Sitemap o un comentario).

Por qué importa: es la trampa clásica de Cloudflare Pages. Si el archivo no existe, Pages no devuelve 404: sirve el index.html como fallback. El crawler recibe una página HTML donde esperaba reglas, la marca como malformada, y Lighthouse te baja SEO. Además es donde vive la política de bots de IA (guía §13).

Cómo se arregla: poner un robots.txt real en la raíz del sitio, con su Sitemap: apuntando al dominio de producción.

✗ falla

HTTP 200 del índice

Pide http://127.0.0.1/<sitio>/ y espera un 200.

Por qué importa: es el chequeo más tonto y el que más veces salvó el día. Si esto falla, el sitio está roto en la VM — permisos, nginx, un index.html que no está donde debería.

Cómo se arregla: mirar el error de nginx antes de tocar cualquier otra cosa.

⚠ aviso

<img> sin alt

Cuenta las imágenes del index.html sin atributo alt.

Por qué importa: quien usa lector de pantalla no ve nada ahí. Google tampoco. Es la mitad de la nota de accesibilidad de Lighthouse.

Cómo se arregla: alt que describa la imagen. Si es puro adorno, alt="" (vacío a propósito) también cuenta como resuelto.

⚠ aviso

Imágenes de más de 200 KB

Cualquier imagen publicada que pase el umbral de 200 KB. Muestra las 5 más pesadas.

Por qué importa: es lo primero que se come el tiempo de carga en celular. El puntaje de Velocidad casi siempre se explica acá.

Cómo se arregla: pasarlas a AVIF y reducirlas al tamaño en que se muestran (ver el pipeline de assets, §5). El original grande se guarda afuera de la carpeta publicada.

⚠ aviso

solo-avif: imágenes servidas que no son AVIF

De las imágenes realmente referenciadas por el sitio, cuántas siguen siendo jpg / png / gif.

Por qué importa: AVIF ahorra entre 50 y 70% sobre el mismo jpg/png, con la misma pinta. Es el ahorro más grande y más barato que hay.

Cómo se arregla: convertir y actualizar la referencia. Ojo con la diferencia: este ítem mira lo que se sirve; el de arriba mira lo que pesa.

⚠ aviso

Imágenes con más resolución de la que se muestran

Compara los píxeles reales del archivo contra el ancho declarado en el <img>. Avisa si el archivo tiene más de 2,2× lo necesario.

Por qué importa: estás mandando por la red píxeles que el navegador tira a la basura. Un 2× está bien (pantallas retina); 4× es puro desperdicio.

Cómo se arregla: reducir el archivo al doble del ancho declarado y guardar el original afuera de la carpeta publicada.

⚠ aviso

CSS o JS de más de 100 KB sin minificar

Archivos .css / .js que pasan los 100 KB.

Por qué importa: el JS bloquea el dibujado de la página mientras se descarga y se parsea, así que pesa más de lo que dice la balanza.

Cómo se arregla: minificar. Con una excepción honesta: los catálogos (films_*.js, cartoons_*.js) son datos, no código — ahí la respuesta no es minificar sino partirlos o servirlos como JSON. Está anotado como pendiente, no como olvido.

⚠ aviso

Assets sin uso (peso muerto)

Archivos que están en la carpeta publicada pero que nadie referencia desde el HTML, el CSS ni el JS. Excluye translations/ y los .json, que se piden dinámicamente.

Por qué importa: no le hacen más lenta la página al visitante (nunca los descarga), pero inflan el deploy, el repo y el backup. En kino y arjlover son decenas de miles de archivos y cientos de megas: son las capturas del catálogo espejado, y ahí el número es esperable.

Cómo se arregla: borrar si es basura de verdad. Si es material del catálogo, dejarlo y saber que ese ⚠ es ruido conocido, no una tarea.

⚠ aviso

HTTP/3 en el edge

Pregunta por HTTP/3 al dominio propio del sitio. Si el sitio no tiene subdominio (vive bajo el hub), el ítem no aplica y no cuenta.

Por qué importa: HTTP/3 arranca la conexión en menos vueltas; se nota en móvil y en redes malas. En Cloudflare es un interruptor, no un trabajo.

Cómo se arregla: activarlo en el panel de Cloudflare de esa zona.

informativo

Peso publicado y mezcla de formatos

El total en KB de la carpeta (sin _antes/ ni .git/) y cuántos jpg/png hay contra cuántos avif/webp.

Por qué importa: no genera ni ✗ ni ⚠ — es el contexto para leer todo lo demás. Un sitio de 900 MB con 40 avisos y uno de 2 MB con 40 avisos no son el mismo problema.

La guía, sección por sección

La fuente de verdad es ~/GUIA_SITIOS.md. Esto es el índice comentado: qué dice cada §, para saber a cuál ir. Los ítems de la auditoría de arriba son solo la parte que una máquina puede chequear sola — la mayoría de la guía no la mide ningún validador.

§1 Dónde vive todo

Un sitio = una carpeta en /var/www/kxklod/<sitio>/, que nginx sirve al instante y sin caché en kxklod.loadsavedelete.com/<sitio>/. Eso es dev. El subdominio propio lo cablea Nikolai en Cloudflare. El hub linkea a cada sitio (§7).

§2 Estructura de la carpeta

Siempre lo mismo: index.html de una sola página con secciones ancladas, style.css y script.js linkeados con ?v=N para bustear caché, video/, og.jpg de 1200×630, logo en avif + png, y su .gitignore.

§3 El <head> estándar

Se copia de un sitio que ya esté bien y se adapta. En orden: charset y viewport; title y description reales, no placeholder; favicon SVG inline; Open Graph completo; Twitter card summary_large_image; las fuentes auto-hosteadas — nunca Google Fonts, su CSS bloquea el render 2–3 s en mobile; y el style.css?v=N.

§4 Diseño: propio por sitio, toolkit compartido

El look es único de cada sitio (paleta de 4–6 colores, display + body + mono, un elemento firma memorable; inspiración en httpster.net antes de definir nada). Lo que se reutiliza es el script.js, siempre vanilla y sin build: initReveal(), el modal de video, la nav sticky, el i18n por localStorage. Piso de accesibilidad innegociable: prefers-reduced-motion, :focus-visible, alt, HTML semántico.

La trampa grande: las secciones data-driven (renderizadas desde un array en el JS) son invisibles para los crawlers de IA, que no ejecutan JavaScript. Ya no se acepta sin más: se mitiga con el resumen estático de §13.

§4.1 — Temas (lo que ves acá arriba, el chip “lente”): un sitio con varios temas no se hace agregando temas, se hace escribiendo bien el CSS la primera vez. Las variables van por rol: --ink es la superficie y --paper la tinta — nunca “oscuro” y “claro” —, y así el tema claro se da vuelta sin tocar una sola regla. En el CSS no se escribe un color: se pide un rol, y un color crudo suelto (sobre todo en el JS) se queda con el tema viejo puesto. Cada paleta se valida a WCAG (~/panel-contraste.py, que las lee del propio style.css): corrió sobre el tema que ya estaba en prod y lo reprobó. Lo que no se tematiza es el estado: los tres aspectos de señal siguen siendo distinguibles en las 6 lentes y con la palabra al lado — un monocromo lindo que te esconda un servicio caído no sirve para nada.

Principio “moderno primero”: solo tecnología moderna; la compatibilidad con lo viejo se sacrifica cuando cuesta performance — y al decidir, se estima cuánto cuesta (KB, ms, PSI) en vez de suponerlo. El peso en disco no es la restricción; las métricas y el SEO sí.

§5 Pipeline de assets

La sección más larga, y donde se gana la velocidad.

  • Imágenes → AVIF, siempre. avifenc -q 55-72 --speed 5. Nada de jpg/png sueltos; la única excepción es og.jpg, porque algunos scrapers viejos de redes no leen avif.
  • Resolución = lo que se muestra (tope ~2× para retina). El original grande se guarda fuera del webroot. Píxeles que no se ven son peso regalado.
  • Video self-hosted, nunca YouTube/Vimeo — por marca (te pueden dar de baja la cuenta), por performance y porque si un país bloquea YT los embeds se rompen. AV1 primero + H.264 de fallback, con codecs declarado en ambos <source> o el fallback no dispara. Audio AAC, nunca Opus (Safari no lo lee en MP4 y se quedaría mudo).
  • Encodear no es publicar: no hay AV1 hasta que el <source> está cableado. Y el que se sale del modal de video del toolkit se sale del AV1: los sitios que lo sirven son exactamente los que usan el modal; el que se arma un player propio queda afuera del patrón y del test-av1.js a la vez (mccat, 2026-07-13). Un .av1.mp4 que pesa casi lo mismo que el H.264 no es un encode fallado: el CRF adaptativo gasta el ahorro en calidad hasta llenar el budget de 25 MiB.
  • Sprite sheets AVIF para grillas de más de ~50 celdas: 100 thumbs = 1 archivo = 1 request. Es lo que hace entrar un catálogo de 15k títulos en el límite de 20.000 archivos de Pages. Se particiona por cómo se ve, no por qué es: la pregunta correcta no es cuántas imágenes hay, sino cuáles se descargan siempre juntas.
  • Las capturas del origen no vienen todas del mismo tamaño (filmiki: 264 resoluciones distintas). Un resize ciego alarga las caras hasta un 100%: hay que encajar y rellenar, nunca estirar. Medí las dimensiones reales del lote antes de encodear.

§6 QA antes de dar algo por hecho

El orden del control: curl por el 200 → ~/sanity.sh (todas las refs internas responden) → ~/validar.sh (los ítems de arriba) → Playwright → Lighthouse mobile, apuntando a 90+ en las cuatro categorías.

El chromium del sistema está roto (crashea con SIGTRAP en headless): se usa siempre el chromium pineado de Playwright, en ~/browser-tools/. El gate previo al deploy es preflight-sitio.js <url> — cero 404, favicon, robots, description, title, viewport, lang, sin errores JS — y se corre dos veces: contra dev antes de subir, y contra el *.pages.dev antes del switch de DNS.

404.html es obligatorio, y es el caso general de lo del robots.txt: sin él Pages contesta 200 con el index entero a cualquier URL inexistente. Eso genera soft-404 y, peor, envenena el caché: si la URL cae bajo una regla immutable, el edge cachea la página como si fuera el archivo, por un año. Le pasó a filmiki: tres hojas de sprites devolvían text/html y esas fichas quedaban vacías en prod, idénticas en dev. Y al verificarlo, ojo: sondear una URL inexistente la envenena — para probar el 404 usá siempre una ruta nueva.

§7 Integración con el hub

Agregar la fila del sitio en /var/www/kxklod/index.html siguiendo el patrón split-flap existente, y actualizar el contador del pie.

§8 git — y §8.1: los commits son PÚBLICOS

Un repo por sitio, rama main. nginx bloquea /.git/, así que el repo no se puede clonar por HTTP.

§8.1 es la regla que más caro sale olvidar. Este panel no tiene autenticación y su DNS no pasa por el proxy de Cloudflare: cualquiera que abra la URL ve los mensajes de commit, el autor, la rama, y puede navegar el árbol completo de archivos de cualquier commit de la historia vía snapshots. El noindex pide no indexar, no impide leer. Consecuencias: nada de secretos en los mensajes ni en los archivos —y no alcanza con borrarlos después, porque el snapshot sirve cualquier commit viejo: si pasó, hay que rotar la credencial. Los secretos viven fuera de /var/www, en ~/.cloudflare.env y ~/.zabbix.env con permisos 600.

§9 Trampas de esta VM

  • /tmp es un tmpfs chico (~987 MB): descargas y transcodes grandes van bajo /home o /var. Si se llena, fallan hasta los echo.
  • Lo que se sube desde la app del celular aterriza en /tmp, no en la carpeta del sitio.
  • No hay pip: python por python3 -m o binarios en ~/.local/bin. node y npm sí están.

§10 Visitas y topología de red

El TLS no lo termina esta VM: hay un nginx reverse proxy en el router (192.168.3.1) que termina HTTPS y nos manda HTTP plano con X-Forwarded-For. Por eso existe realip.conf: sin él el access.log registraría al proxy en vez del visitante. Las visitas se leen con ~/visitas.sh (“visita” = IP única por día; el grueso del tráfico crudo son bots, nunca contar líneas del log a lo bruto). Desde la LAN el dominio público no responde: no hay NAT loopback.

§11 Cloudflare Pages: deploy, caché, dominios

Dev es esta VM; prod es Pages, bajo el dominio propio del sitio. Nada se toca directo en prod: se edita en dev, se commitea, y cuando gusta se puja. El deploy siempre sale de git archive HEADcommit = deploy.

La caché es política fija, no se sube y baja el TTL: el HTML siempre revalida y las carpetas de assets van immutable por un año, con ?v= para bustear. La trampa verificada: Pages concatena los headers de todas las reglas que matchean en vez de pisarlos, así que en la regla específica hay que des-setear primero con ! Cache-Control.

§12 Restricción por país (Argentina)

Hoy está en simulacro: nginx registra quién caería, pero no bloquea a nadie. Motivo del plan: el origen está expuesto (el dominio no pasa por el proxy de Cloudflare) y hay bots crawleándolo.

La traba para activarlo: el ~95% del tráfico entra por el proxy 172.16.0.1, que pisa el X-Forwarded-For con su propia IP privada — la IP real del visitante se pierde y no se puede geo-filtrar. Antes de encender el bloqueo hay que arreglar eso, o decidir qué pasa con ese tráfico.

§13 SEO avanzado + IA-friendly

El principio que ordena todo lo demás: estos sitios promocionan productos y trabajo humano. El diseño lindo se amerita por el esfuerzo que hay detrás, pero la clave es ser encontrable — por buscadores clásicos y por asistentes de IA, que hoy son vía principal de descubrimiento: si no estás en esa respuesta, no existís. Doble audiencia sobre el mismo HTML: lindo para el humano, estructurado para la máquina que lo trae.

Regla de oro: los crawlers de IA no ejecutan JavaScript. Todo lo que solo existe en script.js es invisible para ellos. De ahí sale el resto:

  • 1. Contenido crítico en HTML estático. Los datos pueden seguir en el JS, pero el HTML debe traer al menos un resumen indexable. El patrón de referencia es aurora: sus 62 puestos se pre-renderizan en HTML y el JS los re-renderiza localizados, sin cambio visual.
  • 2. JSON-LD (schema.org) en el <head>, con el tipo que corresponda. Estándar consolidado: es lo que buscadores e IAs consumen directo.
  • 3. robots.txt con política explícita de bots de IA. La política es permitir: queremos que las IAs nos conozcan y nos citen.
  • 4. llms.txt — honestidad: es una propuesta, no un estándar, y ningún crawler grande confirmó consumirlo. Se hace porque cuesta un archivo. Las piezas probadas son la 1, 2, 3 y 7.
  • 5. canonical al dominio de prod, para que dev no compita con prod en el índice.
  • 6. sitemap.xml solo donde aporta (catálogos multi-vista).
  • 7. Feed Atom para novedades fechadas: es la versión push-friendly del llms.txt, y es estándar desde 2005.

La trampa de robots.txt que nos mordió (RFC 9309): un grupo con User-agent nombrado reemplaza al grupo *, no lo hereda. Si le escribís User-agent: GPTBot + Allow: /, GPTBot ignora todos los Disallow que pusiste en *. Bien usado es una herramienta: en kino y arjlover el catálogo está cerrado para todos y abierto para los bots de IA, que es justo el material que queremos que citen.

Estado: §13 completa y verificada en prod en los 16 sitios (2026-07-11). argentinafilma es el sitio modelo — el único con el set completo, incluido el feed Atom: copiar de ahí. Los 3 catálogos ya son indexables (2026-07-13): filmiki, arjlover y kino tienen su catálogo estático por letra + sitemap.xml. Un bot pasó de ver 44 palabras a 173.921 en kino y 71.308 en arjlover. Lo que enseñó: el display:flex en las filas cuesta 620 ms de layout (perf 88) — sin flex y con content-visibility, kino sirve 659 filas con perf 100; y el rojo de marca de los dos sitios no pasa contraste como texto (hay que usar la variante oscura). Y no alcanza con generarlo: hay que linkearlo desde el pie (si el bot ve algo que el visitante no ve, es cloaking) y corregir el llms.txt, que decía que la lista completa no estaba en HTML.

Checklist de un sitio nuevo

  • Carpeta con index / style / script, y <head> completo (§3) con canonical.
  • Diseño propio + toolkit (§4). Assets en avif; resolución ≈ la de render; originales fuera del webroot (§5).
  • og.jpg 1200×630.
  • SEO/IA (§13): HTML estático, JSON-LD, robots.txt con política de bots de IA, llms.txt.
  • sanity.sh ✓ y validar.sh ✓ · PageSpeed mobile 90+ en las 4 categorías.
  • Gate en el hub + contador actualizado (§7) · git init + commit + entrada en la bitácora.

A Apéndice — aprendizajes MEDIDOS

Regla meta: medí, no supongas. Nada de esto era predecible por teoría; todo cambió al probarlo.

  • AVIF comprime por calidad, no por píxeles. Bajar un thumb a 0,56× los píxeles bajó solo 9% los bytes. Para bajar peso de verdad hay que tocar el cuantizador.
  • Ítems por sheet = múltiplo de thumbs por ítem, y sheets chicos (30 celdas) para que el scroll baje solo lo visible.
  • El ?v= debe incluir lo que cambia los píxeles (resolución, cuantizador), no solo la lista de archivos — si no, el browser sigue sirviendo el sheet viejo immutable.
  • El CLS suele ser algo que se corre al entrar contenido async (el footer saltando 0,14 al poblarse la grilla). Y se mide con throttling: sin throttling da 0 y te miente.
  • Render chunked para listas grandes: tandas de ~40 con setTimeout, nunca requestAnimationFrame (se throttlea en tabs no visibles y en headless). Medido en kino: Perf 66→78, TBT 680→310 ms.

Los cuatro badges de Lighthouse

Los mide Google sobre la copia de la VM, en modo mobile. Escala 0–100: verde ≥90, ámbar ≥50, rojo <50, gris = sin dato.

SEO

Títulos, metas, robots, enlaces rastreables. Es el más fácil de dejar en 100 y el que más rinde: si esto no está bien, el sitio no existe para nadie.

Vel — Velocidad

Qué tan rápido carga y responde. Es el único que suele estar en ámbar, y casi siempre por imágenes pesadas. En catálogos gigantes, un 46 no es un bug: es el precio de servir miles de fichas.

A11y — Accesibilidad

Lector de pantalla, teclado, contraste, etiquetas. Lo que más lo baja son los alt faltantes.

BP — Buenas prácticas

Seguridad, errores en consola, APIs obsoletas. Si baja, algo se rompió: mirá la consola del navegador.

Dudas frecuentes

¿Cada cuánto se miden los badges de test?
No se miden solos. Corren cuando se ejecuta run-tests.sh <sitio>, típicamente después de un deploy. El badge guarda contra qué commit se midió: si el repo siguió avanzando, el badge habla del pasado.
El badge dice ✓ pero el sitio igual tiene avisos. ¿Está mintiendo?
No: el ✓ significa cero fallas (✗). Los avisos (⚠) se cuentan aparte y se muestran con su número. Un sitio puede estar sano y aun así tener deuda de optimización.
¿Por qué el detalle solo muestra 8 líneas?
Porque es un resumen, no un reporte. Para la lista completa, en la VM: ~/validar.sh <sitio>.
El tablero dice “sin datos”.
No pudo leer data.json: el colector no corrió. Se revisa con systemctl --user status panel-data.timer.
¿Esto es producción?
No. Esta VM es DEV y se cae con los cortes de luz. La producción de cada sitio vive en Cloudflare Pages, bajo su propio dominio — es el link prod ↗ de cada fila.