// wiki tecnica
Documentacion tecnica
Stack, endpoints, modelos, infraestructura y problemas resueltos.
Stack tecnologico
- Backend: Python 3.12 + FastAPI + Uvicorn
- ORM: SQLModel (SQLAlchemy + Pydantic)
- Base de datos: PostgreSQL 16 (asyncpg). Fallback SQLite (aiosqlite)
- Templates: Jinja2 (server-side rendering, SPA vanilla JS para el curso)
- PDF: fpdf2 (certificados) + qrcode (verificacion QR)
- Pagos: Stripe Checkout (hosted payment page)
- Contenedores: Docker + Docker Compose
- Red: Tailscale (acceso privado), DuckDNS (dominio publico)
Infraestructura
Servidor
- Host: CT 102 (LXC Proxmox)
- IP Tailscale: 100.99.20.69
- Puerto: 8100 (app), 7681 (terminal web ttyd)
- Dominio: tacolu-content.duckdns.org
Docker Compose
services:
postgres: # PostgreSQL 16 Alpine — puerto 5432 interno
tacolu-content: # App FastAPI — puerto 8100
ttyd: # Terminal web — puerto 7681
volumes:
pgdata: # Datos PostgreSQL persistentes
Volumenes montados (app)
./data:/app/data # certificados, leads PDFs
./music:/app/music # pistas BSO
./output:/app/output # videos generados
./app:/app/app # codigo (hot-reload)
./.env:/app/.env:ro # configuracion
Endpoints API
Publicos (sin auth)
| Metodo | Ruta | Descripcion |
| GET | / | Landing page |
| GET | /blog | Listado de articulos |
| GET | /blog/{slug} | Articulo individual |
| GET | /formacion | Curso interactivo (18 modulos) |
| GET | /formacion/examen | Pagina del examen final |
| GET | /sobre | Sobre nosotros |
| GET | /wiki | Wiki publica (usuario) |
| GET | /wiki/tech | Wiki tecnica (interna) |
| GET | /verificar/{token} | Verificacion publica de certificado |
| GET | /health | Healthcheck |
Examen + Certificado
| Metodo | Ruta | Descripcion |
| POST | /api/examen/checkout | Crea sesion Stripe o bypass si no hay claves |
| GET | /formacion/examen/success | Callback post-pago Stripe |
| POST | /api/examen/iniciar | Genera 40 preguntas aleatorias, arranca cronometro |
| POST | /api/examen/enviar | Corrige, calcula nota, genera certificado si aprueba |
| GET | /api/examen/certificado/{token} | Descarga PDF del certificado |
| GET | /api/examen/estado/{token} | Estado del examen (recuperacion de sesion) |
| GET | /api/examen/contador | Numero de aprobados (social proof) |
| POST | /api/examen/stripe-webhook | Webhook Stripe (backup confirmacion pago) |
Leads
| Metodo | Ruta | Descripcion |
| POST | /api/leads | Captura email, devuelve resumen PDF |
| GET | /api/leads/count | Total de leads capturados |
Admin (requiere JWT)
| Metodo | Ruta | Descripcion |
| POST | /api/auth/login | Login, devuelve JWT |
| GET | /api/jobs | Listar jobs de video |
| POST | /api/jobs | Crear job de video |
| GET | /api/tracks | Listar pistas de audio |
Modelos de datos
Examen
id INTEGER PK
token VARCHAR UNIQUE # UUID, identifica el examen
nombre VARCHAR # nombre completo del alumno
email VARCHAR # email del alumno
dni VARCHAR # DNI/NIE
stripe_session_id VARCHAR # ID sesion Stripe (null si modo prueba)
paid BOOLEAN # pago completado
paid_at TIMESTAMP # fecha de pago
preguntas_json TEXT # JSON [{id, correctAnswer}] — 40 preguntas
respuestas_json TEXT # JSON [{id, answer}] — respuestas del alumno
nota FLOAT # 0-100
aprobado BOOLEAN
intentos INTEGER # intentos usados
max_intentos INTEGER # 3 por defecto
started_at TIMESTAMP # inicio del intento actual
finished_at TIMESTAMP # fin del intento
cert_path VARCHAR # ruta al PDF del certificado
created_at TIMESTAMP
Lead
id INTEGER PK
nombre VARCHAR
email VARCHAR
source VARCHAR # "resumen-pdf"
created_at TIMESTAMP
Articulo
id INTEGER PK
titulo VARCHAR
slug VARCHAR UNIQUE
contenido TEXT # HTML del articulo
estado VARCHAR # borrador|revision|aprobado|publicado
categoria VARCHAR
fecha_publicacion TIMESTAMP
created_at TIMESTAMP
updated_at TIMESTAMP
Autenticacion
JWT con HMAC-SHA256 via python-jose. Token en cookie tacolu_auth.
Lifetime: 24h. Solo usado para el panel admin (/dashboard, /api/jobs).
El examen y las paginas publicas NO requieren autenticacion.
Certificados PDF
- Libreria: fpdf2 (ligera, ~1MB, sin dependencias C)
- Formato: A4 apaisado, 2 paginas
- QR: generado con
qrcode, apunta a /verificar/{token}
- Fuentes: Helvetica core (latin-1). Los titulos de modulos se sanitizan para evitar caracteres fuera de rango
- Almacenamiento:
data/certificados/cert_{token}.pdf
Decisiones de arquitectura
Sin cuentas de usuario para el examen
El examen se identifica con un token UUID guardado en localStorage. Evita la friccion de
registro/login para un producto de pago unico. El email + DNI del formulario son suficientes
para identificar al alumno.
fpdf2 en vez de WeasyPrint
WeasyPrint necesita ~200MB de dependencias (cairo, pango, gdk-pixbuf). fpdf2 son 1MB y genera
PDFs validos con fuentes core. Limitacion: solo latin-1, no soporta unicode completo ni CSS.
Stripe Checkout (hosted) en vez de Stripe Elements
Stripe Checkout es una pagina hospedada por Stripe — no necesitamos manejar datos de tarjeta
ni cumplir PCI DSS nivel alto. El flujo es: crear sesion server-side, redirigir al usuario,
verificar al volver. Menos codigo, menos riesgo.
PostgreSQL en vez de SQLite
SQLite funcionaba bien en desarrollo pero no escala a concurrencia real.
PostgreSQL via asyncpg da pool de conexiones, transacciones ACID reales,
y compatibilidad con herramientas estandar (pgAdmin, backups, replicas).
La migracion fue transparente: mismos modelos SQLModel, solo cambio el driver.
Problemas y soluciones
TypeError: can't compare offset-naive and offset-aware datetimes
Causa: SQLite pierde tzinfo al guardar. PostgreSQL rechaza mezclar naive y aware.
Fix: _now() devuelve naive UTC (.replace(tzinfo=None)). Todos los datetime guardados en DB son naive UTC.
Regex SyntaxError en Windows al parsear course_data.js
Causa: python -c con regex [^"\\] falla en cmd.exe por escape de comillas.
Fix: escribir el script a un .py temporal y ejecutar python script.py.
FPDFUnicodeEncodingException con em-dash en titulos de modulos
Causa: el caracter — (U+2014) no esta en latin-1, que es lo que usa Helvetica core.
Fix: sanitizar titulos con .replace("—", "-") y .encode("latin-1", errors="replace").
Contenedor en crash loop tras cambiar a PostgreSQL
Causa: asyncpg no estaba instalado en la imagen Docker (se instalo con pip exec que no persiste).
Fix: rebuild de la imagen con docker compose build para incluir asyncpg en la capa pip install.
WebFetch no alcanza IPs de Tailscale
Causa: el servidor de WebFetch no esta en la tailnet.
Fix: verificar siempre con curl desde la maquina local, no con WebFetch.
Respuestas del examen perdidas por crash en enviar
Causa: el servidor crasheaba antes de guardar las respuestas por error de timezone.
Fix: recuperar respuestas del JS del navegador (JSON.stringify(respuestas)), corregir manualmente en DB, regenerar certificado.
Stripe SDK y PDF bloquean event loop async
Causa: stripe.checkout.Session.create(), fpdf2 y qrcode son sincronos, llamados desde async def.
Fix: asyncio.to_thread() en todas las llamadas blocking (Stripe, certificado, resumen PDF).
correctAnswer visible en DevTools del navegador
Causa: course_data.js se servia completo con | safe, incluyendo respuestas correctas de las 69 preguntas.
Fix: _get_clean_course_data() elimina correctAnswer y explanation via regex. Tests del curso corrigen via POST /api/curso/check (server-side).
pip install en docker exec no persiste entre reinicios
Causa: la capa del filesystem del contenedor se recrea al reiniciar. pip exec instala en la capa efimera.
Fix: siempre docker compose build para que las dependencias queden en la imagen.
Auditoria de seguridad (junio 2026)
Se ejecutaron dos revisiones automatizadas (security-review + code-review ultra) que analizaron los 42 ficheros del proyecto. Se encontraron 4 criticos, 9 altos, 10 medios y 6 bajos. Todos resueltos.
Medidas de seguridad activas
| Medida | Implementacion |
| Rate limiting | slowapi: checkout 3/min, enviar 10/min, leads 5/min, curso/check 20/min |
| CORS | Origins restringidos: tacolu-content.duckdns.org, formacion, IP tailscale |
| Headers seguridad | X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy |
| XSS hardening | esc() en innerHTML del examen, textContent en tests del curso, correctAnswer eliminado del JS |
| Input validation | Field(max_length) en todos los schemas Pydantic (nombre 200, email 100, dni 15, token 50) |
| Doble envio | Check finished_at != None antes de aceptar respuestas del examen |
| Admin seed | Password desde ADMIN_PASSWORD env var, genera aleatoria si no existe |
| JWT secret | Sin default — la app no arranca si SECRET_KEY no esta en .env |
| Docker non-root | appuser en Dockerfile, HEALTHCHECK integrado |
| PG password | Sin fallback en docker-compose (falla si no esta en .env) |
| Borradores | /borradores redirige a /blog sin cookie auth |
| Queries | SELECT COUNT en vez de cargar todos los registros en memoria |
| Modo prueba | Bypass Stripe vinculado a APP_ENV=development, no a ausencia de clave |
| DNI en PDF | Enmascarado (532***9L) en ambas paginas del certificado |
| Timer examen | Basado en remaining_seconds del servidor, no en reloj del cliente |
| Path traversal | Validacion de ruta dentro de CERTS_DIR antes de servir FileResponse |
| PII en logs | Tokens truncados a 8 chars, emails ofuscados (lu***@dom.com) |
| ttyd bind | Puerto 7681 vinculado a IP Tailscale (100.99.20.69), no 0.0.0.0 |
| Alembic | Inicializado: alembic.ini + migrations/env.py async + script.py.mako |
Mejoras pendientes
Prioridad alta (antes de abrir a internet)
- Claves Stripe reales — configurar STRIPE_SECRET_KEY y STRIPE_PUBLISHABLE_KEY en .env. Sin ellas el examen es gratis (modo prueba activo). Requiere crear cuenta en stripe.com
- Dominio HTTPS — Cloudflare Tunnel o Caddy con cert Let's Encrypt. Cookie Secure flag se activa automaticamente con HTTPS
Prioridad media (requieren servicios externos)
- Email transaccional — enviar certificado por email tras aprobar (Resend o SMTP). Requiere cuenta en proveedor de email
- Secuencia de nurture — 3 emails automaticos tras captura de lead. Requiere email transaccional primero
- Webhook Stripe error handling — devolver 500 si el token no se encuentra. Requiere Stripe configurado primero
Prioridad baja (codigo)
- Upload tracks — validar magic bytes ademas de extension, generar nombre UUID
- Pillow explicito — anadir a dependencias (hoy es transitiva via qrcode)
- Cache invalidation — invalidar _questions_cache si mtime de course_data.json cambia
Futuro (producto/marketing)
- Curso avanzado — "Tacografo para talleres" (calibracion, reparacion, homologacion). Precio 79-149 EUR
- Google/Meta Ads — SEM para "curso tacografo digital", segmentar conductores 25-55
Descartados
CSRF tokens — no necesario. La API exige Content-Type: application/json en todos los POSTs que mutan estado. Los navegadores no envian JSON en POSTs cross-site automaticos (CORS lo bloquea). Con CORSMiddleware + origins restringidos, CSRF no es un vector real para APIs JSON. SameSite=Lax en cookies es proteccion adicional.
Resueltos en esta sesion (ya no pendientes)
Modo prueba vinculado a APP_ENV — hecho (examen.py L126)
Timer del examen relativo — hecho (remaining_seconds)
DNI enmascarado en PDF — hecho (certificado.py L155, L317)
Path traversal defense — hecho (examen.py L374-378)
PII en logs — hecho (examen.py, leads.py)
Terminal web ttyd — hecho (docker-compose.yml, bind a Tailscale IP)
Alembic migraciones — hecho (inicializado, listo para generar revisiones)
course_data.js a JSON puro — hecho (course_data.json, 3 parsers regex eliminados, | safe eliminado)
Migracion a JSON: completada
course_data.js renombrado a .bak. Todo el codigo lee ahora
course_data.json con json.load(). El frontend recibe el JSON
limpio (sin correctAnswer) via JSON.parse({{ course_data | tojson }}).
Cero regex, cero | safe.
Ficheros afectados
app/course_data.json — fuente de verdad (20 modulos, 69 preguntas)
app/course_data.js.bak — backup del original JS, ya no se usa
app/api/examen.py — _parse_questions() usa json.load()
app/api/leads.py — _parse_modules() usa json.load()
app/api/public.py — _get_clean_course_data() usa json.load() + dict pop
app/services/certificado.py — _parse_module_titles() usa json.load()
app/templates/pub_formacion.html — JSON.parse({{ course_data | tojson }})
Changelog
2026-05-30 Examen final + Stripe + certificado PDF + verificacion + progreso
2026-05-30 Certificado rediseñado: minimalista 2 paginas + QR + APTO/NO APTO
2026-05-30 Verificacion publica /verificar/{token}
2026-05-30 Contador social + barra de progreso curso
2026-05-31 Lead magnet: modal + resumen PDF 3 paginas
2026-05-31 Migracion SQLite → PostgreSQL 16 (asyncpg)
2026-05-31 Wiki publica + wiki tecnica
2026-06-01 Auditoria seguridad: 5 criticos + 7 medios/bajos resueltos
2026-06-01 CORS, headers seguridad, Docker non-root, rate limiting, validaciones
2026-06-01 Modo prueba APP_ENV, DNI enmascarado, timer relativo, path traversal
2026-06-01 PII en logs, ttyd bind Tailscale, Alembic inicializado
2026-06-01 Migracion course_data.js → JSON puro (3 regex eliminados, | safe eliminado)
2026-06-01 CSRF descartado (CORS + JSON Content-Type es proteccion suficiente)
2026-06-01 Curso avanzado ST2 completo: 10 modulos, 40 preguntas, 50 slides
2026-06-01 Upload magic bytes, Pillow explicito, cache invalidation por mtime
2026-06-01 Dudas resueltas: motor Rust (bisemanal, equipo, ferry) + BOE (exenciones RD 640/2007)
2026-06-01 Manual documental basico (15 pags) + avanzado ST2 (9 pags) + chuletario + resumen
2026-06-01 Botones de descarga PDF dinamicos por curso (basico vs avanzado)
2026-06-03 Correccion 10 errores factuales: carga/descarga evento unico, software multimarca, DSRC ambos en movimiento
2026-06-03 Briefings v3 para reescritura de manuales (20 pags basico + 15 pags avanzado)
2026-06-03 Motor Rust consultado para resolver dudas periciales (constants.rs, week.rs, teamwork.rs, rest.rs)
2026-06-03 Reescritura manual basico formato v3 (22 pags prosa + graficos) + avanzado ST2 (15 pags)
2026-06-04 Dashboard: gestion de articulos (37 migrados de SQLite), flujo borrador/revision/aprobado/publicado
2026-06-04 Barra de progreso de lectura en articulos (scroll indicator)
2026-06-04 Borradores protegidos en zona privada (/dashboard), quitados del navbar publico
2026-06-04 Endpoints preview PDFs temporales (GET sin email para revision)
Resumen de sesiones
Sesion 1 (30 mayo - 1 junio 2026)
| Feature | Detalle | Estado |
| Examen final + Stripe | 40 preguntas aleatorias de 69, 60 min cronometrado, 3 intentos, certificado PDF | Desplegado (modo prueba) |
| Certificado PDF | 2 paginas minimalistas, QR verificacion, APTO/NO APTO, DNI enmascarado | Desplegado |
| Verificacion publica | /verificar/{token} con badge verde/rojo, DNI parcial | Desplegado |
| Curso avanzado ST2 | 10 modulos, 10 lecciones, 40 preguntas, /formacion/avanzado | Desplegado |
| Lead magnet | Modal tras 2 modulos o 90s, captura email, 4 PDFs descargables | Desplegado |
| Manual basico PDF | 22 paginas v3: prosa + graficos + base legal + nota perito | En revision |
| Manual avanzado ST2 PDF | 15 paginas v3: GNSS, DSRC, DLD, retrofit, calibracion, troubleshooting | En revision |
| Chuletario PDF | 2 paginas A4 apaisado, referencia rapida para cabina | Desplegado |
| Wiki publica + tecnica | /wiki (usuario), /wiki/tech (desarrollo), /wiki/dudas (pericial) | Desplegado |
| PostgreSQL | Migracion de SQLite a PostgreSQL 16 via asyncpg | Desplegado |
| Barra de progreso | localStorage, check verde por modulo, porcentaje visual | Desplegado |
| Contador social | "X personas han obtenido el certificado" en pagina del examen | Desplegado |
Auditoria de seguridad y fixes
Se ejecutaron 2 revisiones automatizadas (security-review + code-review ultra) sobre 42 ficheros. Resultado: 4 criticos, 9 altos, 10 medios, 6 bajos. Todos resueltos:
- 5 criticos: asyncio.to_thread (Stripe/PDF), correctAnswer fuera del JS, admin seed aleatorio, secret_key sin default, rate limiting (slowapi)
- 7 medios: CORS, headers seguridad, Docker non-root (revertido por LXC), validacion inputs, doble envio examen, /borradores protegido, SELECT COUNT
- 7 mas: modo prueba APP_ENV, DNI PDF enmascarado, timer relativo, path traversal, PII logs, ttyd bind Tailscale, Alembic init
- 3 bajos: upload magic bytes, Pillow explicito, cache mtime
- 1 descartado: CSRF (CORS + JSON Content-Type es suficiente)
Migraciones tecnicas
- SQLite → PostgreSQL 16: asyncpg, docker-compose con pg:16-alpine, pool de conexiones, healthcheck
- course_data.js → JSON puro: 3 parsers regex eliminados, | safe eliminado, json.load() en 4 ficheros
- Alembic inicializado: alembic.ini + migrations/env.py async + script.py.mako
Investigacion y hallazgos
- Motor Rust (mini2-tgd-motor-rust): repo GitHub tacolu/mini2-tgd-motor-rust. Port byte-fiel del motor Java Buyond. Contiene todas las constantes del 561/2006 (constants.rs), logica de descansos (rest.rs), semanas ISO 8601 (week.rs), teamwork (teamwork.rs), workday (workday.rs). Usado para resolver 4 dudas periciales sin consultar a Lucas.
- VDO DTCO 4.1a: version actualizada marzo 2025 con OSNMA (autenticacion Galileo anti-spoofing). Manual oficial de 188 pags descargado de fleet.vdo.com. Cursos VDO: online 9h con simulador + presencial en Madrid con DTCO 4.1 real.
- RD 640/2007 + RD 729/2022: 17 exenciones nacionales espanolas completas extraidas del BOE. Letras m, p, q anadidas por el RD 729/2022 (electricos, maquinaria construccion, hormigoneras).
- Calendario retrofit ST2: 31/12/2024 (ST1 intl), 18/08/2025 (analogico/digital intl), 01/07/2026 (furgonetas 2,5-3,5t intl). Coste 800-1.500 EUR/vehiculo.
- Certificado de actividades: solo aplica a tacografos de disco. Con digital/inteligente, las entradas manuales en la VU lo sustituyen.
- Ferry: solo descanso regular 11h, interrupcion max 1h TOTAL (embarque+desembarque), litera obligatoria. Validado por Lucas.
- Compensacion semanal: pegada al descanso semanal NORMAL de 45h, no al reducido. Validado por Lucas.
Sesion 2 (3-4 junio 2026)
| Feature | Detalle | Estado |
| Dashboard articulos | 37 articulos migrados de SQLite, flujo borrador/revision/aprobado/publicado | Desplegado |
| Barra lectura | Scroll indicator rojo en articulos y borradores | Desplegado |
| Correccion errores | 10 errores factuales en cursos JSON (34 ediciones) y PDFs (26 ediciones) | Aplicadas |
| Briefings v3 | Briefings verificados para reescritura de manuales (20 pags basico + 15 avanzado) | Escritos |
| Reescritura PDFs | Manual basico 22 pags v3 + avanzado ST2 15 pags v3 | En revision |
| Preview endpoints | GET /api/preview/manual-basico|manual-st2|chuletario sin email | Temporal |
Errores factuales corregidos (sesion 2)
| Error | Donde | Correccion | Fuente |
| Pantalla ST2 "color/tactil" | PDF basico pag 11 | LCD monocromo en TODAS las generaciones | Manual VDO DTCO 4.1 |
| Carga/descarga "inicio y fin" | Curso avanzado M4, PDFs | Evento UNICO, no par inicio/fin | Manual VDO DTCO 4.1 pag 57 |
| Software "ecosistemas cerrados" | Curso avanzado M6/M9, PDFs | Software SIEMPRE multimarca, ITS = telematica real-time | Validacion Lucas |
| DSRC v1 "vehiculo parado" | Cursos, PDFs, chuletario | AMBAS versiones leen en movimiento, rango CEN 5-25m | Annexe 1C Reg. 165/2014 |
| Descanso semanal invertido | PDF basico pag 18 | NORMAL 45h NO en vehiculo, REDUCIDO 24h SI en cabina | Validacion Lucas |
| CMR = otros trabajos | PDF basico pag 13-14 | CMR/albaran = DISPONIBILIDAD, no otros trabajos | Validacion Lucas |
| Certificado actividades digital | PDF basico pag 19 | Solo aplica a analogicos, con digital son entradas manuales | Validacion Lucas |
| "8-12% expedientes" | PDF avanzado pag 7 | Estadistica inventada, eliminada | Sin fuente = eliminado |
| "Conduccion fantasma" | PDF basico pag 14 | Sin movimiento de ruedas = no hay conduccion | Validacion Lucas |
| Espera no conocida | PDF basico pag 13 | 1a hora = disponibilidad, resto = otros trabajos | Validacion Lucas |
Fuentes consultadas (sesion 2)
- Motor Rust: tacolu/mini2-tgd-motor-rust — constants.rs (todas las constantes 561/2006), week.rs (semanas ISO 8601), teamwork.rs (equipo), rest.rs (descansos)
- Manual VDO DTCO 4.1: pag 57 — "stores the location and time of a loading/unloading process" (singular, evento unico)
- Annexe 1C Reg. 165/2014: DSRC lectura "when the vehicle is in motion" (ambas versiones)
- BOE RD 640/2007 + RD 729/2022: 17 exenciones nacionales verificadas
- FAQ tacografointeligente.com: registro carga/descarga "actualmente una opcion del conductor"
Sesion 3 (5 junio 2026)
| Feature | Detalle | Estado |
| Audio shorts ElevenLabs | 3 shorts del articulo "56 dias" generados con voz clonada Lucas | Desplegado |
| Audio articulo completo | 5482 chars, articulo "56 dias" completo con voz clonada | Desplegado |
| Video HeyGen | Short 1 generado con talking photo (10MB, 1080x1920) | Desplegado |
| Pagina /media | Tabla shorts (audio+video), audio articulo, otros archivos | Desplegado |
| Shorts en DB | Campo shorts_json en tabla articulo, 3 shorts con entrada/despedida fija | Desplegado |
| HeyGen key renovada | API key actualizada, 443 creditos disponibles | OK |
| Acceso iPad/iPhone | Problema: ACLs Tailscale no incluian puerto 8100 para tag:mobile | Resuelto |
Red/Infra resuelto (sesion 3)
- iPad no accedia a LXC 102: las ACLs de Tailscale solo permitian puertos especificos a tag:mobile (80, 443, 3000, 5050, 8080, 8090, 8443, 15672, 26000-26999). El puerto 8100 NO estaba en la lista. Formación en 26200 funcionaba porque esta en el rango 26000-26999.
- Fix: anadir "8100" a la regla de grants mobile→server en las ACLs de Tailscale.
- ts-forward corrupto: la cadena ts-forward de iptables tenia reglas nftables incompatibles. Fix: flush + ACCEPT. Script permanente en /etc/local.d/fix-tailscale-fw.start.
- Nodo expirado: al cambiar tags, el nodo se desautentico. Reautenticado via pct exec desde Proxmox.
Pipeline de contenido (definido, parcialmente implementado)
- Entrada shorts: "Atencion conductor: esto es nuevo y te puede costar dinero. Soy Lucas, de Formación Tacógrafo."
- Despedida shorts: "En formacion puedes hacer el curso, sacarte el certificado y dejar de tener dudas. Nos vemos, sigueme."
- Flujo completo (pendiente): revisar articulo → extraer shorts → aprobar → ElevenLabs audio → HeyGen video → publicar con audio → subir YT
- Avatar HeyGen: Lucas se va a regrabar para tener avatar mas natural. Pendiente nuevo ID.
Acceso web
| Desde | URL | Estado |
| PC (Tailscale) | http://100.99.20.69:8100/ | Funciona |
| iPad/iPhone (Tailscale) | http://100.99.20.69:8100/ | Funciona (puerto 8100 anadido a ACLs) |
| Internet publico | Pendiente Cloudflare Tunnel | Pendiente |
Pendientes
- Stripe: crear cuenta, configurar claves en .env
- Cloudflare Tunnel: para acceso publico sin Tailscale (amigos, testers)
- Email transaccional: Resend o SMTP
- PDFs v3: reescritura pags 10-20 basico (tmp_parte2_new.py pendiente de integrar)
- Ferry + reducido: verificar si Art. 9(1) aplica a descanso reducido 9h
- Avatar HeyGen: Lucas se regraba → nuevo avatar → regenerar shorts
- Flujo automatizado: aprobar articulo → audio → video → publicar → YT
Estadisticas acumuladas
- Commits: 35+
- Ficheros creados/modificados: 80+
- Agentes lanzados: 55+
- Modulos de curso: 30 (20 basico + 10 avanzado)
- PDFs: 4 tipos (manual basico 22p, manual ST2 15p, chuletario 2p, resumen 3p)
- Preguntas de test: 109 (69 basico + 40 avanzado)
- Bugs de seguridad resueltos: 23
- Errores factuales corregidos: 10
- Articulos blog: 37 (migrados de SQLite, gestionables desde dashboard)
- Audios generados: 4 (3 shorts + 1 articulo completo)
- Videos generados: 1 (short 1, HeyGen talking photo)