Hub

personal

hub-web-viewer

active medium personal fase: 2-completa-fase-3-en-curso
Creado
2026-05-20
Actualizado
2026-05-30
Directorios
  • /home/sergio/agy

Pendientes abiertos (1)

Ver todos →

🎯 Top de ataque (1)

  • #302 📅 2026-06-05 ⏱ 1-2h RSS / Atom feed de log diario (solo Sergio).

Actividad en bitácora 8 días

jun
jul
ago
sep
oct
nov
dic
ene
feb
mar
abr
may
L
X
V
Menos
Más

Hub Web Viewer

Contexto

Sergio quiere visualizar los pendientes del hub (~/agy/) desde el navegador — celular, segunda compu, mostrar a alguien, etc. — sin tener que abrir terminal y hacer cat. Hoy todo vive en MD versionados; las vistas se generan re-leyendo projects/*.md, PENDIENTES.md, INDEX.md, clients/*.md y log/*.md.

Modo: read-only. Los .md siguen siendo única fuente de verdad. La web es vista derivada, no editor.

Sin bidireccional. Sergio confirmó (2026-05-20) que solo necesita visualizar. Cuando esté frente a la compu sigue editando los MD directo desde Antigravity CLI / el editor. Esto elimina toda la complejidad de sync, conflictos, y dual source of truth.

Por qué este enfoque (descartes razonados)

  • Jira / Linear / Trello SaaS — descartado. Sync bidireccional propenso a romper; costos y lock-in para un workflow que ya está resuelto en archivos planos.
  • GitHub Projects + Action que parsea checkboxes — descartado. Bidireccional frágil; ya no se necesita.
  • Obsidian Publish + plugin Kanban — descartado. $8/mes y depende de Obsidian propietario.
  • Custom Laravel/Next con BD — descartado. Overkill: otro servicio que mantener; un static site cubre el 100% del valor.
  • Static site (elegido) — esfuerzo bajo (1-2 días), $0, sin lock-in, sync trivialmente unidireccional (push → rebuild → publish), versionado nativo en el repo del hub.

Tareas pendientes

Fase 0 — Decisiones marco ✅ CERRADAS 2026-05-21

  • (2026-05-21) Decisión #1 — Stack: Astro. Content Collections con frontmatter tipado, dark mode trivial, deploy estático puro, opcional Vue/Svelte si después se quiere interactividad ligera.
  • (2026-05-21) Decisión #2 — Hosting: Self-hosted en poseidon detrás de nginx. Los archivos viven en poseidon, build determinístico ejecutado ahí.
  • (2026-05-21) Decisión #3 — Acceso: nginx público + oauth2-proxy con Google SSO, allowlist exacta svalencia@e-electrosystems.com. Cert con Let’s Encrypt (certbot).
  • (2026-05-21) Decisión #4 — Trigger de rebuild: GitHub Action en push a main. El Action SSH-ea a poseidon (ssh poseidon "cd /var/www/hub-web-viewer && git pull && npm ci && npm run build") usando deploy key. Latencia esperada ~1-2 min.
  • (2026-05-21) Decisión #5 — Alcance v1: PENDIENTES + Proyectos + Clientes. Sin log/inbox en v1 (entran en Fase 2 o 3).

Fase 1 — MVP ✅ CERRADA 2026-05-22

  • (2026-05-21) #289 Scaffold del proyecto en ~/code/hub-web-viewer/ — commit 540d17a, Astro 6.3.7 + Tailwind v4 + dark mode + submodule del hub.
  • (2026-05-22) #290 Parser de frontmatter + body — Content Collections con Zod en src/content.config.ts, commit 61d6452.
  • (2026-05-22) #291 Vista PENDIENTES por prioridad con anchors #NNNsrc/pages/pendientes.astro + src/lib/pendientes.ts, commit 9f7856d.
  • (2026-05-22) #292 Vista proyecto individual — src/pages/projects/[slug].astro, commit 61d6452.
  • (2026-05-21) #293 Dark mode — @custom-variant dark + <html class="dark">, commit 540d17a.
  • (2026-05-22) #294 Deploy — Cloudflare Workers Static Assets + Basic Auth + GH Action en hub para rebuild, commits ee77186+ef070ba+e4cb561. Live.

Fase 2 — Refinamientos ✅ CERRADA 2026-05-26

  • (2026-05-25) Búsqueda full-text client-side con Pagefind.
  • (2026-05-26) #297 Vista log diario (/log listing + /log/[date] individual), commit 989495b.
  • (2026-05-26) #295 Filtros antigüedad clickeables en /projects (Todos/Fresco/Stale/Frozen via hash URL), commit 162e211.
  • (2026-05-26) #298 Métricas dashboard extendidas: Próximas fechas + Disparo condicional + Top proyectos con más pendientes abiertos, commit 6f3e67e.
  • (2026-05-22) #296 Vista cliente (clients/*.md con proyectos vinculados) — src/pages/clients/[slug].astro + remark plugin de reescritura, commit 7361358.
  • (2026-05-22) #299 Badge / chip por status (active / paused / backlog / done / in-progress / ready-to-build) — statusClasses + Badge.astro en /projects/index y /projects/[slug], commits 61d6452 + 56687e2.

Fase 3 — Opcional / futuro

  • (2026-05-28) #300 Vista /calendario con “Próximas fechas” — derivada auto de los projects/*.md (no de la sección manual de PENDIENTES.md, que se desactualiza). Agenda por cubetas (vencidas/hoy/esta semana/este mes/más adelante) + mini-calendarios de meses cercanos; dashboard “Próximas fechas” migrado a la misma fuente. Commit ee1c5b7 pusheado → CF rebuild. Ver bitácora 2026-05-28.
  • #301 📅 2026-06-03 · ⏱ 2-3h · 🤖 medio — Heatmap de actividad por proyecto (días con bitácora).
  • #302 📅 2026-06-05 · ⏱ 1-2h — RSS / Atom feed de log diario (solo Sergio).
  • #WEB-001 📅 2026-05-30 · ⏱ 2-3h · 🤖 medio — Soporte del tag 🤖 <nivel> (alto/medio/ligero) en el viewer: parsearlo en src/lib/pendientes.ts, normalizar el valor, sumarlo al schema/regex, y mostrar chip por pendiente + filtro clickeable en /pendientes (estilo los de antigüedad 🕒/🥶). Convención ya viva en ANTIGRAVITY.md; el viewer aún no lo deriva.
  • (2026-05-27) #303 PWA installable en celular — manifest + SW manual (sin plugin, @vite-pwa/astro solo soporta hasta Astro 5). Iconos PNG generados de SVG con ImageMagick. SW stale-while-revalidate para HTML/_astro/pagefind/icons; network-first fallback para el resto. Solo cachea responses 200 type=basic; 401 nunca entra al cache (Basic Auth compat). Commit 20a15e2 pusheado → CF Workers rebuild via deploy hook. Sergio instala en celular: Chrome Android (banner o ⋮ → Instalar app) / Safari iOS (Compartir → Añadir a pantalla de inicio).

En progreso

Fase 1 MVP — scaffold completo, listo para iterar. Estado:

  • (2026-05-21) Dominio público decidido: hub.electrosystemsnet.com. DNS aún sin apuntar — se hace junto con setup de nginx en poseidon.
  • (2026-05-21) Repo del viewer: ~/code/hub-web-viewer/ local + remote sevaor/hub-web-viewer privado en GitHub, separado del hub. Commit inicial 540d17a pusheado.
  • (2026-05-21) Scaffold Astro 6.3.7 template minimal + TS strict + Tailwind v4 vía @tailwindcss/vite + @custom-variant dark + dark mode default (<html class="dark">).
  • (2026-05-21) Submodule del hub agregado: sevaor/sergio-hub en hub/. Confirmado al apuntar 35eec8d (el HEAD del hub al momento del clone, que ya incluye el commit 5c23415 de cierre de Fase 0).
  • (2026-05-21) Página index placeholder con tarjetas para PENDIENTES / Proyectos / Clientes / Estado. npm run build verde en 1.77s.
  • (2026-05-21) Gotcha capturado: create-astro@5 exige Node ≥22.12; resuelto con nvm + Node 22.22.3 LTS (per-user, no toca apt). .nvmrc=22 en el repo.
  • (2026-05-22) Content Collections con frontmatter tipado — Zod schema en src/content.config.ts (glob *.md + */README.md, excluye _INDEX.md). Campos comunes + 10 opcionales (aliases, deployed_at, end_customer, enlace, parent, related_projects, related, source, phase, host, public_url) con .passthrough() por si aparecen nuevos. Carga 35 proyectos sin errores.
  • (2026-05-22) Vistas /projects + /projects/[slug] — listing agrupado por priority con badges de status/priority/cliente + marcador antigüedad (🕒 ≥7d / 🥶 ≥14d desde updated:). Vista individual expone frontmatter completo arriba (creado/actualizado/host/end_customer/directorios/aliases/related_projects) y renderiza body MD con Tailwind Typography (@plugin "@tailwindcss/typography"). Home con contadores reales (35 totales / N activos). Build verde, 38 páginas en ~3.5s. Commit 61d6452 pusheado.
  • (2026-05-22) Parser de hub/PENDIENTES.mdsrc/lib/pendientes.ts con marked (gfm), reescribe projects/<slug>(.md|/README.md)/projects/<slug>/, convierte #NNN en anchors clickeables con id="pNNN" único (skip duplicados para HTML válido) y :target highlight CSS. Página src/pages/pendientes.astro lee hub/PENDIENTES.md con readFileSync en build-time, header con conteo (211 anchors, 100+ pendientes abiertos, 30+ cerrados recientes). Commit 9f7856d.
  • (2026-05-22) Vista /clients/[slug] — collection clients con Zod (client, name, type, created, updated). Listing ordenado por activos descendente con conteos por status. Pseudo-cliente personal sintetizado para los 5 proyectos con client: personal (no hay clients/personal.md). Vista individual con header + frontmatter + grid de proyectos vinculados + render MD del cliente. Remark plugin remark-hub-links.mjs registrado en astro.config.mjs reescribe projects/<slug>(.md|/README.md), clients/<slug>.md, PENDIENTES.md, INDEX.md (con ../ opcional) a las rutas del viewer — aplica a TODOS los render() de collections, no solo aquí. Badge client en /projects/[slug] ahora linkea a /clients/<slug>/. Build verde 48 páginas. Commit 7361358.
  • (2026-05-22) Dashboard / — 4 KPI cards (proyectos, pendientes por prioridad, clientes, cerrados visibles), top prioridades (high+active), actividad reciente (top 6 por updated: desc), activos sin tocar (🕒/🥶), chips por cliente, cerrado reciente (parser de la sección con regex independientes para fecha/ID/proyecto/headline) con links a #pNNN. Gotcha clave: el submodule hub/ estaba congelado en 35eec8d desde el clone; lo bumpée a 1dc958d (latest) — en el deploy GH Action habrá que correr git submodule update --remote --merge hub antes del build, o nunca verá hub más nuevo. Helpers extractCerradoReciente() y countOpenByPriority() agregados a src/lib/pendientes.ts. Build verde 49 páginas. Commit 2a3bea4.
  • (2026-05-22) Deploy en Cloudflare Workers + Basic Auth — live (terminó siendo Workers, no Pages; Basic Auth, no Zero Trust):
    • Worker hub-web-viewer con wrangler.toml + worker.js (Basic Auth middleware con run_worker_first = true + binding [assets] a dist/).
    • GH Action en sevaor/sergio-hub/.github/workflows/trigger-viewer-rebuild.yml llama deploy hook en cada push a main (commit 7550d80).
    • Secrets runtime configurados: CLOUDFLARE_API_TOKEN (Workers Scripts:Edit), AUTH_USER, AUTH_PASS.
    • Live en hub-web-viewer.<subdomain>.workers.dev. Browser pide Basic Auth y entra.
    • Guía completa de lo que terminó funcionando (con los 7 gotchas reales): docs/cloudflare-deploy.md del viewer. Commit e4cb561.
  • (2026-05-22) Custom domain — descartado. La zone electrosystemsnet.com vive en Oracle OCI DNS, no en Cloudflare, así que “Add Custom Domain” del Worker no funciona directo (necesita CF como autoritativo de la zone o subzone). Opciones evaluadas: (A) delegar hub.electrosystemsnet.com via NS records en OCI apuntando a CF nameservers, (B) migrar la zone entera a CF, (C) quedarse con la URL *.workers.dev larga. Sergio escogió C — bookmark en el navegador resuelve el use case sin la complejidad extra de tocar DNS. Si en el futuro quiere la URL bonita, el camino A (subzone delegation) tiene el menor blast radius: agregar hub.electrosystemsnet.com como zone en CF, copiar los 2 nameservers que asigne, agregarlos como record NS para el name hub en la zone parent en OCI DNS, y de ahí “Add Custom Domain” en el Worker funciona normal.

