Skip to content

Consola: procesos de sondeo y consultas simultáneas

2026-10-19. Fuente homelab-maint/app.py, rama main; runtime CT212, /opt/homelab-maint/app.py. Se corrige un timeout/cancelación que podía dejar vivos ping, curl o el shell SSH local. Cada proceso tiene grupo propio, se recoge su handle incluso al cancelar durante spawn y se termina/recolecta el grupo en finally. Esto no garantiza detener comandos remotos que el servidor SSH haya desvinculado: las sondas actuales son de lectura y sus comandos NFS ya tienen timeout.

/api/probes comparte el mismo lote entre peticiones concurrentes y conserva su resultado cinco segundos. Cancelar una petición no cancela el lote de otra pestaña. La fecha at conserva la hora real del lote; no se renueva al servir caché. Se conserva el significado HTTP anterior: un 4xx demuestra respuesta HTTP, no login funcional. Una sonda TCP tampoco demuestra salud completa del servicio.

34 pruebas pasan: las 32 anteriores y dos nuevas que ejecutan timeout/cancelación con procesos locales reales, prueban ausencia del PID tras cleanup, caché, caducidad y desconexión de un consumidor concurrente. Sin acciones sobre NAS, servicios multimedia ni notificaciones como prueba.

Desplegado con deploy.sh tras API running=[]; se preservan estado/logs. API tras reinicio: L0, running=[], recovery=null. Hash local/instalado coincidente: <REDACTED>. Rollback: copia fechada más reciente /opt/homelab-maint-rollback-* creada por deploy.sh; restaurar app.py únicamente con running=[] y reiniciar el servicio. No se activan las transiciones L2–L4 pendientes.

Estado manual y falsos positivos: 20 octubre, 07:56 CEST

Las fuentes fcf3426 y edf71bd de homelab-maint corrigen tres casos:

  • Los endpoints manuales de nivel/marcado preparan una copia del estado y sólo actualizan memoria después de guardarla. Un OSError no publica un nivel u OK que no haya podido persistirse. No se añade garantía de durabilidad ante corte eléctrico ni coordinación entre varios workers.
  • La sonda Docker de VM208 propaga el error de docker ps; cero contenedores sigue siendo un resultado válido si Docker respondió correctamente.
  • La sonda Vera conserva el RC remoto: se retiró | tail -1, que convertía un systemctl is-active inactivo o un error de guest en un falso saludable.

Doce pruebas nuevas del conjunto reciente usan estado privado, ASGI en proceso sin lifespan y helpers SSH/Docker/guest falsos. El conjunto completo suma 74 pruebas, correcto en Python3.12 y3.14 en CI39377261624. No se llaman sondas reales como validación.

Aplicación protectora selectiva a las 07:56 CEST, tras revalidar CT212 running en proxmox2, unidad active/running, RUNS0, L0 y recovery sin pendiente. Sólo se sustituyeron app.py y tasks.yaml, con backup privado, CAS de hashes metadatos e identidad, compilación nativa sin importar app y YAML safe_load. No se ejecutó deploy.sh, pip ni se cambiaron unidades, static, bin o venv. Se reinició únicamente homelab-maint.service después de instalar ambos archivos.

Archivo SHA256 instalado
app.py <REDACTED>
tasks.yaml <REDACTED>

Postcheck de servicio/API/hash correcto: RUNS0, L0, recovery sin pendiente y 11 estados persistidos. state.json conserva hash e identidad completos: <REDACTED>. El GET se limitó a /api/tasks; no se llamó /api/probes, portada, HA ni Sablier.

Recuperación: /root/homelab-maint-protective-g0i8gx8z dentro de CT212, directorio 0700, originales y recibo0600. Para volver atrás, revalidar ausencia de tareas, estado y fuentes actuales antes de reponer únicamente esos dos originales y reiniciar la consola. No restaurar state.json ni repetir tareas. Los reemplazos son individuales y la comprobación de API no bloquea clientes concurrentes; la ventana debe mantenerse ociosa. No se han habilitado transiciones L2–L4.

