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
pusha 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/— commit540d17a, 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, commit61d6452. - (2026-05-22) #291 Vista PENDIENTES por prioridad con anchors
#NNN—src/pages/pendientes.astro+src/lib/pendientes.ts, commit9f7856d. - (2026-05-22) #292 Vista proyecto individual —
src/pages/projects/[slug].astro, commit61d6452. - (2026-05-21) #293 Dark mode —
@custom-variant dark+<html class="dark">, commit540d17a. - (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 (
/loglisting +/log/[date]individual), commit989495b. - (2026-05-26) #295 Filtros antigüedad clickeables en
/projects(Todos/Fresco/Stale/Frozen via hash URL), commit162e211. - (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/*.mdcon proyectos vinculados) —src/pages/clients/[slug].astro+ remark plugin de reescritura, commit7361358. - (2026-05-22) #299 Badge / chip por status (active / paused / backlog / done / in-progress / ready-to-build) —
statusClasses+Badge.astroen /projects/index y /projects/[slug], commits61d6452+56687e2.
Fase 3 — Opcional / futuro
- (2026-05-28) #300 Vista
/calendariocon “Próximas fechas” — derivada auto de losprojects/*.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. Commitee1c5b7pusheado → 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 ensrc/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 enANTIGRAVITY.md; el viewer aún no lo deriva. - (2026-05-27) #303 PWA installable en celular — manifest + SW manual (sin plugin,
@vite-pwa/astrosolo 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 200type=basic; 401 nunca entra al cache (Basic Auth compat). Commit20a15e2pusheado → 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 + remotesevaor/hub-web-viewerprivado en GitHub, separado del hub. Commit inicial540d17apusheado. - (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-hubenhub/. Confirmado al apuntar35eec8d(el HEAD del hub al momento del clone, que ya incluye el commit5c23415de cierre de Fase 0). - (2026-05-21) Página index placeholder con tarjetas para PENDIENTES / Proyectos / Clientes / Estado.
npm run buildverde en 1.77s. - (2026-05-21) Gotcha capturado:
create-astro@5exige Node ≥22.12; resuelto con nvm + Node 22.22.3 LTS (per-user, no toca apt)..nvmrc=22en 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 desdeupdated:). 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. Commit61d6452pusheado. - (2026-05-22) Parser de
hub/PENDIENTES.md—src/lib/pendientes.tsconmarked(gfm), reescribeprojects/<slug>(.md|/README.md)→/projects/<slug>/, convierte#NNNen anchors clickeables conid="pNNN"único (skip duplicados para HTML válido) y:targethighlight CSS. Páginasrc/pages/pendientes.astroleehub/PENDIENTES.mdconreadFileSyncen build-time, header con conteo (211 anchors, 100+ pendientes abiertos, 30+ cerrados recientes). Commit9f7856d. - (2026-05-22) Vista
/clients/[slug]— collectionclientscon Zod (client,name,type,created,updated). Listing ordenado por activos descendente con conteos por status. Pseudo-clientepersonalsintetizado para los 5 proyectos conclient: personal(no hayclients/personal.md). Vista individual con header + frontmatter + grid de proyectos vinculados + render MD del cliente. Remark pluginremark-hub-links.mjsregistrado enastro.config.mjsreescribeprojects/<slug>(.md|/README.md),clients/<slug>.md,PENDIENTES.md,INDEX.md(con../opcional) a las rutas del viewer — aplica a TODOS losrender()de collections, no solo aquí. Badge client en/projects/[slug]ahora linkea a/clients/<slug>/. Build verde 48 páginas. Commit7361358. - (2026-05-22) Dashboard
/— 4 KPI cards (proyectos, pendientes por prioridad, clientes, cerrados visibles), top prioridades (high+active), actividad reciente (top 6 porupdated: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 submodulehub/estaba congelado en35eec8ddesde el clone; lo bumpée a1dc958d(latest) — en el deploy GH Action habrá que corrergit submodule update --remote --merge hubantes del build, o nunca verá hub más nuevo. HelpersextractCerradoReciente()ycountOpenByPriority()agregados asrc/lib/pendientes.ts. Build verde 49 páginas. Commit2a3bea4. - (2026-05-22) Deploy en Cloudflare Workers + Basic Auth — live (terminó siendo Workers, no Pages; Basic Auth, no Zero Trust):
- Worker
hub-web-viewerconwrangler.toml+worker.js(Basic Auth middleware conrun_worker_first = true+ binding[assets]adist/). - GH Action en
sevaor/sergio-hub/.github/workflows/trigger-viewer-rebuild.ymlllama deploy hook en cada push a main (commit7550d80). - 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.mddel viewer. Commite4cb561.
- Worker
- (2026-05-22) Custom domain — descartado. La zone
electrosystemsnet.comvive 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) delegarhub.electrosystemsnet.comvia NS records en OCI apuntando a CF nameservers, (B) migrar la zone entera a CF, (C) quedarse con la URL*.workers.devlarga. 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: agregarhub.electrosystemsnet.comcomo zone en CF, copiar los 2 nameservers que asigne, agregarlos como record NS para el namehuben 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-fileconsvalencia@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 proyectoelectrosystems, configurar Nginx+SSL enreverse-proxyy automatizar despliegues con GitHub Actions (“igual que holbox y medicinas”). - Hice:
- Infraestructura: Validé que
val-softtenía suficientes recursos. Documenté sus características creandoservers/val-soft/README.mdy actualizandoINVENTORY.mden el repositorioelectrosystems. - 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 parahub.val-soft.comy documenté el sitio enservers/reverse-proxy/README.md. - CI/CD Automation:
- Instalé el servicio
actions.runnerdirectamente enval-soft. - Creé el workflow
.github/workflows/deploy.ymlpara correr en elself-hostedrunner. - 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+rsynclocales) que completa todo el pipeline de despliegue en menos de 2 minutos tras unpush.
- Instalé el servicio
- Infraestructura: Validé que
- Estado al cierre:
hub-web-viewersirviendo porhub.val-soft.comusando 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-DDdentro 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×12pxgrid-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
ComplexityLeveltype + campocomplexityenOpenPending.COMPLEXITY_REparsea🤖 alto|medio|ligerodel bullet.stripDescriptionelimina 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-levelen cada<li>, CSS:has()que ocultasection-groupyproject-cardvacíos, script JS con sync de hash URL (#level=alto).- Commit
916873fpusheado → 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
/calendariodecí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()ensrc/lib/fechas.tsderiva “hoy” conIntl.DateTimeFormat('en-CA', { timeZone: 'America/Ciudad_Juarez' })(fallbackAmerica/Denver, mismo horario de montaña con DST de EE.UU.; último recurso UTC−6h). Usado en/calendarioy 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. Commitca89166→ 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 fechasde PENDIENTES.md — la misma que se quedó stale y originó la queja. El calendario nuevo deriva las fechas directo de los📅 YYYY-MM-DDen los bodies de losprojects/*.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)leeprojects/<slug>.md+<slug>/README.mddirecto del filesystem (mismo criterio quecontent.config.ts). Gotcha: nientry.bodynientry.filePathdel glob loader de Astro 6.3.7 son fiables para los entries*/README.md—entry.bodyvenía vacío yentry.filePathveníaundefined, así que #256 (holbox, único fechado en un README) no aparecía. Leer del disco lo resolvió.collectDatedPendings(bodies)extrae los- [ ]con📅 fechaconcreta, deduplica por#NNN(prefiriendo el proyecto canónico sobre design-docs satélite conparent:), 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 usacollectDatedPendings(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.mddentro de un comentario JSDoc cerró el bloque/* */antes de tiempo y tronó esbuild — reescrito sin*/. - Deploy: commit
ee1c5b7pusheado asevaor/hub-web-viewermain (rebase sobre 2 commits remotos que bumpeaban el submódulo; re-bumpeadohuba HEAD4c914cc). 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) usamarked.parseInline(no parseBlock — preserva inline en<li>sin envolver en<p>) + reescribe links del hub + convierte#NNNa<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#NNNde**#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):matchAllsobre### [\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 (projectSlugsset) — evita mostrar slugs huérfanos del PENDIENTES.md sin MD propio.
- ”⏰ Próximas fechas” + ”🔒 Disparo condicional” en grid 2-col después de “Top prioridades / Actividad reciente”. Card ámbar (fechas) + violeta (condicional), filtra entries
- 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-checador2026-08-14,gi-siptrunks-replacementsin fecha,cobros-recurrentes2027-03-01). Disparo condicional con 2 (cpe-benito-juarezcap 200Mbps,adfsa-migrationMFC-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.astrorediseñado con filtros client-side:- Helper local
ageBucket(updated)que clasifica comofresh(<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>conhref="#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.hashcon regexage=(\w+), aplica filtro, actualiza el conteo “(N)” de cada section al filtrado dinámicamente, listeners en los chips cone.preventDefault()+history.replaceStatepara evitar scroll-to-top del navegador en cambio de hash, listenerhashchangepara 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.
- Helper local
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-ageen 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 endist/, (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.
- Sergio: validar visualmente click en chips + deeplinks
- 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()leehub/log/*.mdfiltrado por regexYYYY-MM-DD.md(los archivos del hub no tienen frontmatter, solo h1# YYYY-MM-DDy cuerpo), devuelve{date, filename, bytes, preview}ordenado desc.loadLog(date)yrenderLog(raw)para vista individual: correnmarkedcon 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 collectionrender()así que el plugin remark global no aplica). Decisión clave: los#NNNen 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óny captura las primeras 3 líneas significativas; fallback al primer texto no-heading si no encuentra. Limpia**,*,`(no underscores — eso rompe identifiers comoarray_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].astro—getStaticPathsdesdelistLogs(). 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:targetporque 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). Marcadodata-pagefind-ignorepor 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 gridsm:grid-cols-2de 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
#NNNreescritos correctamente verificado endist/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.jsonaú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 | bashde nvm. Sergio escogió Dockernode: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 delgit submodule update --initno tenía credenciales en esta máquina) y bumpeado de1dc958d→b75b1be(HEAD actual del hub). - Hice:
- Pre-fix de schema (no relacionado a pagefind pero bloqueaba el build):
- Proyectos nuevos del hub usan
status: in-progressyready-to-build, fuera del enumactive|paused|backlog|doneque teníasrc/content.config.ts. Cambiéstatusaz.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
statusClassesparain-progress(verde como active) yready-to-build(violeta), másstatusFallbackClassslate para cualquier status desconocido futuro. Replacé?? ''por?? statusFallbackClassen las 4 pages que usan badges (preserva la regla [[feedback-dark-mode-aesthetics]]). STATUS_ORDERampliado con los 2 nuevos en posición lógica;sortProjectsahora usastatusRank()que asignalengtha desconocidos (van al final, no al inicio como hacíaindexOf -1).- Gotcha del glob:
electrosystems-network-map/README.mdmatcheaba*/README.mdpero NO es el entry del proyecto (el entry eselectrosystems-network-map.mden root) — es un sub-doc de la KB. Sin frontmatter de proyecto. Fix: agregué!electrosystems-network-map/README.mdal pattern. Si más adelante aparece otro caso similar, considerar custom loader que solo trate<dir>/README.mdcomo entry cuando no hay sibling<dir>.mden root.
- Proyectos nuevos del hub usan
- Pagefind instalado como devDep (
pagefind@1.5.2). Cambiépackage.jsonbuild script aastro build && pagefind --site dist --output-subdir pagefind. Conservébuild:astroybuild:indexpor 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">. Eldata-pagefind-ignoreevita 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. Enastro 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-primarysky-400,--pagefind-ui-textslate-900/200,--pagefind-ui-backgroundwhite/slate-950,--pagefind-ui-borderslate-300/700,--pagefind-ui-tagslate-100/800) con set light + overridehtml.dark .hub-searchpara dark mode. Drawer absoluto conmax-height: 70vhoverflow-y auto, posicionado conwidth: 32remymargin-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.
- Pre-fix de schema (no relacionado a pagefind pero bloqueaba el build):
- 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 deid="search",data-pagefind-body,data-pagefind-ignoreen HTML buildeado de/,/projects/,/pendientes/,/clients/. WASM detectado comoes(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(commit127fa74). Sergio conectó el repo en el dashboard de CF. - Gotcha #1 (clone falla): primer build tronó en “updating repository submodules”. Causa:
.gitmodulescon SSH URL (git@github.com:...), CF Pages/Workers auto-corregit submodule update --initdurante el clone con auth HTTPS via GH App. Fix: cambié.gitmodulesa HTTPS (commit6da05c6) + le dije a Sergio que pusieragit config --global url."git@github.com:".insteadOf "https://github.com/"en su~/.gitconfiglocal 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 permitewrangler pages deploy. Fix: Sergio creó API token custom conPages:Edity lo metió como secretCLOUDFLARE_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.tomlcon[assets]apuntando adist/(commitee77186), cambié Deploy command anpx wrangler deploy(nowrangler pages deploy), agreguéWorkers Scripts:Edital 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: comparaAuthorization: Basicdecodificado contraenv.AUTH_USER/env.AUTH_PASSconsafeEqual()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). Commitef070ba.
- Escribí
- Gotcha #5 (YAML frontmatter trona el build): próximo rebuild trona en
holbox.mdlínea 10:185 conbad 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 (commit686c1aben el hub). Memoria nuevareference_yaml_frontmatter_colon_gotcha.mdpara prevenir recurrencia. - Gotcha #6 (Build secrets vs Runtime secrets): Sergio configuró
AUTH_USER/AUTH_PASSpero 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.mdcon la realidad de lo que funcionó (Workers + Basic Auth) + 7 gotchas reales documentados. Borrédocs/cloudflare-pages-setup.md(obsoleta) ydocs/trigger-viewer-rebuild.yml.template(el GH Action ya vive ensevaor/sergio-hub). Commite4cb561.
- Reescribí
- Setup inicial: documenté Pages + Zero Trust en
- 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 servepor 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 SSHgit@github.com:sevaor/sergio-hub.git) ygit config --global(sin URL rewrites). Gotcha #1: CF Pages auth via HTTPS, no SSH — solución esgit config --global url."https://github.com/".insteadOf "git@github.com:"en el build command (no toco.gitmodulespara no romper workflow local). Gotcha #2: CF Pages corregit submodule update --initpor default (commit pinneado, no HEAD) — solución--remote --mergeen 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.mden el viewer con paso a paso: conectar repo + autorizar GH App también parasevaor/sergio-hub(sin esto el build truena), build command exacto, env varNODE_VERSION=22.12.0, Access policy con Google IdP + email allowlist, custom domainhub.electrosystemsnet.comopcional, sección Deploy Hook con el handoff “pásame la URL y commiteamos el GH Action en el hub”.docs/trigger-viewer-rebuild.yml.templatecon el GH Action listo (curl al${{ secrets.CF_DEPLOY_HOOK }}en cada push a main del hub). Vive endocs/temporalmente — su lugar final essevaor/sergio-hub/.github/workflows/trigger-viewer-rebuild.ymluna vez que tenga la URL del hook.- Commit:
127fa74(“docs: setup de Cloudflare Pages + Access”) pusheado.
- Surface de 3 opciones con tradeoffs: (A) Cloudflare Pages + Access — público con SSO, sin infra; (B) local-only
- Falta:
- Sergio: ejecutar los pasos 1-7 de
docs/cloudflare-pages-setup.mden el dashboard CF. Crear Deploy Hook y pasarme la URL. - Yo: commitear
.github/workflows/trigger-viewer-rebuild.ymlensevaor/sergio-hubcon la URL en secret.
- Sergio: ejecutar los pasos 1-7 de
- 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)ycountOpenByPriority(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.astrorediseñ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 statusenhub-web-viewer/hubapuntaba a35eec8d(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 a1dc958d(latest, incluyeelectro-ia + backups-infrainventory bridge +machine-parityproyecto + todos los míos). Implicación operativa: la GH Action de deploy debe ejecutargit submodule update --remote --mergeantes delnpm run buildo 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-paritytambién). - Commit:
2a3bea4(“feat: dashboard / con KPIs, top prioridades, stale, recientes, cerrados”) pusheado asevaor/hub-web-viewer. Incluye bump del submodule a 1dc958d.
- Falta (Fase 1): infra poseidon — nginx + oauth2-proxy + certbot para
hub.electrosystemsnet.com(allowlistsvalencia@e-electrosystems.com) + GitHub Action que SSH-a a poseidon y corregit 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 conclients/del hub y con/projects/. - Hice:
- Auditoría: 7 archivos en
clients/(benito-juarez, deportes-campeon, electrosystems, greco-cell, holbox, joyerias-meza, telcel) — no hayclients/personal.md, pero 5 proyectos tienenclient: personalen su frontmatter. Decidí sintetizar un pseudo-cliente “personal” con badgepseudo-clientey nota explicativa. src/content.config.ts— agregué collectionclientsconclientSchema(Zod:client,name,type,created,updated).src/lib/clients.ts—buildClientGroups(clients, projects)que cruza el frontmatterclient:de los proyectos con las entries declients/. Acumula proyectos huérfanos (slug que no existe enclients/) y, si el slug espersonal, lo etiqueta comoisPersonal=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) + badgepseudocuando aplica.src/pages/clients/[slug].astro—getStaticPathsdesdebuildClientGroups. Header con badges,<dl>de frontmatter, grid de proyectos vinculados consortProjects(), después<Content />del body MD. SiisPersonal=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 (sinunist-util-visitpara no agregar deps). Detecta linkslink.urlcon 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 enmarkdown.remarkPlugins. Aplica a TODA collection que userender()(projects + clients), no solo a /clients/[slug]. Ventaja: el body del electro-ia/README.md que referenciaprojects-hubahora 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.
- Auditoría: 7 archivos en
- 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.ts—renderPendientes(raw)pasa el MD pormarkedcon GFM, después tres post-procesos:- Reescribe
href="projects/<slug>.md"yhref="projects/<slug>/README.md"→/projects/<slug>/(links del listing de PENDIENTES caen en las vistas individuales). - Reescribe
href="INDEX.md"→/(placeholder hasta que exista dashboard real). - 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 recibeid="pNNN", las demás solohref="#pNNN"— evita IDs duplicados (HTML inválido) cuando un#NNNaparece tanto en su sección como en “Cerrado reciente”. Helpers extra:countOpenPendings()ycountResolvedRecently()con regex sobre el MD crudo (no requieren marked).
- Reescribe
src/pages/pendientes.astro—readFileSync(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 conset:html={html}envuelto enprose dark:prose-invert.<style is:global>define.hub-anchorcon borde dashed teal y:targethighlight 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 linkshref="/projects/<slug>/"se generen correctamente. - Commit:
9f7856d(“feat: vista /pendientes con anchors clickeables #NNN”) pusheado asevaor/hub-web-viewer.
- Falta (Fase 1): vista
/clients/[slug]agrupando porclient:+ 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
.mdde root + folderselectro-ia/yprojects-hub/que usanREADME.mdcomo 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— collectionprojectsconglob({ 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— helperssortProjects()(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 convariantlibre.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].astro—getStaticPaths()usandodata.projectcomo 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 enprose prose-slate dark:prose-invert+ tweaks para code/pre.- Tailwind Typography v4 — instalado
@tailwindcss/typographycomo devDep y registrado con@plugin "@tailwindcss/typography";ensrc/styles/global.css(sintaxis Tailwind v4, no entailwind.config.js). src/pages/index.astro— convertí la tarjeta “Proyectos” en link real con contadores reales (getCollectionpara 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]”) asevaor/hub-web-viewermain. Autorizado per-sesión por Sergio.
- Auditoría de frontmatter de los 33
- 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/+ remotesevaor/hub-web-viewerprivado, y submodule desevaor/sergio-hub(opción a). - Hice:
- Gotcha de Node:
create-astro@5.0.6exige Node ≥22.12; la laptop tenía 18.19.1. Sergio escogió nvm sobre NodeSource. Instalénvm v0.40.1per-user (~/.nvm/),nvm install 22→ Node 22.22.3 LTS (npm 10.9.8),nvm alias default 22..nvmrc=22en el repo nuevo. Cada Bash subsecuente requieresource "$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 --yesinstala el setup nuevo vía@tailwindcss/vite(no PostCSS). Ensrc/styles/global.cssagregué@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 enhub/apuntando a35eec8d(HEAD del hub al momento, incluye el commit5c23415de 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 --push→https://github.com/sevaor/hub-web-viewer(privado, ssh). Push inicial OK.
- Gotcha de Node:
- 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/*.mdy 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.
- Releí PENDIENTES.md con la estructura nueva (IDs
- 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.mdcon build determinístico. Prioridad media. - Capturé proyecto
hub-web-viewer.mdcon 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.