Notas técnicas

Estructura propuesta

~/code/hub-web-viewer/         # repo separado del hub
├── src/
│   ├── content/               # symlink o git submodule a ~/agy/
│   ├── pages/
│   │   ├── index.astro        # dashboard equivalente a INDEX.md
│   │   ├── pendientes.astro   # vista at-a-glance kanban
│   │   ├── projects/[slug].astro
│   │   └── clients/[slug].astro
│   └── components/
├── astro.config.mjs
└── package.json

Notas de seguridad

  • Nunca commitear secretos al build output. Los MD del hub no deberían tenerlos (regla del ANTIGRAVITY.md), pero confirmar antes de publicar.
  • Auth obligatorio si va a estar accesible desde internet — los nombres de clientes, hostnames, IPs y dinámica comercial no son públicos.
  • oauth2-proxy delante de nginx con Google OIDC; allowlist explícita por email (--email-domain=* + --authenticated-emails-file con svalencia@e-electrosystems.com). El cookie secret se genera localmente y nunca sale del server.

Idempotencia del build

El build tiene que ser determinístico: dado el mismo commit del hub, produce el mismo HTML. Esto facilita debugging y permite hosting estático puro sin lambda functions.

Cambio de decisión 2026-05-22 — hosting

Original (Fase 0): self-hosted en poseidon detrás de nginx + oauth2-proxy + certbot + GH Action SSH.

Nuevo: Cloudflare Pages + Cloudflare Access (gratis hasta 50 users en Zero Trust). Mismo resultado funcional (Google SSO + email allowlist + custom domain + auto-rebuild en push) sin infra propia ni ataduras a poseidon.

Por qué cambió: es solo para Sergio, no para un equipo. Mantener nginx/oauth2-proxy/certbot configurados y debuggear-cuando-truenen no aporta valor. CF Pages hace todo eso transparente.

Descartadas en la misma conversación:

  • Local-only con dist/ commiteada + npx serve (Opción B): muy simple pero requiere correr el server en cada máquina. Bueno si tu prioridad es zero internet, no es la tuya.
  • Tailscale Serve sobre laptop (Opción C): rechazada — depende de que la laptop esté prendida; además mezclar Tailscale + tu WireGuard actual añade ruido al stack de VPN.

Cambio de decisión 2026-05-31 — hosting

Original (Fase 1 / Cloudflare Pages): Hosting en Cloudflare Pages.

Nuevo: Self-hosted en val-soft (192.168.20.8) + Reverse Proxy (192.168.20.14). Se regresa a infraestructura propia usando Nginx y Let’s Encrypt para el dominio hub.val-soft.com.

Por qué cambió: Preferencia por infraestructura y control propio, hospedando el sitio en una VM dedicada (val-soft). El flujo automatizado se mantuvo configurando un self-hosted runner de GitHub Actions directamente en val-soft.

Bitácora