Timeout o señal no pueden terminar en OK: fuente del20oct18:32

La PR1 de homelab-maint se fusiona18:32:39 CEST en <REDACTED>. Antes, un paso con ignore_rc podía agotar timeout, activar recovery y continuar hasta actualizar last_ok o elevar el nivel. La regresión se reprodujo únicamente con procesos locales y estado privado, sin ejecutar ha-update, NAS ni otra tarea real. La revisión añadió también el caso de terminación local por señal.

Ahora timeout o rc negativo detienen el lote antes de pasos/verificación, conservan nivel y último éxito y exigen revisión para tareas mutativas. Un sondeo read-only interrumpido falla sin imponer recovery mutativa. Los RC ordinarios expresamente tolerados, incluidos42/255 en fixtures, mantienen su comportamiento; no se interpreta137 como prueba de señal local. Una operación SSH remota puede continuar pese a terminar el proceso local: no hay reintento ni certificación de terminación remota.

Las81 pruebas completas pasan en copia temporal local y repetición independiente; CI de PR39458215379 y CI posterior39458375727 pasan en Linux Python3.12/3.14. App fuente SHA256 <REDACTED>. Las pruebas ejecutan grupos locales reales y comprueban ausencia de marcadores posteriores y persistencia del estado; no importan el runtime desplegado ni activan lifespan, probes o productos. El estado de aplicación se documenta separadamente: merge y CI no demuestran despliegue.

Aplicación protectora18:39 y verificación independiente18:40 CEST

El20oct18:39:46,538772 CEST se sustituyó sólo /opt/homelab-maint/app.py de CT212, tras revalidar propietario proxmox2, unidad activa y API ociosa/L0/sin recovery. La baseline coincidía exactamente con la app edf71bd y los archivos preservados de las07:56. El candidato es el blob exacto de main 9616b37, compilado con Python nativo3.13.5 sin importar la aplicación. Se conservaron UID:GID501:50 y modo0644; respaldo privado, CAS de identidades/hashes, sustitución atómica y fsync. Se reinició únicamente homelab-maint.service después de volver a comprobar estado/API ociosos.

La lectura independiente 18:40:32,576600 CEST acredita propietario actual, active/running, PID2485→2806 e inicio monotónico35847028573→74453434769. App instalada SHA9da441… exacta (22295bytes), L0/RUNS0,11 estados y sin recovery pendiente. State SHA2eae… y tasks f2db… conservan identidad y hash completos de baseline; también unidad31f489…, configuración venv aede…, metadatos seleccionados de venv y de17 logs. No se leyó contenido de logs ni se importó la app. No se ejecutaron tareas, probes, HA, llamadas al NAS, deploy.sh o pip; no se cambiaron unidades ni otros archivos.

Recuperación: dentro de CT212, /root/homelab-maint-app-killguard-s7gcrhjy modo0700, app.py.before y receipt.json0600. El original conserva hash8c3c65… y el recibo declara verified/ok, SHA256 <REDACTED>. Para recuperar, revalidar propiedad, ausencia de tareas y configuración actual; restaurar únicamente el archivo acreditado con sus permisos, sin state/logs, y reiniciar sólo la consola. No repetir tareas como prueba de recuperación.

Evidencia privada en ~/.local/state/ct212-app-protection-preflight-20261020-inegqfak/:

  • Preflight SHA256 <REDACTED>.
  • Instalador revisado <REDACTED>; siete fixtures de transacción pasan también en revisión independiente, sin runtime.
  • Resultado de aplicación <REDACTED>.
  • Postverify independiente <REDACTED>.

El flock sólo coordina instaladores cooperantes: las lecturas idle no bloquean nuevos clientes API; la ventana se mantuvo ociosa y el estado posterior coincide. Un resultado incierto exige inspeccionar recibo y runtime, sin retry ni rollback automático. No se habilitan transiciones L2–L4 ni se resuelve por esto el watchdog.

