cis-style · hub

cis-style · hub · kit v9 · f417275

Patrones de página

Un componente resuelve una pieza; un patrón resuelve una situación que se repite en los sitios del grupo. Cada patrón trae cuándo usarlo, el ejemplo vivo armado con las clases reales del kit y la regla que lo respalda.

Las reglas se citan por identificador (H01 a H27, cis-modelo/modelo/reglas.py). La cita convierte la revisión en medición: una inconsistencia deja de ser opinión y pasa a ser una violación con evidencia.

1 · Gate SSO

Cuándo. En todo sitio privado del grupo. El sitio nunca implementa login propio: Caddy hace forward_auth contra cis-auth (Authentik) y la aplicación recibe la identidad ya resuelta por OIDC con core-auth-lib. Un formulario de usuario y clave fuera de auth.innovacionsantiago.cl viola H19.

La portada de acceso, según la spec de auth.innovacionsantiago.cl:

<div class=login-card style="max-width:360px">
  <svg width="34" height="34" viewBox="0 0 200 200" fill="none" stroke="currentColor" style="display:block;margin:0 auto 14px"><circle cx="100" cy="100" r="92" stroke-width="4"/><circle cx="100" cy="100" r="56" stroke-width="18"/></svg>
  <div class=login-card__title style="text-align:center">Entrar a cis-admin</div>
  <button class=login-card__btn>Continuar con cis-auth</button>
  <p class=login-card__meta>Una sola cuenta para todo el grupo<br>auth.innovacionsantiago.cl</p>
</div>

El chip de sesión del chrome, con sus dos estados (el estado se decide server-side o en build, cero JS):

<div style="display:flex;gap:22px;align-items:center;flex-wrap:wrap">
  <div class=gr-login data-state=anon>
    <a class=gr-login__enter href="/oauth/login">Entrar</a>
  </div>
  <div class=gr-login data-state=auth>
    <a class=gr-login__user href="/cuenta"><span class=gr-login__dot></span><span class=gr-login__name>M. Illanes</span></a>
    <a class=gr-login__exit href="/oauth/logout">Salir</a>
  </div>
</div>

El gate vive en el proxy, fuera de la aplicación:

panel.innovacionsantiago.cl {
    forward_auth 127.0.0.1:9000 {
        uri /outpost.goauthentik.io/auth/caddy
        copy_headers X-Authentik-Username X-Authentik-Email X-Authentik-Groups
    }
    reverse_proxy 127.0.0.1:8080
}
<pre style="margin:0;font:12px/1.7 var(--font-mono);color:var(--ink);border:1px solid var(--line);background:var(--paper-raised);padding:14px 16px;overflow-x:auto">panel.innovacionsantiago.cl {
    forward_auth 127.0.0.1:9000 {
        uri /outpost.goauthentik.io/auth/caddy
        copy_headers X-Authentik-Username X-Authentik-Email X-Authentik-Groups
    }
    reverse_proxy 127.0.0.1:8080
}</pre>

H19 · «La autenticación se integra a cis-auth por OIDC con core-auth-lib. No se reinventa login ni JWT por servicio.» Severidad alta. Y una trampa medida: el 302 al login prueba que el sitio está cerrado al anónimo, no que esté bien acotado. La aplicación en Authentik necesita binding a un grupo; publicada sin bindings queda abierta a cualquier cuenta. Fuente: cis-modelo/modelo/reglas.py · CANON.md §Auth.

2 · Panel de verificación de documentos

Cuándo. En todo documento que sale del grupo en papel o PDF: actas, certificados, cartas. El documento imprime un código C-XXXXXX y el panel responde si el archivo que tienes al frente es auténtico. Lo sirve cis-verify en docs.circulodesantiago.cl/v/{numero}.

Documento válido: código, hash completo y la URL del validador escrita:

Documento verificado auténtico
C-000482
SHA-256
9f2a41c8d0b3e6a7551f0c98d2ab34c6e1f7a8b90d3c5e2f4a6b8c0d1e2f3a4b
Emitido
2026-08-14 · Compañía de Innovación de Santiago SpA
Verifica en
docs.circulodesantiago.cl/v/C-000482
<div style="max-width:540px;border:1px solid var(--line);background:var(--paper-raised);padding:22px 24px">
  <div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:14px">
    <span class=eyebrow>Documento verificado</span>
    <span class="badge badge--ok">auténtico</span>
  </div>
  <div style="font:700 30px var(--font-mono);letter-spacing:.02em;margin-bottom:16px">C-000482</div>
  <dl style="margin:0;display:grid;grid-template-columns:92px 1fr;gap:8px 16px;font-size:12.5px">
    <dt style="font:10.5px var(--font-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ink-muted)">SHA-256</dt>
    <dd style="margin:0;font:11.5px var(--font-mono);word-break:break-all;color:var(--ink)">9f2a41c8d0b3e6a7551f0c98d2ab34c6e1f7a8b90d3c5e2f4a6b8c0d1e2f3a4b</dd>
    <dt style="font:10.5px var(--font-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ink-muted)">Emitido</dt>
    <dd style="margin:0">2026-08-14 · Compañía de Innovación de Santiago SpA</dd>
    <dt style="font:10.5px var(--font-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ink-muted)">Verifica en</dt>
    <dd style="margin:0;font:11.5px var(--font-mono)">docs.circulodesantiago.cl/v/C-000482</dd>
  </dl>
</div>

Código que no existe: el rechazo dice qué pasó y qué hacer:

Verificación no corresponde
C-000999

El código no corresponde a ningún documento emitido. Revisa que lo copiaste completo, con la letra y los seis dígitos.

<div style="max-width:540px;border:1px solid var(--line);background:var(--paper-raised);padding:22px 24px">
  <div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:12px">
    <span class="eyebrow eyebrow--muted">Verificación</span>
    <span class="badge badge--err">no corresponde</span>
  </div>
  <div style="font:700 24px var(--font-mono);letter-spacing:.02em;margin-bottom:10px;color:var(--ink-muted)">C-000999</div>
  <p style="margin:0;font-size:13px;color:var(--ink-muted)">El código no corresponde a ningún documento emitido. Revisa que lo copiaste completo, con la letra y los seis dígitos.</p>
</div>

H14 · H26 · El código y el hash van en JetBrains Mono, la voz secundaria de la casa (H14). El hash SHA-256 se muestra completo: truncado no verifica nada. La URL del validador va escrita, porque el papel no tiene enlaces. El copy del rechazo cumple la norma de prosa (H26). Fuente: cis-modelo/modelo/reglas.py · cis-verify.

3 · Sello de frescura

Cuándo. Bajo toda cifra medida que muestras: saldo, uptime, conteo. Una cifra sin fecha ni fuente es una opinión con dígitos. El sello declara cuándo se midió, con qué, y si el dato es fresco o cache.

119 hosts servidos

medido 2026-08-20 14:32 · caddy adapt + dig · refresco activo

412 documentos emitidos

al 2026-08-19 09:10 · cache diario · stale sin refrescar

<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:16px;max-width:580px">
  <div>
    <div class=stat>
      <span class=stat__value>119</span>
      <span class=stat__label>hosts servidos</span>
    </div>
    <p style="margin:6px 2px 0;font:10.5px var(--font-mono);letter-spacing:.04em;color:var(--ink-soft)">medido 2026-08-20 14:32 · caddy adapt + dig · refresco activo</p>
  </div>
  <div>
    <div class=stat>
      <span class=stat__value>412</span>
      <span class=stat__label>documentos emitidos</span>
    </div>
    <p style="margin:6px 2px 0;font:10.5px var(--font-mono);letter-spacing:.04em;color:var(--ink-soft)">al 2026-08-19 09:10 · cache diario · <span class="badge badge--warn">stale</span> sin refrescar</p>
  </div>
</div>

H13 (mismo espíritu) · «La versión que un sitio carga de verdad coincide con la que el registro declara»: lo declarado se contrasta con lo medido. El sello exige tres datos: timestamp, fuente (URL, query o comando) y si hubo refresco activo o cache. Con más de 5 minutos de edad, el dato se refresca o se marca stale explícito. Fuente: CLAUDE.md del workspace, «Stale-data y freshness checks» · cis-modelo/modelo/reglas.py.

4 · Noindex en sitios técnicos

Cuándo. En todo host de clase servicio o infraestructura: paneles, hubs de documentación, APIs con vista HTML. Doble capa: la meta en el documento y la cabecera X-Robots-Tag en el proxy, porque la meta no cubre JSON ni archivos.