2026-05-31 — Migración a val-soft y CI/CD con self-hosted runner

  • Pidió: Cambiar el visor a la infraestructura propia (val-soft), validar recursos, documentar el servidor en el proyecto electrosystems, configurar Nginx+SSL en reverse-proxy y automatizar despliegues con GitHub Actions (“igual que holbox y medicinas”).
  • Hice:
    • Infraestructura: Validé que val-soft tenía suficientes recursos. Documenté sus características creando servers/val-soft/README.md y actualizando INVENTORY.md en el repositorio electrosystems.
    • Despliegue y Reverse Proxy: Construí el sitio localmente con Docker y transferí a /var/www/hub-web-viewer. Configuré Nginx en el reverse proxy generando certificados Let’s Encrypt para hub.val-soft.com y documenté el sitio en servers/reverse-proxy/README.md.
    • CI/CD Automation:
      • Instalé el servicio actions.runner directamente en val-soft.
      • Creé el workflow .github/workflows/deploy.yml para correr en el self-hosted runner.
      • Resolví los permisos de acceso al submódulo privado (sergio-hub) generando una Deploy Key SSH dedicada.
      • Validé la primera ejecución exitosa de la acción (npm run build + rsync locales) que completa todo el pipeline de despliegue en menos de 2 minutos tras un push.
  • Estado al cierre: hub-web-viewer sirviendo por hub.val-soft.com usando Nginx. GitHub Actions automatiza las actualizaciones extrayendo del submódulo sin intervención manual.

2026-05-30 — #301 cerrado: heatmap de actividad por proyecto

  • src/lib/activity.ts: collectProjectActivity() parsea headers ### YYYY-MM-DD dentro de ## Bitácora; buildHeatmap() construye 52 semanas alineadas a domingo con month labels y celdas null para días futuros.
  • projects/[slug].astro: sección “Actividad en bitácora” entre pendientes y artículo. Grid 12×12px grid-auto-flow:column, etiquetas L/X/V a la izquierda, month labels encima, leyenda. Solo aparece si el proyecto tiene al menos 1 día de actividad.
  • Commit 4091fd4 → CF rebuild.