Checkout CI sin credenciales persistentes: 18:48 CEST

PR2 de homelab-maint, merge <REDACTED>, añade únicamente persist-credentials: false al checkout de CI. Conserva pin, permisos de lectura, matriz y comandos. Las81 pruebas pasan en Linux Python3.12/3.14, CI de PR y CI posterior. No cambia el blob de app, instalado previamente con hash9da441…; no requiere otro despliegue ni reinicio de la consola.

Contención de las transiciones L1, 22 octubre 19:00 CEST

La revisión de L1 encontró un riesgo distinto de los timeouts anteriores: exit-l1 despausaba todos los contenedores, arrancaba RunWisp en tres hosts, retiraba el marcador de Vera, activaba una automatización HA y expiraba silencios sin un recibo que identificase los cambios propios de enter-l1. Podía revertir pausas o protecciones anteriores. L2–L4 ya estaban bloqueados y sus comandos antiguos permanecen como borradores inactivos; no se ejecutaron al revisarlos.

La PR3 de homelab-maint, main dd94d12a, bloquea también ambas transiciones L1 con disabled_reason. Los pasos activos conservan sólo el preflight de lectura; los comandos anteriores quedan en draft_steps/draft_verify, que el runner no utiliza. La UI respeta el bloqueo y la API rechaza409 antes de ejecutar. El preflight independiente de lectura sigue disponible. La restauración selectiva de L1 no está implementada: esta entrega contiene la reanudación global, no acredita un ciclo L0→L1→L0.

Las dos regresiones ASGI nuevas fallaban antes; verifican409 sin runner, procesos, escrituras de estado ni logs. La suite de83 pruebas pasa localmente en Python3.14 y la CI en Python3.12/3.14. Dos fixtures de cancelación usan ahora una transición sintética habilitada, conservando la cobertura de abort antes del arranque y doble abort idempotente sin depender de una tarea L1 bloqueada.

Entrega a CT212 el22oct a las19:00:44: únicamente tasks.yaml, mediante backup privado y CAS, sin reinicio. La aplicación relee el YAML en cada petición. SHA256 instalado <REDACTED>, UID501/GID50 y modo0644 conservados. El GET local confirma exactamente las dos tareas cambiadas; PID2806, L0/RUNS0, state.json y metadata de17 logs intactos. No se invocó L1 ni su preflight en producción; tampoco se consultó HA o Vera. Retorno privado en /root/maint-l1-containment-20261022-db6mfkty/ dentro de CT212. Antes de restaurar una versión, revisar su seguridad: el backup conserva las órdenes antiguas y no es una recomendación de reactivarlas.

Persistencia durable de estado, 22 octubre 19:41 UTC

La PR4 de homelab-maint, main <REDACTED>, corrige el registro que precede a los comandos. El temporal es exclusivo y privado, se sincroniza el archivo, se publica con replace y se sincroniza el directorio. La evidencia de un estado ilegible se publica sin sustituir otra copia y también se sincroniza antes de reemplazar el original. Un archivo previo distinto o un symlink falla cerrado.

Ocho regresiones nuevas cubren errores de fsync/replace antes del arranque, revisión que conserva el bloqueo en memoria, archivo corrupto, colisiones y reutilización de evidencia. Pasan 91 pruebas en Python 3.12 y 3.14, sin conexiones externas en las suites locales; ambos jobs de la CI 39840853939 terminaron SUCCESS para el head f760f99a. Un error de fsync del directorio tras replace deja publicación incierta: el archivo nuevo puede ser visible aunque la API conserve su bloqueo en memoria. No se promete que el archivo antiguo siga publicado en ese caso ni se ha ensayado un corte físico de alimentación.

Entrega única a CT212 a las 19:41 UTC: sólo app.py, con CAS y retorno privado en /root/maint-app-only-1c6tjodk/. SHA instalado <REDACTED>, UID 501/GID 50 y modo 0644 conservados. Una fixture nativa de las tres funciones, extraídas por AST sin importar la aplicación, comprobó publicación, modo 0600 y archivo/reutilización de bytes corruptos en un temporal del mismo filesystem. El archivo real de estado no se reescribió para probarlo.