<meta name=robots content="noindex, nofollow">
status.innovacionsantiago.cl {
    header X-Robots-Tag "noindex, nofollow"
    reverse_proxy 127.0.0.1:8080
}
<div style="display:grid;gap:12px;max-width:640px">
  <pre style="margin:0;font:12px/1.7 var(--font-mono);color:var(--ink);border:1px solid var(--line);background:var(--paper-raised);padding:14px 16px;overflow-x:auto">&lt;meta name=robots content="noindex, nofollow"&gt;</pre>
  <pre style="margin:0;font:12px/1.7 var(--font-mono);color:var(--ink);border:1px solid var(--line);background:var(--paper-raised);padding:14px 16px;overflow-x:auto">status.innovacionsantiago.cl {
    header X-Robots-Tag "noindex, nofollow"
    reverse_proxy 127.0.0.1:8080
}</pre>
</div>

H11 · «Todo sitio técnico responde noindex y no se sirve al anónimo si publica algo interno.» Se comprueba midiendo X-Robots-Tag en cada host de clase servicio o infraestructura. Complementa H05: la infraestructura no lleva marca ni namebrand. Fuente: cis-modelo/modelo/reglas.py · CATALOGO.md §2 · incidente del 2026-05-29, donde un crawler encontró un endpoint técnico indexado y drenó la cuota que había detrás.

5 · Estados vacíos

Cuándo. Cuando la vista existe y los datos no: primera visita, filtro sin coincidencias, cola ya procesada. El peor estado vacío es la página en blanco que no explica nada.

Filtro sin coincidencias, con la acción que lo deshace:

Sin resultados

Ningún documento coincide con el filtro «actas 2025».

<div style="border:1px solid var(--line);background:var(--paper-raised);padding:40px 24px;text-align:center;max-width:560px">
  <p style="margin:0 0 6px;font:10.5px var(--font-mono);letter-spacing:.12em;text-transform:uppercase;color:var(--ink-soft)">Sin resultados</p>
  <p style="margin:0 0 16px;font-size:13.5px;color:var(--ink-muted)">Ningún documento coincide con el filtro «actas 2025».</p>
  <button class="btn btn--ghost btn--sm">Limpiar filtro</button>
</div>

Primer uso: declara cuándo van a aparecer los datos:

Aún sin datos

La primera medición corre esta noche a las 03:00. Vuelve mañana.

<div style="border:1px solid var(--line);background:var(--paper-raised);padding:40px 24px;text-align:center;max-width:560px">
  <p style="margin:0 0 6px;font:10.5px var(--font-mono);letter-spacing:.12em;text-transform:uppercase;color:var(--ink-soft)">Aún sin datos</p>
  <p style="margin:0;font-size:13.5px;color:var(--ink-muted)">La primera medición corre esta noche a las 03:00. Vuelve mañana.</p>
</div>

H16 · H26 · La caja vacía lleva borde uniforme de 1 px, sin barrita de acento (H16). El copy dice qué falta y qué puedes hacer, en una frase, sin ilustraciones ni disculpas (H26). Si sabes cuándo van a aparecer los datos, dilo: es el sello de frescura aplicado al vacío. Fuente: cis-modelo/modelo/reglas.py · core/prosa/NORMA.md.

6 · Tabla de estado con badge

Cuándo. En paneles que listan servicios, trabajos o documentos con su estado actual. Es la forma canónica de responder «¿está todo bien?» de un vistazo.

ServicioEstadoMedidoLatencia
cis-authen línea14:32:0538 ms
cis-verifyen línea14:32:0551 ms
datos-apidegradado14:32:06412 ms
panel-leden falla14:31:40·
<table class=table>
  <thead><tr><th>Servicio</th><th>Estado</th><th>Medido</th><th style="text-align:right">Latencia</th></tr></thead>
  <tbody>
    <tr><td>cis-auth</td><td><span class="badge badge--ok">en línea</span></td><td style="font-family:var(--font-mono);font-size:12px">14:32:05</td><td class=num>38 ms</td></tr>
    <tr><td>cis-verify</td><td><span class="badge badge--ok">en línea</span></td><td style="font-family:var(--font-mono);font-size:12px">14:32:05</td><td class=num>51 ms</td></tr>
    <tr><td>datos-api</td><td><span class="badge badge--warn">degradado</span></td><td style="font-family:var(--font-mono);font-size:12px">14:32:06</td><td class=num>412 ms</td></tr>
    <tr><td>panel-led</td><td><span class="badge badge--err">en falla</span></td><td style="font-family:var(--font-mono);font-size:12px">14:31:40</td><td class=num>·</td></tr>
  </tbody>