Resuelto 2026-05-30 [#301]: heatmap de actividad en /projects/[slug]

2026-05-30 — #WEB-001 cerrado: chip y filtro 🤖 nivel de complejidad en /pendientes

  • ComplexityLevel type + campo complexity en OpenPending.
  • COMPLEXITY_RE parsea 🤖 alto|medio|ligero del bullet.
  • stripDescription elimina el tag del texto (paso global antes del strip de prefijos).
  • pendientes.astro: barra de filtros Todos/Alto/Medio/Ligero con conteos, chip de color por item (violeta/sky/teal), data-level en cada <li>, CSS :has() que oculta section-group y project-card vacíos, script JS con sync de hash URL (#level=alto).
  • Commit 916873f pusheado → CF rebuild en curso.

Resuelto 2026-05-30 [#WEB-001]: chip 🤖 por pendiente + filtro clickeable en /pendientes

2026-05-30 — #WEB-001 abierto: soporte del tag 🤖 nivel de complejidad

Sergio pidió que el hub, al crear cada pendiente, anote un nivel de complejidad estable (🤖 alto / medio / ligero, opcional) para elegir el tier de modelo al momento de atacarlo. Se grabó como convención permanente en el ANTIGRAVITY.md del hub (sección Tags inline en cada pendiente) — anclado a la naturaleza del trabajo, no al nombre del modelo, para que no envejezca cuando cambie el catálogo. El viewer todavía no lee el tag; queda #WEB-001 para que /pendientes lo derive en build-time: parsear · 🤖 <nivel> en src/lib/pendientes.ts, normalizar el valor, sumarlo al schema/regex, y exponer chip por pendiente + filtro clickeable (mismo patrón que los filtros de antigüedad 🕒/🥶 de /projects). Nivel asignado 🤖 medio: toca parser + schema + UI con alcance acotado.

2026-05-28 (hotfix #300) — “hoy” en hora de Cd. Juárez, no UTC

  • Síntoma: Sergio reportó que el /calendario decía que ya era 29 cuando en Juárez aún era 28 (y #249, que es “mañana”, salía como “hoy”).
  • Causa: el build corre en UTC en Cloudflare; new Date() de noche en Juárez (UTC−6 en mayo, MDT) ya marca el día siguiente. Es la regla [[feedback-timezone-cd-juarez]] aplicada al build del viewer, no solo al contenido del hub.
  • Fix: todayJuarez() en src/lib/fechas.ts deriva “hoy” con Intl.DateTimeFormat('en-CA', { timeZone: 'America/Ciudad_Juarez' }) (fallback America/Denver, mismo horario de montaña con DST de EE.UU.; último recurso UTC−6h). Usado en /calendario y en “Próximas fechas” del dashboard. Verificado: con UTC=29-may 01:31, todayJuarez() = 28-may; #249 (29-may) cae en “Esta semana / mañana” y el ring de hoy en el mini-grid queda en el 28. Commit ca89166 → CF rebuild.

2026-05-28 — #300 Vista /calendario (primer feature de Fase 3)

  • Pidió: tras detectar que las fechas nuevas de los pendientes no se reflejaban en PENDIENTES.md, Sergio pidió priorizar #300 — “principalmente me interesa para poderlo ver en el hub-web-viewer”.
  • Decisión de diseño clave: el dashboard viejo parseaba la sección manual ## ⏰ Próximas fechas de PENDIENTES.md — la misma que se quedó stale y originó la queja. El calendario nuevo deriva las fechas directo de los 📅 YYYY-MM-DD en los bodies de los projects/*.md (fuente de verdad real). Así, aunque se olvide mantener la sección manual, el viewer siempre muestra lo correcto.
  • Hice:
    • src/lib/fechas.ts (nuevo):
      • loadProjectBodies(baseDir) lee projects/<slug>.md + <slug>/README.md directo del filesystem (mismo criterio que content.config.ts). Gotcha: ni entry.body ni entry.filePath del glob loader de Astro 6.3.7 son fiables para los entries */README.mdentry.body venía vacío y entry.filePath venía undefined, así que #256 (holbox, único fechado en un README) no aparecía. Leer del disco lo resolvió.
      • collectDatedPendings(bodies) extrae los - [ ] con 📅 fecha concreta, deduplica por #NNN (prefiriendo el proyecto canónico sobre design-docs satélite con parent:), ordena por fecha asc. Descripción: se quita el prefijo de tags (**#NNN**, 📅, · ⏱ est, 🔥/⚠️, em-dash) en vez de partir por em-dash — porque algunos pendientes lo omiten (· 🔥 **(Fase X)** texto, ej. #185 tareas-hijo).
      • bucketize(pendings, todayStr) agrupa en vencidas/hoy/esta semana/este mes/más adelante.
    • src/pages/calendario.astro (nuevo): mini-calendarios (grids mensuales) de los meses cercanos con días marcados (count + color urgente, hoy con ring) que linkean a la agenda; agenda por cubetas con fecha+weekday+relativo (“en 3 días”), badges 🔥/⚠️/deadline/⏱, #NNN/pendientes/#pNNN, chip de proyecto → /projects/<slug>/, descripción inline con links del hub reescritos.
    • src/pages/index.astro (dashboard): “Próximas fechas” ahora usa collectDatedPendings (top 6 futuras + badge de vencidas) en vez de la sección manual; link “Ver calendario completo →”. Nav superior con “Calendario”.
    • src/layouts/Layout.astro: link “Calendario” en el nav global.
  • Verificación: build verde 72 páginas (de 71); los 10 pendientes con fecha renderizados (#249/#390/#185/#369/#380/#256/#378/#379/#381/#382), incluido #256 de holbox/README tras el fix de disco; descripciones limpias; dark mode emparejado; sin classes Tailwind dinámicas (las cubetas usan strings literales para que el JIT v4 las detecte). Pagefind local falla (binario no instalado en esta máquina) pero corre en CF.
  • Gotcha menor: un */README.md dentro de un comentario JSDoc cerró el bloque /* */ antes de tiempo y tronó esbuild — reescrito sin */.
  • Deploy: commit ee1c5b7 pusheado a sevaor/hub-web-viewer main (rebase sobre 2 commits remotos que bumpeaban el submódulo; re-bumpeado hub a HEAD 4c914cc). CF rebuild disparado. Falta validación visual de Sergio.
  • Estado Fase 3: #300 cerrado. Quedan #301 (heatmap) y #302 (RSS feed) opcionales.

2026-05-26 — Métricas dashboard extendidas (cuarto feature de Fase 2)

  • Pidió: “Vamos con metricas dashboard extendidas” tras filtros antigüedad live.
  • Auditoría previa: comparé INDEX.md vs PENDIENTES.md como fuentes. INDEX.md está desactualizado (última update 2026-05-21) — Sergio no lo mantiene rigurosamente; PENDIENTES.md sí (auto-commit hook + reglas atómicas). Decisión: parsear de PENDIENTES.md, ignorar INDEX.md como fuente.
  • Hice:
    • src/lib/pendientes.ts — 3 helpers nuevos:
      • renderInlineMd(md) (privado) usa marked.parseInline (no parseBlock — preserva inline en <li> sin envolver en <p>) + reescribe links del hub + convierte #NNN a <a class="hub-anchor"> apuntando a /pendientes/#pNNN.
      • extractSection(raw, sectionRegex, limit) (privado) parser genérico que captura una sección entre headings, filtra solo bullets - , detecta ~~tachado~~ o ✅ como resueltos, extrae ID #NNN de **#NNN**, renderiza el resto del bullet como inline MD.
      • extractFechas(raw, limit=5) extrae ## ⏰ Próximas fechas. extractCondicional(raw, limit=5) extrae ## 🔒 Disparo condicional.
      • countOpenByProject(raw): matchAll sobre ### [\slug`](cada bloque de proyecto en las secciones de prioridad) y cuenta^- [ ]dentro del bloque entre un header y el siguiente. Devuelve{slug, open}[]ordenado desc. Sergio anida proyectos en múltiples sub-secciones (alta/media/baja por cliente/etc.) — el helper acumula con(counts.get(slug) ?? 0) + open`.
    • src/pages/index.astro — 3 secciones nuevas:
      • ”⏰ Próximas fechas” + ”🔒 Disparo condicional” en grid 2-col después de “Top prioridades / Actividad reciente”. Card ámbar (fechas) + violeta (condicional), filtra entries resolved (tachadas o con ✅). Links a /pendientes/#-próximas-fechas.
      • ”📋 Proyectos con más pendientes abiertos” (top 8) — grid 2-col con tarjetas <a> linkeando a /projects/<slug>/, badge rose con el número de pendientes. Filtrado: solo proyectos que existen en la collection (projectSlugs set) — evita mostrar slugs huérfanos del PENDIENTES.md sin MD propio.
  • Verificación: build verde, 68 páginas. Top 8 verificados en HTML: es-antenas-new (12), backups-infra (11), amadeus/holbox/machine-parity/aprende-ingles (10), orion-decommission (9), electro-ia (7). Próximas fechas con 3 entries activos (jm-checador 2026-08-14, gi-siptrunks-replacement sin fecha, cobros-recurrentes 2027-03-01). Disparo condicional con 2 (cpe-benito-juarez cap 200Mbps, adfsa-migration MFC-R2).
  • Decisión de scope: NO incluí sección “Bloqueadores 🚨” de INDEX.md (única info que aportaría) porque INDEX.md no se actualiza fiable; podría llevar a info stale. Si Sergio quiere bloqueadores en el dashboard, mejor agregar una tag 🚨 o flag al frontmatter de proyectos y filtrar por eso.
  • Falta:
    • Sergio: validar visualmente tras push + rebuild (~90s).
    • Yo: commit + push.
  • Estado Fase 2: 4 de 4 features cerrados (Pagefind ✅ + log ✅ + filtros antigüedad ✅ + métricas dashboard ✅). Solo queda la migración opcional Default UI → Component UI de Pagefind (1h, mejor a11y) como única tarea de Fase 2 pendiente.

2026-05-26 — Filtros antigüedad clickeables en /projects (tercer feature de Fase 2)

  • Pidió: “filtros antiguedad clickeables” tras confirmar log diario live.
  • Hice:
    • src/pages/projects/index.astro rediseñado con filtros client-side:
      • Helper local ageBucket(updated) que clasifica como fresh (<7d) / stale (≥7d) / frozen (≥14d). Aplicado a cada <li data-age="fresh|stale|frozen">.
      • Conteos pre-calculados server-side (ageCounts.fresh/stale/frozen) mostrados en los chips.
      • Barra de filtros con 4 chips (Todos / Fresco / 🕒 Stale / 🥶 Frozen), cada uno con su conteo entre paréntesis. Diseñados como <a> con href="#age=stale" etc — funcionan como deeplinks shareables.
      • Wrapper <main class="age-filter" data-filter-age="all"> cambia su atributo según filtro activo.
      • CSS para ocultar items: [data-filter-age="stale"] li[data-age]:not([data-age="stale"])display: none. Equivalente para fresh/frozen.
      • CSS para ocultar secciones vacías con :has(): .age-filter[data-filter-age="stale"] .age-group:not(:has(li[data-age="stale"]))display: none. Soporte amplio en Chrome 105+ (el viewer solo es para Sergio en navegadores modernos, no para sucursales Holbox con Chrome 109).
      • JS inline minimalista (~30 líneas, vanilla): lee location.hash con regex age=(\w+), aplica filtro, actualiza el conteo “(N)” de cada section al filtrado dinámicamente, listeners en los chips con e.preventDefault() + history.replaceState para evitar scroll-to-top del navegador en cambio de hash, listener hashchange para soporte de navegación back/forward y deep-links desde otras páginas. Mensaje “Sin proyectos en este filtro.” oculto por default, se muestra solo si el filtro deja todo vacío.
      • Chip activo con estilo emerald: .age-filter[data-filter-age="stale"] .age-chip[data-age-filter="stale"] → fondo emerald + texto blanco. Variante dark.
    • src/pages/index.astro (dashboard) — agregué link “Filtrar todos →” al lado del header de “Activos sin tocar (N)” apuntando a /projects/#age=stale. Los chips individuales siguen llevando a /projects/<slug>/ (no se rompió ese flujo).
  • Verificación: build verde, 47 items con data-age en HTML buildeado, 4 chips de filtro, JS inline incluido, deeplink desde home presente. 17,559 palabras indexadas (-3 vs build anterior: marginal por whitespace).
  • Decisión de scope: preferí filtros client-side via hash sobre rutas estáticas pre-compiladas (/projects/stale/) porque (1) Astro static no soporta querystrings dinámicos para SSG, (2) hash queries dan URLs shareables sin 3x el contenido en dist/, (3) JS ya está cargado por Pagefind así que cero overhead nuevo.
  • Falta:
    • Sergio: validar visualmente click en chips + deeplinks /projects/#age=stale + back/forward del navegador.
    • Yo: commit + push.
  • Estado Fase 2: 3 de 4 features cerrados (Pagefind ✅ + log ✅ + filtros antigüedad ✅; faltan métricas dashboard extendidas + migración opcional Pagefind Component UI).

2026-05-26 — Vista log diario (segundo feature de Fase 2)

  • Pidió: “Vamos con el log diario” tras confirmar visualmente que Pagefind ya funcionaba en el sitio live.
  • Hice:
    • src/lib/log.ts (nuevo helper) con 3 funciones:
      • listLogs() lee hub/log/*.md filtrado por regex YYYY-MM-DD.md (los archivos del hub no tienen frontmatter, solo h1 # YYYY-MM-DD y cuerpo), devuelve {date, filename, bytes, preview} ordenado desc.
      • loadLog(date) y renderLog(raw) para vista individual: corren marked con GFM, luego reescriben links del hub (projects/<slug>.md/projects/<slug>/, clients/, PENDIENTES.md/pendientes/, INDEX.md/, log/YYYY-MM-DD.md/log/YYYY-MM-DD/) con regex (los logs no pasan por la collection render() así que el plugin remark global no aplica). Decisión clave: los #NNN en logs se reescriben a <a href="/pendientes/#pNNN" class="hub-anchor"> sin id local — los IDs son refs al PENDIENTES.md, no anchors propios de la página de log.
      • extractPreview(raw) busca secciones ## Foco/Hecho/Resumen/Cierre/Sesión y captura las primeras 3 líneas significativas; fallback al primer texto no-heading si no encuentra. Limpia **, *, ` (no underscores — eso rompe identifiers como array_values). Cortado a 240 chars con .
      • formatLogDate(date) devuelve {weekday, pretty} en español (lunes/martes/… + “26 de mayo de 2026”).
    • src/pages/log/index.astro — listing agrupado por mes (mayo 2026 · 11) con tarjetas que muestran fecha + weekday + bytes (33.9 KB) + preview line-clamp-3. Nav cross-section (Inicio / Pendientes / Proyectos).
    • src/pages/log/[date].astrogetStaticPaths desde listLogs(). Header con fecha tabular + weekday + “26 de mayo de 2026” + nav cronológico (← prev / next →). Body con <Content /> envuelto en Tailwind Typography. <style is:global> para .hub-anchor (versión sin :target porque cada anchor linkea fuera de la página actual).
    • src/layouts/Layout.astro — agregué <nav> debajo del brand con 4 links (Pendientes / Proyectos / Clientes / Log) visible en ≥640px; en móvil queda solo el search box (los links principales viven en cada page). Marcado data-pagefind-ignore por estar dentro del header — no contamina el índice.
    • src/pages/index.astro (dashboard) — agregué import { formatLogDate, listLogs }, calculé recentLogs = listLogs().slice(0, 6), agregué botón “Log” al nav de arriba y una <section> al final con grid sm:grid-cols-2 de los últimos 6 días + preview corto (line-clamp-2) + link “Ver todo →” al /log/.
  • Verificación: build verde, 68 páginas (de 56), 17,562 palabras indexadas por Pagefind (+716 vs solo proyectos). 11 logs convertidos a páginas individuales. Anchors #NNN reescritos correctamente verificado en dist/log/2026-05-23/index.html (13 anchors → /pendientes/#p190, etc.).
  • Gotcha capturado: primera versión del extractor usaba .replace(/[*_]/g, ”)que comía underscores de identifiers comoarray_valuesoDispositivo::with. Fix mínimo: regex específica **|*|“ (solo MD formatting real).
  • Falta:
    • Sergio: validar visualmente tras push + rebuild (~90s).
    • Yo: commit + push (regla Bash(git push origin main) en .claude/settings.local.json aún no carga en esta sesión — la próxima sesión sí; si Sergio no quiere esperar, push directo).
  • Estado: Fase 2 al 50% (Pagefind ✅ + log ✅; faltan filtros antigüedad clickeables + métricas dashboard extendidas + migración a Pagefind Component UI).

2026-05-25 — Pagefind (primer feature de Fase 2)

  • Pidió: “vamos por el pagefind” tras pregunta abierta sobre qué sigue.
  • Setup máquina: este equipo no tenía Node 22 (solo v12 vía apt). Classifier bloqueó curl | bash de nvm. Sergio escogió Docker node:22-bookworm-slim (consistente con regla [[feedback-php-mysql-via-docker]] extendida a Node) con -u $(id -u):$(id -g) para mantener ownership de archivos. Submodule del hub re-clonado vía SSH (la auto-resolución HTTPS del git submodule update --init no tenía credenciales en esta máquina) y bumpeado de 1dc958db75b1be (HEAD actual del hub).
  • Hice:
    • Pre-fix de schema (no relacionado a pagefind pero bloqueaba el build):
      • Proyectos nuevos del hub usan status: in-progress y ready-to-build, fuera del enum active|paused|backlog|done que tenía src/content.config.ts. Cambié status a z.string() para desacoplar el viewer del enum del ANTIGRAVITY.md del hub (el ANTIGRAVITY.md sigue siendo source of truth para cómo escribir, pero el viewer no debería tronar si Sergio agrega un status nuevo).
      • Agregué entradas a statusClasses para in-progress (verde como active) y ready-to-build (violeta), más statusFallbackClass slate para cualquier status desconocido futuro. Replacé ?? '' por ?? statusFallbackClass en las 4 pages que usan badges (preserva la regla [[feedback-dark-mode-aesthetics]]).
      • STATUS_ORDER ampliado con los 2 nuevos en posición lógica; sortProjects ahora usa statusRank() que asigna length a desconocidos (van al final, no al inicio como hacía indexOf -1).
      • Gotcha del glob: electrosystems-network-map/README.md matcheaba */README.md pero NO es el entry del proyecto (el entry es electrosystems-network-map.md en root) — es un sub-doc de la KB. Sin frontmatter de proyecto. Fix: agregué !electrosystems-network-map/README.md al pattern. Si más adelante aparece otro caso similar, considerar custom loader que solo trate <dir>/README.md como entry cuando no hay sibling <dir>.md en root.
    • Pagefind instalado como devDep (pagefind@1.5.2). Cambié package.json build script a astro build && pagefind --site dist --output-subdir pagefind. Conservé build:astro y build:index por separado por si después se quiere debuggear cada paso.
    • Layout.astro rediseñado con header global sticky:
      • <header data-pagefind-ignore> con brand “Hub” + caja de search <div id="search">. El data-pagefind-ignore evita que el chrome del header (la palabra “Hub” repetida en cada página) contamine el índice.
      • <div data-pagefind-body> envuelve el <slot /> → Pagefind solo indexa el contenido real de cada page. Usé <div> y no <main> porque las páginas ya tienen su propio <main> y se anidarían (HTML inválido).
      • <link rel="stylesheet" href="/pagefind/pagefind-ui.css"> y <script src="/pagefind/pagefind-ui.js"> cargados en cada página. En astro dev (sin build) esos paths tiran 404 silenciosos pero no rompen — el search simplemente no aparece hasta que se hace un build con pagefind. Aceptable porque dev no usa búsqueda.
      • Translations en español (placeholder “Buscar pendientes, proyectos, clientes…”, “Sin resultados”, “Cargar más”, etc.).
      • CSS overrides: definí CSS vars Pagefind (--pagefind-ui-primary sky-400, --pagefind-ui-text slate-900/200, --pagefind-ui-background white/slate-950, --pagefind-ui-border slate-300/700, --pagefind-ui-tag slate-100/800) con set light + override html.dark .hub-search para dark mode. Drawer absoluto con max-height: 70vh overflow-y auto, posicionado con width: 32rem y margin-left: -16rem (sobre el centro del input) en ≥640px; full-width en móvil. <mark> con amarillo claro en light / tabaco en dark.
    • Pagefind nota informativa: la 1.5.0+ recomienda Component UI nuevo (mejor a11y + modal) en vez de Default UI. Me quedé con Default UI por velocidad; migrar a Component UI es 1h de trabajo si se quiere mejor accesibilidad después.
  • Verificación: build completo docker run ... npm run build → 56 páginas Astro + Pagefind indexó 16,846 palabras en 0.325s. dist/pagefind/ pesa 2.1MB (86 archivos: WASM ES + chunks + UI scripts). Confirmé presencia de id="search", data-pagefind-body, data-pagefind-ignore en HTML buildeado de /, /projects/, /pendientes/, /clients/. WASM detectado como es (español) automáticamente.
  • Operacional: el Worker de Cloudflare con binding [assets] sirve /pagefind/* transparente. Basic Auth se persiste en browser por todo el origin → los fetch de fragments/chunks ya van autenticados sin reprompt.
  • Falta:
    • Sergio: confirmar visualmente en el sitio live tras push + rebuild en CF (~90s después de push al viewer).
    • Yo: pedir auth para commit + push de ~/code/hub-web-viewer (no estaba clonado en esta máquina, lo cloné durante la sesión).
  • Estado: Pagefind feature completo en local. Pendiente solo el commit + push para que la CF Action rebuilde con el índice.

2026-05-22 (cierre — Fase 1 live)

  • Pidió: ejecutar plan Cloudflare Pages + Access. Después decidió que Zero Trust no por la fricción del registro con tarjeta. Final: Workers Static Assets + Basic Auth inline.
  • Hice:
    • Setup inicial: documenté Pages + Zero Trust en docs/cloudflare-pages-setup.md (commit 127fa74). Sergio conectó el repo en el dashboard de CF.
    • Gotcha #1 (clone falla): primer build tronó en “updating repository submodules”. Causa: .gitmodules con SSH URL (git@github.com:...), CF Pages/Workers auto-corre git submodule update --init durante el clone con auth HTTPS via GH App. Fix: cambié .gitmodules a HTTPS (commit 6da05c6) + le dije a Sergio que pusiera git config --global url."git@github.com:".insteadOf "https://github.com/" en su ~/.gitconfig local para preservar el workflow SSH sin afectar a CF.
    • Gotcha #2 (deploy falla con auth): build verde 49 páginas pero deploy step truena con Authentication error [code: 10000] del API token auto-inyectado. Causa: token de CF tiene scopes limitados al propio proyecto, no permite wrangler pages deploy. Fix: Sergio creó API token custom con Pages:Edit y lo metió como secret CLOUDFLARE_API_TOKEN.
    • Gotcha #3 (project not found): próximo intento truena con Project not found. ... hub-web-viewer. Sergio compartió URL del dashboard: /workers/services/view/hub-web-viewer (NO /pages/view/). El nuevo flujo unificado de CF crea el proyecto como Worker con Static Assets, no como Pages. Fix: agregué wrangler.toml con [assets] apuntando a dist/ (commit ee77186), cambié Deploy command a npx wrangler deploy (no wrangler pages deploy), agregué Workers Scripts:Edit al API token. Deploy completo, sitio servido en *.workers.dev.
    • Gotcha #4 (Zero Trust friction): Sergio entra sin auth en incógnito. Al ir a Zero Trust → Access para configurar la policy, CF pide detalles de pago aunque el plan sea gratis. Sergio prefirió no. Pivot: Basic Auth inline en el Worker.
      • Escribí worker.js (30 líneas) con middleware Basic Auth: compara Authorization: Basic decodificado contra env.AUTH_USER/env.AUTH_PASS con safeEqual() constant-time; si match, env.ASSETS.fetch(request).
      • wrangler.toml: agregué main = "worker.js", binding = "ASSETS", run_worker_first = true (sin esto el worker no corre antes de servir el static asset). Commit ef070ba.
    • Gotcha #5 (YAML frontmatter trona el build): próximo rebuild trona en holbox.md línea 10:185 con bad indentation of a mapping entry. Causa: last-fix: ...; Inmediatamente antes: NaN al cargar... — el segundo : interno interpretado como mapping nuevo por js-yaml. Fix: wrap value en single quotes + cambio del segundo : a em dash (commit 686c1ab en el hub). Memoria nueva reference_yaml_frontmatter_colon_gotcha.md para prevenir recurrencia.
    • Gotcha #6 (Build secrets vs Runtime secrets): Sergio configuró AUTH_USER/AUTH_PASS pero el sitio respondió “Auth not configured”. Causa: las metió en Settings → Build → Variables and Secrets (build-time, no expuestas al runtime del Worker). Las dos secciones tienen UI idéntica. Fix: re-configurarlas en Settings → Variables and Secrets (raíz, runtime). Aplican en segundos sin redeploy.
    • Live: Sergio confirma que entra con Basic Auth.
    • Cleanup:
      • Reescribí docs/cloudflare-deploy.md con la realidad de lo que funcionó (Workers + Basic Auth) + 7 gotchas reales documentados. Borré docs/cloudflare-pages-setup.md (obsoleta) y docs/trigger-viewer-rebuild.yml.template (el GH Action ya vive en sevaor/sergio-hub). Commit e4cb561.
  • Estado final:
    • Worker live, Basic Auth activo, deploy hook conectado.
    • Push al viewer → CF rebuilda. Push al hub → GH Action → CF rebuilda con submódulo al día.
    • Costo: $0. Infra propia: cero.
  • Falta solo: custom domain hub.electrosystemsnet.com (opcional — está documentado en la guía, son 3 clicks en el dashboard).

2026-05-22 (continuación noche II)

  • Pidió: “ya lo estuve pensándolo bien, y creo que meterle infra en poseidon es overkill porque solo es para mí” — buscaba opciones para correr en laptop y verlo desde casa (atado a hub-portability).
  • Hice:
    • Surface de 3 opciones con tradeoffs: (A) Cloudflare Pages + Access — público con SSO, sin infra; (B) local-only dist/ commiteada + npx serve por máquina; (C) Tailscale Serve sobre laptop.
    • Recomendé A para el caso “abro 30s a checar algo en cualquier lugar” y B para “voy a sentarme a trabajar en casa”. Descarté C por la dependencia de “laptop prendida”.
    • Sergio escogió A.
    • Verificación previa: revisé .gitmodules (URL SSH git@github.com:sevaor/sergio-hub.git) y git config --global (sin URL rewrites). Gotcha #1: CF Pages auth via HTTPS, no SSH — solución es git config --global url."https://github.com/".insteadOf "git@github.com:" en el build command (no toco .gitmodules para no romper workflow local). Gotcha #2: CF Pages corre git submodule update --init por default (commit pinneado, no HEAD) — solución --remote --merge en el build command para traer hub al día. Gotcha #3: CF rebuilda en push al viewer pero no detecta cambios en el hub — solución Deploy Hook + GH Action en el repo del hub.
    • docs/cloudflare-pages-setup.md en el viewer con paso a paso: conectar repo + autorizar GH App también para sevaor/sergio-hub (sin esto el build truena), build command exacto, env var NODE_VERSION=22.12.0, Access policy con Google IdP + email allowlist, custom domain hub.electrosystemsnet.com opcional, sección Deploy Hook con el handoff “pásame la URL y commiteamos el GH Action en el hub”.
    • docs/trigger-viewer-rebuild.yml.template con el GH Action listo (curl al ${{ secrets.CF_DEPLOY_HOOK }} en cada push a main del hub). Vive en docs/ temporalmente — su lugar final es sevaor/sergio-hub/.github/workflows/trigger-viewer-rebuild.yml una vez que tenga la URL del hook.
    • Commit: 127fa74 (“docs: setup de Cloudflare Pages + Access”) pusheado.
  • Falta:
    • Sergio: ejecutar los pasos 1-7 de docs/cloudflare-pages-setup.md en el dashboard CF. Crear Deploy Hook y pasarme la URL.
    • Yo: commitear .github/workflows/trigger-viewer-rebuild.yml en sevaor/sergio-hub con la URL en secret.
  • Flujo final operativo proyectado:
    • Push al viewer → CF rebuilda automático.
    • Push al hub → GH Action llama deploy hook → CF rebuilda con submódulo al día.
    • Latencia ~90s. hub.electrosystemsnet.com (o *.pages.dev) refleja el cambio.

2026-05-22 (continuación noche)

  • Pidió: “ok, dale al dashboard /”.
  • Hice:
    • src/lib/pendientes.ts — agregué extractCerradoReciente(raw, limit=8) y countOpenByPriority(raw). Primera versión del extractor combinaba fecha + ID en un solo regex \*\*(date)(?:[^*]*)?\s*(?:\[#(\d{3})\])?\*\*; falló con líneas como **2026-05-21 noche tardío [#131 + #132]** porque [^*]* greedy se comía el [#131] antes del optional ID match. Lo separé en dos regex independientes: \*\*(\d{4}-\d{2}-\d{2}) para fecha (no necesita cerrar **) y \[#(\d{3}) para el primer ID en la línea. Funciona para ambos formatos.
    • src/pages/index.astro rediseñado completo como dashboard:
      • Header con nav a /pendientes (verde, destacado), /projects, /clients.
      • 4 KPI cards: Proyectos (total + breakdown por status), Pendientes abiertos (rojo, breakdown alta/media/baja), Clientes (total + reales vs pseudo), Cerrado reciente (azul cielo, conteo de _**Resuelto).
      • 2-col grid: izquierda Top prioridades (high+active sorted), derecha Actividad reciente (top 6 por updated desc).
      • Sección Activos sin tocar (chips ámbar con 🕒/🥶 + label días).
      • Por cliente (chips ordenados por activos desc, conteo prominente).
      • Cerrado reciente (top 6 entries con fecha + #id linkeado a #pNNN + proyecto linkeado + headline en negrita).
    • Gotcha mayor: primer build mostraba entries de 2026-05-21 como las más recientes en “Cerrado reciente” — pero yo había agregado #168 / #167 / #166 hoy 2026-05-22. Diagnóstico: git submodule status en hub-web-viewer/hub apuntaba a 35eec8d (HEAD del hub al momento del clone original, 2026-05-21 tarde-noche). Todos los commits del hub desde entonces eran invisibles para el build. Fix: cd hub-web-viewer && git submodule update --remote --merge hub → bump a 1dc958d (latest, incluye electro-ia + backups-infra inventory bridge + machine-parity proyecto + todos los míos). Implicación operativa: la GH Action de deploy debe ejecutar git submodule update --remote --merge antes del npm run build o el deploy a poseidon siempre verá un hub estancado. Capturé esto en la nota del item de #129.
    • Build: 49 páginas en ~3.5s (35 → 49 porque al actualizar el submodule entró machine-parity también).
    • Commit: 2a3bea4 (“feat: dashboard / con KPIs, top prioridades, stale, recientes, cerrados”) pusheado a sevaor/hub-web-viewer. Incluye bump del submodule a 1dc958d.
  • Falta (Fase 1): infra poseidon — nginx + oauth2-proxy + certbot para hub.electrosystemsnet.com (allowlist svalencia@e-electrosystems.com) + GitHub Action que SSH-a a poseidon y corre git pull --recurse-submodules && git submodule update --remote --merge && npm ci && npm run build. Eso es lo único bloqueante para ver el viewer en internet.

2026-05-22 (continuación tarde II)

  • Pidió: “avanzamos con vista /clientes/[slug]” — usé /clients/ (en inglés) por consistencia con clients/ del hub y con /projects/.
  • Hice:
    • Auditoría: 7 archivos en clients/ (benito-juarez, deportes-campeon, electrosystems, greco-cell, holbox, joyerias-meza, telcel) — no hay clients/personal.md, pero 5 proyectos tienen client: personal en su frontmatter. Decidí sintetizar un pseudo-cliente “personal” con badge pseudo-cliente y nota explicativa.
    • src/content.config.ts — agregué collection clients con clientSchema (Zod: client, name, type, created, updated).
    • src/lib/clients.tsbuildClientGroups(clients, projects) que cruza el frontmatter client: de los proyectos con las entries de clients/. Acumula proyectos huérfanos (slug que no existe en clients/) y, si el slug es personal, lo etiqueta como isPersonal=true. Ordena por activos desc.
    • src/pages/clients/index.astro — listing 2-col con conteo por status (totales / activos en verde / pausa en ámbar / backlog / done) + badge pseudo cuando aplica.
    • src/pages/clients/[slug].astrogetStaticPaths desde buildClientGroups. Header con badges, <dl> de frontmatter, grid de proyectos vinculados con sortProjects(), después <Content /> del body MD. Si isPersonal=true, no hay Content y muestra una nota amarilla al final explicando la convención.
    • src/lib/remark-hub-links.mjs — plugin Astro/remark que recorre el AST con un walker simple (sin unist-util-visit para no agregar deps). Detecta links link.url con regex y reescribe:
      • (?:\.\./)?projects/<slug>(\.md|/README\.md)?(#anchor)?/projects/<slug>/<#anchor>
      • (?:\.\./)?clients/<slug>\.md(#anchor)?/clients/<slug>/<#anchor>
      • (?:\.\./)?PENDIENTES\.md(#anchor)?/pendientes/<#anchor>
      • (?:\.\./)?INDEX\.md(#anchor)?/<#anchor>
    • astro.config.mjs — registré el plugin en markdown.remarkPlugins. Aplica a TODA collection que use render() (projects + clients), no solo a /clients/[slug]. Ventaja: el body del electro-ia/README.md que referencia projects-hub ahora también linkea bien.
    • src/pages/projects/[slug].astro — convertí la línea {d.client} en <a href="/clients/<slug>/"> para navegar al cliente desde un proyecto.
    • src/pages/index.astro — convertí la tarjeta Clientes en link real con texto descriptivo.
    • src/pages/projects/index.astro — nav cross-section (← Inicio · Clientes → · Pendientes →).
    • Build: 48 páginas en ~10.4s (35 proyectos + listing + 8 clientes + personal + listing clientes + /pendientes + home). Verifiqué reescritura del body de clients/electrosystems.md — 6 links [projects-hub](../projects/projects-hub/README.md) ahora apuntan a /projects/projects-hub/.
    • Commit: 7361358 (“feat: vistas /clients y /clients/[slug] + remark plugin reescribe links”) pusheado.
  • Falta (Fase 1): dashboard / con métricas reales (top prioridades, proyectos por status global, decisiones recientes) — equivalente al INDEX.md. Después: infra poseidon (nginx + oauth2-proxy + GH Action deploy).

2026-05-22 (continuación tarde)

  • Pidió: “vamos ahora con el parser de pendientes”.
  • Hice:
    • src/lib/pendientes.tsrenderPendientes(raw) pasa el MD por marked con GFM, después tres post-procesos:
      1. Reescribe href="projects/<slug>.md" y href="projects/<slug>/README.md"/projects/<slug>/ (links del listing de PENDIENTES caen en las vistas individuales).
      2. Reescribe href="INDEX.md"/ (placeholder hasta que exista dashboard real).
      3. Convierte #NNN (3 dígitos, no precedido por " o word-char para no confundir con commits hex o atributos HTML) en <a class="hub-anchor">. Primera ocurrencia recibe id="pNNN", las demás solo href="#pNNN" — evita IDs duplicados (HTML inválido) cuando un #NNN aparece tanto en su sección como en “Cerrado reciente”. Helpers extra: countOpenPendings() y countResolvedRecently() con regex sobre el MD crudo (no requieren marked).
    • src/pages/pendientes.astroreadFileSync(process.cwd() + '/hub/PENDIENTES.md') en build-time (no hace falta collection porque es un singleton sin frontmatter). Header con contadores + mtime del archivo. Body con set:html={html} envuelto en prose dark:prose-invert. <style is:global> define .hub-anchor con borde dashed teal y :target highlight amarillo (light) / tabaco (dark).
    • src/pages/index.astro — convertí la tarjeta PENDIENTES en link real con contador real.
    • Build: 39 páginas en ~3.6s. Verifiqué que id="pNNN" quede único (todos los IDs aparecen una sola vez) y que los links href="/projects/<slug>/" se generen correctamente.
    • Commit: 9f7856d (“feat: vista /pendientes con anchors clickeables #NNN”) pusheado a sevaor/hub-web-viewer.
  • Falta (Fase 1): vista /clients/[slug] agrupando por client: + dashboard / con métricas reales + infra poseidon (nginx + oauth2-proxy + GH Action deploy).

2026-05-22

  • Pidió: “empieza con eso” — confirmación para arrancar Content Collections schema + render del primer proyecto antes de tocar poseidon/DNS.
  • Hice:
    • Auditoría de frontmatter de los 33 .md de root + folders electro-ia/ y projects-hub/ que usan README.md como entry. Detecté campos opcionales adicionales (aliases, deployed_at, end_customer, enlace, parent, related_projects, related, source, phase, host, public_url) y los incluí todos en el schema con .passthrough() por defensa.
    • src/content.config.ts — collection projects con glob({ pattern: ['*.md', '*/README.md', '!_*.md'], base: './hub/projects' }). Primer build falló porque puse ../hub/projects (Astro resuelve relativo al project root, no a content.config.ts); fix a ./hub/projects. Después cargó las 35 entries sin errores de Zod.
    • src/lib/projects.ts — helpers sortProjects() (priority→status→updated desc), ageMarker() (🕒 ≥7d / 🥶 ≥14d), statusClasses/priorityClasses (paleta tailwind con dark mode siempre emparejado por regla [[feedback-dark-mode-aesthetics]]).
    • src/components/Badge.astro — badge reutilizable con variant libre.
    • src/pages/projects/index.astro — listing agrupado por priority (high → medium → low), tarjetas con badges + marcador antigüedad + fecha de update. Header con contadores totales.
    • src/pages/projects/[slug].astrogetStaticPaths() usando data.project como slug. Header con badges + <dl> de frontmatter expandido (creado, actualizado, host, end_customer, enlace, parent linkeable, directorios, aliases, related_projects linkeables). Render del body MD con <Content /> envuelto en prose prose-slate dark:prose-invert + tweaks para code/pre.
    • Tailwind Typography v4 — instalado @tailwindcss/typography como devDep y registrado con @plugin "@tailwindcss/typography"; en src/styles/global.css (sintaxis Tailwind v4, no en tailwind.config.js).
    • src/pages/index.astro — convertí la tarjeta “Proyectos” en link real con contadores reales (getCollection para totales y activos).
    • Build: 38 páginas en ~3.5s (35 proyectos + listing + home + favicon). Verifiqué render de /projects/hub-web-viewer/ (h1 + body MD).
    • Commit y push: 61d6452 (“feat: Content Collection projects + vistas /projects y /projects/[slug]”) a sevaor/hub-web-viewer main. Autorizado per-sesión por Sergio.
  • Falta (Fase 1): parser de PENDIENTES.md con anchors #NNN, vista /clients/[slug], dashboard / con métricas, y la infra (nginx+oauth2-proxy en poseidon + GH Action deploy).
  • Estado al cierre: 35 proyectos navegables localmente con npm run dev. Pre-deploy real, falta SSO + dominio. Próximo paso natural: parser de PENDIENTES o vista de clientes (lo que Sergio priorice).

2026-05-21 (continuación tarde-noche)

  • Pidió: confirmó las 3 micro-decisiones de scaffold para arrancar Fase 1 MVP — dominio hub.electrosystemsnet.com, repo separado en ~/code/hub-web-viewer/ + remote sevaor/hub-web-viewer privado, y submodule de sevaor/sergio-hub (opción a).
  • Hice:
    • Gotcha de Node: create-astro@5.0.6 exige Node ≥22.12; la laptop tenía 18.19.1. Sergio escogió nvm sobre NodeSource. Instalé nvm v0.40.1 per-user (~/.nvm/), nvm install 22 → Node 22.22.3 LTS (npm 10.9.8), nvm alias default 22. .nvmrc=22 en el repo nuevo. Cada Bash subsecuente requiere source "$HOME/.nvm/nvm.sh" para activar la version — no se persiste cross-shell sin login interactivo.
    • Scaffold Astro: npm create astro@latest hub-web-viewer -- --template minimal --typescript strict --no-install --no-git --yes → Astro 6.3.7.
    • Tailwind v4: npx astro add tailwind --yes instala el setup nuevo vía @tailwindcss/vite (no PostCSS). En src/styles/global.css agregué @custom-variant dark (&:where(.dark, .dark *)); para tener dark mode forzable por clase (no solo media query).
    • Layout base (src/layouts/Layout.astro): <html lang="es" class="dark"> (dark mode default), paleta slate, min-h-screen, robots noindex/nofollow (precaución antes del SSO).
    • Página index placeholder (src/pages/index.astro): hero “Fase 1 MVP — scaffold” + grid 2x2 de tarjetas para las 3 vistas pendientes (PENDIENTES / Proyectos / Clientes) + tarjeta “Estado” en emerald.
    • git init -b main en el repo del viewer.
    • Submodule add del hub: bloqueado en primer intento por el classifier (URL inferida de memoria — buena fricción, no debería confiar en URLs adivinadas para integraciones); Sergio confirmó explícitamente git@github.com:sevaor/sergio-hub.git, ejecuté → submodule clonado en hub/ apuntando a 35eec8d (HEAD del hub al momento, incluye el commit 5c23415 de cierre de Fase 0).
    • Build sanity: npm run build → 1 página, 1.77s, OK.
    • Commit inicial local 540d17a (“chore: scaffold inicial Astro + Tailwind v4 + dark mode”) con co-author footer.
    • Repo remoto: gh repo create sevaor/hub-web-viewer --private --source=. --remote=origin --pushhttps://github.com/sevaor/hub-web-viewer (privado, ssh). Push inicial OK.
  • Estado al cierre: scaffold publicable, build verde, submodule del hub vivo. Sin lógica real aún — index es placeholder visual. Próximo paso natural: definir schema de Content Collections para hub/projects/*.md y parsear el primer proyecto en una vista de prueba.
  • Falta: todos los items abiertos en “En progreso” (Content Collections schema, parser PENDIENTES, las 4 vistas, nginx+oauth2-proxy en poseidon, GitHub Action deploy).

2026-05-21

  • Pidió: “Quiero empezar a darle al hub-web-viewer”.
  • Hice:
    • Releí PENDIENTES.md con la estructura nueva (IDs #NNN, header compacto, atomicidad por edición).
    • Surface tensión arquitectónica al escoger Sergio “self-hosted en poseidon” + “Google SSO” + “GitHub Action en push” — esos tres no encajan directo. Propuse 3 opciones para el bridge auth (CF Tunnel+Access, nginx+oauth2-proxy, solo WG sin SSO).
    • Sergio cerró las 5 decisiones marco: Astro / self-hosted poseidon / nginx+oauth2-proxy (Google SSO svalencia@e-electrosystems.com) / GitHub Action en push (via SSH a poseidon) / alcance v1 PENDIENTES+Proyectos+Clientes.
    • Cerré Fase 0 (#128) en el doc del proyecto + en PENDIENTES.md atómicamente.
    • Pasé a “En progreso” la lista de 8 sub-pasos de Fase 1.
  • Falta: arrancar Fase 1 — empezando por (a) dominio público para el viewer, (b) ubicación del repo ~/code/hub-web-viewer/, (c) scaffold Astro + Tailwind. Próxima sesión sigue desde ahí.

2026-05-20

  • Pidió: ligar pendientes del hub a software tipo Jira o custom para visualizar en web; sincronización siempre con los MD.
  • Hice:
    • Surface tradeoffs antes de ejecutar (regla [[feedback-challenge-assumptions]]): cuestioné si necesitaba bidireccional (Jira) o solo visualizar (static site). Comparé Jira/Linear/Trello, GitHub Projects, Obsidian Publish, Custom app vs static site.
    • Sergio confirmó: solo visualizar.
    • Camino elegido: static site read-only que renderiza projects/*.md + PENDIENTES.md + INDEX.md con build determinístico. Prioridad media.
    • Capturé proyecto hub-web-viewer.md con contexto, 5 decisiones marco abiertas (stack, hosting, auth, trigger rebuild, alcance v1), Fases 0/1/2/3 estructuradas, recomendaciones por decisión, estructura propuesta y notas de seguridad.
  • Falta: cerrar las 5 decisiones marco en próxima sesión antes de cualquier scaffold.