La consola se paró y arrancó una vez. PID 3635 estable durante diez segundos, Result success y NRestarts 0. Estado, tareas y unidad conservados byte a byte y con full9; del directorio de logs sólo se verificó metadata, no los bytes de sus archivos. Revisión independiente 19:42:17 UTC: código exacto, UID/modo, estado y tareas intactos, API L0/RUNS0 sin transición ni recuperación, ambos bloqueos L1 conservados y boots de host/CT estables. No se ejecutaron tareas o probes, ni se consultaron HA o Vera.

Recibos locales maint-app-only-delivery-cos65wln/native.json, SHA <REDACTED>, e independent.json, SHA <REDACTED>. Este cambio no implementa recuperación selectiva L1. Siguen faltando recibos por operación y recurso realmente cambiado, con identidad, estado previo y efecto confirmado; no basta invertir una lista de acciones previstas.

Autenticación por LAN, revisión del 22 de octubre

La política viva de Cloudflare para maint.monxas.casa conserva una sola regla allow limitada al propietario, con PocketID como IdP y sesión de 24 horas. Se consultaron aplicación y política por API, sin modificarlas. Esto protege la entrada por Cloudflare; no prueba autenticación por la ruta local.

La fuente revisada de la consola (da342e4c) no autentica peticiones. La unidad escucha en 0.0.0.0:8800, ejecuta como root y la ruta Caddy mantenida en d96e0eb8 sólo reenvía al backend. La confirmación de una tarea y las guardas L1 no verifican identidad.

La comprobación desde Mac con destino TLS fijado a la VIP .250, sin cookies, Authorization ni redirecciones, devolvió HTTP 200 en /api/tasks; el JSON contenía el esquema esperado de la aplicación. Queda confirmado el acceso anónimo a la API por esa entrada LAN. El intento directo a .212:8800 produjo un error de conexión: no demuestra la política del firewall ni permite extender el hallazgo a todas las rutas de red. Sólo se emitieron GET, sin tareas o probes.

La PR5 de homelab-maint, main <REDACTED>, incorpora validación criptográfica del JWT de Access en origen para todas las rutas funcionales, con audiencia y emisor específicos y rechazo de peticiones sin identidad. Sólo la salud mínima /api/health queda exenta desde el peer socket loopback. La decisión es aceptar el header de Access, sin habilitar una alternativa por cookie directa en LAN. Por ello la entrada LAN que evita Cloudflare recibirá un rechazo; no se promete login automático con DNS local ni autenticación OIDC nativa independiente de Internet. La documentación de Cloudflare describe la validación de firma, emisor, audiencia y claves públicas.

Preflight de CT212: propietario proxmox2, boot del invitado 1ef89884…, PID 3635 activo y NRestarts 0, L0 y ninguna tarea en curso; ambos bloqueos L1 conservados. Recibo local maint-auth-preflight-t341f3kv/before.json.

Pasan 111 pruebas en Python 3.12 y 3.14 con conexiones externas bloqueadas en las suites locales. Ambos jobs de la CI 39845793760 terminaron SUCCESS para db0a4e60; los ocho archivos del merge son idénticos a los revisados. Las regresiones incluyen la aplicación real configurada para producción, firmas RSA, claims incorrectos, rechazo antes de ejecutar tareas o probes, caché y límites JWKS, y caducidad del SSE real conservando la tarea.

Ensayo aislado previo en CT212 con Python 3.13: 20 pruebas de autenticación correctas con red bloqueada, más una lectura separada de las claves públicas de Access por HTTPS. Se comprobaron dos claves compatibles. El ensayo mantuvo la producción intacta y fijó las versiones de las 15 dependencias existentes.