</table>

base/components.css · El estado va como texto dentro del badge, nunca color solo: lo exige el daltonismo y lo agradece el grep. Las cifras van en td.num, mono con cifras tabulares. La columna «Medido» es el patrón 3 aplicado a tablas: una fila sin timestamp reporta un estado que no puedes fechar. Fuente: base/components.css (.table, .badge) · CLAUDE.md del workspace.

7 · Fila de stats de dashboard

Cuándo. En la cabecera de un dashboard: las tres a cinco cifras que resumen el sistema antes de cualquier detalle.

119hosts
27reglas
22automáticas
3violaciones abiertas

medido 2026-08-20 14:32 · cis-modelo · una sola corrida para toda la fila · refresco activo

<div>
  <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px">
    <div class=stat><span class=stat__value>119</span><span class=stat__label>hosts</span></div>
    <div class=stat><span class=stat__value>27</span><span class=stat__label>reglas</span></div>
    <div class=stat><span class=stat__value>22</span><span class=stat__label>automáticas</span></div>
    <div class=stat><span class=stat__value>3</span><span class=stat__label>violaciones abiertas</span></div>
  </div>
  <p style="margin:8px 2px 0;font:10.5px var(--font-mono);letter-spacing:.04em;color:var(--ink-soft)">medido 2026-08-20 14:32 · cis-modelo · una sola corrida para toda la fila · refresco activo</p>
</div>

H14 · La stat es la tarjeta de una sola cifra: valor mono pesado, etiqueta mono en mayúsculas con tracking. Si toda la fila sale de la misma corrida, comparte un sello de frescura; si mezcla fuentes, cada stat lleva el suyo. Fuente: base/components.css (.stat) · cis-modelo/modelo/reglas.py.

8 · Aviso y nota

Cuándo. Para contexto que el lector puede necesitar (nota), condiciones que exigen acción pronto (atención) y fallas activas (error). Siempre dentro del flujo del contenido, sin flotar ni tapar.

Nota El validador acepta el código con o sin guion.
Atención El certificado vence el 2026-09-01. Renueva antes del 25 de agosto.
Error La sonda no alcanza el servicio desde las 14:10. Revisa el runbook antes de reiniciar.
<div style="display:grid;gap:12px;max-width:560px">
  <div style="border:1px solid var(--line);background:var(--accent-soft);padding:13px 16px;font-size:13px;line-height:1.55">
    <b style="display:block;margin-bottom:4px;font:600 10.5px var(--font-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--accent)">Nota</b>
    El validador acepta el código con o sin guion.
  </div>
  <div style="border:1px solid color-mix(in srgb, var(--warn) 30%, transparent);background:color-mix(in srgb, var(--warn) 8%, var(--paper));padding:13px 16px;font-size:13px;line-height:1.55">
    <b style="display:block;margin-bottom:4px;font:600 10.5px var(--font-mono);letter-spacing:.1em;text-transform:uppercase;color:color-mix(in srgb, var(--warn) 60%, var(--ink))">Atención</b>
    El certificado vence el 2026-09-01. Renueva antes del 25 de agosto.
  </div>
  <div style="border:1px solid color-mix(in srgb, var(--err) 30%, transparent);background:color-mix(in srgb, var(--err) 8%, var(--paper));padding:13px 16px;font-size:13px;line-height:1.55">
    <b style="display:block;margin-bottom:4px;font:600 10.5px var(--font-mono);letter-spacing:.1em;text-transform:uppercase;color:color-mix(in srgb, var(--err) 60%, var(--ink))">Error</b>
    La sonda no alcanza el servicio desde las 14:10. Revisa el runbook antes de reiniciar.
  </div>
</div>

H16 · «Prohibido el borde de acento a la izquierda en tarjetas, avisos y notas. La jerarquía se hace con fondo, tipografía o etiqueta.» Decisión de Martín del 2026-08-06. Si ves una barrita de color a la izquierda en un sitio del grupo, es un sitio sin migrar. Fuente: cis-modelo/modelo/reglas.py · CANON.md §Componentes UI.

Fuentes