Entrega 20:13:11–26 UTC: sólo app.py, el módulo access_auth.py, requisitos y unidad fuente/instalada; una parada y arranque de la consola. Se añadieron PyJWT 2.15.1, cryptography 50.0.2, cffi 2.1.1 y pycparser 3.12 desde las ruedas del ensayo; las versiones existentes no cambiaron. El servicio quedó en PID 3724, activo y NRestarts 0, estable diez segundos. Salud mínima por loopback: HTTP 200 y cero tareas; /api/tasks anónimo, también por loopback: HTTP 401. proxy_headers=False conserva el peer socket real para la salud mínima.

Estado y tareas conservaron hash y full9, además de la metadata del directorio de logs; no se afirman los bytes de todos sus archivos. No se ejecutaron tareas, probes o POST productivos, ni se accedió a HA o Vera. Retorno privado de código y unidad en /root/maint-auth-staging-<REDACTED>/backup; no restaurarlo sin revisar que devolvería la versión sin este filtro. Recibo nativo maint-auth-preflight-t341f3kv/native-apply.json, SHA256 <REDACTED>.

Verificación independiente 20:15:17–25 UTC: propietario y boots estables, cinco archivos instalados exactos, PID 3724 activo/NRestarts 0, configuración MAINT cargada exacta y ninguna unidad drop-in. Las 15 versiones anteriores y las cuatro añadidas coinciden con el ensayo; estado, tareas y metadata del directorio de logs conservados. Salud loopback 200/cero tareas y API anónima 401. Desde Mac, tres GET por VIP con TLS validado devolvieron 401: anónimo, email de identidad sin firma y assertion malformado. No se probaron credenciales reales. Recibo maint-auth-preflight-t341f3kv/independent.json, SHA256 <REDACTED>.

La verificación de firmas y ASGI con identidades sintéticas no equivale a un login real de producción, que sigue pendiente. El cambio no habilita L1 ni implementa autenticación OIDC nativa independiente de Access/JWKS.

Sondeos sin lectura NFS, 23 de octubre 00:43 UTC

La PR6 de homelab-maint quedó integrada en main como <REDACTED>. El sondeo automático anterior hacía ls de /mnt/nfs_media/nopor en cada consulta de la consola. Ahora consulta la tabla local de montajes con findmnt --kernel --nocanonicalize, limita columnas y comprueba ruta/tipo exactos. La documentación de findmnt distingue la lectura de mountinfo y la canonicalización de rutas. No se recorren directorios NFS ni se dispara autofs para comprobar datos. Vera se retira del sondeo automático mientras el propietario trabaja en paralelo.

El indicador NFS montado en 208 (metadatos) sólo acredita el registro de un montaje NFS/NFS4 en esa ruta. No prueba lectura/escritura, respuesta del NAS ni resolución del estado HELD de recuperación. La prueba TCP es independiente. Pasan 109 pruebas locales y los dos jobs CI Python 3.12/3.14; los fixtures privados ejecutan el pipeline real y cubren NFS, NFS4, ruta equivocada, autofs y fallo del lector. La consulta nativa previa de metadatos fue correcta.

Entrega 00:43:05 UTC: sólo tasks.yaml, sustituido atómicamente tras admitir L0, cero tareas y ausencia de recuperación requerida. Se conservan UID501, GID50 y modo0644, además de estado, app, autenticación y proceso. No se reinicia la consola ni se ejecutan probes, preflight o tareas para verificar la entrega. El archivo queda con SHA256 <REDACTED>. Retorno privado en /root/maint-quiet-probes-20261023-ck8vrxas.

Revisión independiente 00:44:12 UTC: CT212 sigue en proxmox2, boots estables, archivo exacto y metadata conservada, sondeo NFS limitado a metadatos, Vera fuera del sondeo y bloqueos L1 presentes. PID3724 activo, Resultsuccess, NRestarts0; salud loopback HTTP200 y cero tareas. No se consultó la API de probes ni se accedió al NAS o a Vera. Recibo privado maint-quiet-independent-o8w1l729/selected.json, SHA256 <REDACTED>.

Preview de rutas sin mutaciones locales explícitas, 23 de octubre

La PR215 está integrada como <REDACTED>. El sync --dry-run del controlador central antes creaba/truncaba el lock, protegía/escribía cachés y añadía log. Ahora conserva en RAM las etiquetas y las proyecciones de cachés anteriores, sin cambiar caché, permisos, anfitriones aceptados, log o lock. SSH en preview exige claves conocidas y desactiva su actualización, conexiones multiplexadas y comandos locales. No se hace reload ni smoke de aplicaciones.

Es una vista sin bloqueo, no una autorización reutilizable: el sync real conserva su cerrojo y revalida. Las consultas SSH y GET de Cloudflare siguen siendo consultas remotas, que sus sistemas pueden registrar. No se promete instantánea atómica ni ausencia de toda escritura implícita del sistema operativo.

Validación: 42 pruebas correctas y una prueba Caddy opcional omitida en Python 3.12 y 3.14; las cuatro comprobaciones CI del head revisado terminaron bien. Los fixtures ejecutan el preview completo con transportes falsos y rechazan mutaciones; incluyen caché previa/offline, deriva, excepciones, argumentos SSH y preservación del lock/cache en el camino real. Revisión independiente correcta.

Entrega 01:04:23 UTC: sustitución atómica exclusiva de /usr/local/bin/homelab-ctl, SHA256 <REDACTED>. Sintaxis compilada en RAM con el Python nativo, UID/GID/modo preservados. La metadata de 21 rutas de configuración, caché, log, cerrojo, claves y consola se mantuvo igual; PID3724 y NRestarts0, cero tareas. No se invocó el controlador, ni se ejecutó sync, smoke, probes, preflight o reinicio. El retorno del código está en /root/controller-preview-20261023-c0m2khhe; revalidar la ruta en el recibo privado antes de usarlo. La comprobación de comportamiento corresponde a fixtures aislados; no se afirma un preview funcional real contra el homelab.

Revisión independiente 01:06:06 UTC: CT212 en proxmox2, boots estables, ejecutable exacto SHA anterior, UID0/GID0/modo0755 y metadata estables. Sintaxis compilada en RAM sin ejecutar el controlador. Consola PID3724 activa, Resultsuccess, NRestarts0 y salud loopback HTTP200/cero tareas. No se consultaron rutas, tokens, probes o configuración privada. Recibo privado controller-preview-independent-5p1p851g/selected.json, SHA256 <REDACTED>.

Smoke de rutas sin falso éxito, 23 de octubre 01:18–01:20 UTC

El controlador central podía aceptar un error de curl con salida HTTP 000 como éxito. Además usaba TLS sin verificar y seguía redirects. La PR218, integrada como <REDACTED>, corrige ese contrato: cualquier error de transporte, salida inválida/000, 1xx sin respuesta final o 5xx falla. TLS/SNI se verifica contra la VIP, sin curlrc, proxy de entorno ni seguimiento de redirects. Se comprueba la respuesta de entrada; 401/403 no acredita login y 3xx no acredita el destino.

smoke y el smoke posterior al sync omiten las rutas bajo demanda, incluso si ya están activas, para no despertar servicios ni renovar sesiones Sablier. Muestran entradas comprobadas, fallos y omisiones por separado; lo omitido no se cuenta como OK. Un inventario incompleto o incompatible bloquea el smoke antes de cualquier petición; los orígenes de caché y avisos siguen visibles. No se usa este cambio para declarar sanos todos los servicios.

Pruebas de fuente: 52 correctas y una Caddy opcional omitida en Python3.12/3.14; cuatro checks CI correctos. Los fixtures ejecutan un curl privado con los argumentos reales y cubren fallos, estados, omisiones y ambos callers. Entrega 01:18:22 UTC: sustitución exclusiva del ejecutable, SHA256 <REDACTED>. Nueve casos adicionales con Python nativo y transporte aislado sin capacidad de crear procesos/sockets pasaron antes de publicarlo. Metadata de 21 rutas protegidas, unidad y PID3724 conservados; sin sync, smoke HTTP, cambios de rutas ni reinicios. Retorno de código en /root/controller-smoke-20261023-ve4mtmvo.

La consola también ignoraba todo error del paso rutas-smoke. La PR8 de homelab-maint, integrada como <REDACTED>, elimina sólo ese ignore_rc y precisa el nombre de la tarea. Pasan 128 pruebas Python3.12/3.14 y ambos jobs CI. La regresión ASGI/runner usa un CLI privado: salidas1/2 producen fallo y conservan el último éxito; salida0 se acepta, sin probes ni productos reales.

Entrega del catálogo 01:20:12 UTC, después del ejecutable: sólo cambia una línea de tasks.yaml, SHA256 <REDACTED>. UID/GID/modo, bytes de estado, app y autenticación conservados; PID3724 estable, NRestarts0, cero tareas y L1 bloqueado. Retorno privado en /root/maint-smoke-result-20261023-bm0vzuwr. No se ejecutó la tarea productiva: esto demuestra entrega y comportamiento aislado, no login E2E ni salud global.

Revisión independiente 01:21:18 UTC: CT212/proxmox2 y boots estables, ambos archivos con SHA exacto y metadata estable; ejecutable root:root0755 y catálogo 501:50/0644. La tarea contiene un único paso homelab-ctl smoke sin ignore_rc y conserva los bloqueos L1. PID3724 activo, Resultsuccess y NRestarts0; salud loopback HTTP200/cero tareas. Sin ejecutar controlador, probes o smoke ni consultar productos/tokens/configuración privada. Recibo privado controller-smoke-independent-sszfz9_e/selected.json, SHA256 <REDACTED>.

Resultado parcial de sincronización, 23 de octubre

El controlador podía avisar de un fallo de Cloudflare, de recarga Caddy o de copia managed, y después terminar con código 0 y «Hecho» si el smoke pasaba. La PR220, integrada en <REDACTED>, acumula las fases fallidas y devuelve código 3 con «sync parcial». Un éxito HTTP posterior no convierte las otras fases en correctas. Cloudflare requiere el booleano success: true.

Cinco fixtures reproducían el falso éxito antes del cambio. La suite corregida pasa 59 casos y omite uno Caddy opcional en Python3.12/3.14; los cuatro checks CI del commit exacto pasan. Las siete pruebas nuevas cubren cada fase, fallos combinados, tipos de respuesta Cloudflare y el caso completo correcto. Conservan el orden de las acciones; no se añaden reintentos, reinicios ni rollback. Estas son pruebas aisladas, no una incidencia observada ni un sync productivo.

Entrega 01:38:04 UTC: sustituido únicamente /usr/local/bin/homelab-ctl, SHA256 <REDACTED>. Sintaxis compilada en memoria y 16 fixtures nativos aislados correctos: nueve smoke y siete sync, con fronteras remotas simuladas. UID0/GID0/modo0755 conservados, metadata de 21 rutas protegidas sin cambios y consola PID3724 activa, NRestarts0, cero tareas. Sin invocar el controlador productivo, recargar rutas ni reiniciar servicios. Copia de retorno del código en /root/controller-sync-result-20261023-48_honlm; no implica rollback de rutas. Recibo privado controller-sync-publish-n55y4wu9/applied.json, SHA256 <REDACTED>.

Comprobación independiente 01:39:22 UTC: CT212/proxmox2 y boots estables, CLI con hash exacto, root:root0755, un único enlace y metadata estable. Sintaxis compilada en RAM sin ejecutar el código; PID3724/Resultsuccess/NRestarts0 e inicio del proceso estable durante la lectura, salud loopback HTTP200 y cero tareas. Sin controlador, probes, sync, smoke ni lectura de configuración o tokens. Recibo privado controller-sync-publish-n55y4wu9/independent.json, SHA256 <REDACTED>, ligado al recibo de aplicación anterior. Acredita entrega, no ejecución de una sincronización real ni salud funcional de todas las rutas.