feat(events): events UI v3 (W-01…W-11) - #40
Conversation
/api/events/* ya no responde 404 a los paths con segmento `admin` cuando la petición trae cookie nan_session: la autorización (staff o no) la decide el backend y el proxy propaga su 401/403. Sin cookie sigue siendo 404 sin tocar el backend. La admin key nunca se reenvía (esa vía es solo directa contra cloud-api). Para el panel: el content-type text/csv se conserva en ambos sentidos (import y export de participantes) y Content-Disposition también (export?download=1). Todo lo demás sigue normalizado a JSON. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
- `src/lib/eventsAdmin.ts`: `fetchStaffSession` (cookie → /api/auth/me, solo
`role: staff`), `resolveAdminRoute` (404 por rewrite a quien no es staff,
nunca 403), `adminFetch` (cliente SSR de la API de administración con el
envelope de §6: warnings, dry_run, fields, detail; nunca lanza),
`adminHref`, `ADMIN_SCREENS`, `STATUS_LABELS`, `fmtAdminDate`.
- `src/layouts/EventsAdmin.astro`: shell del panel (NanPage noindex/nofollow,
enlace atrás, email del staff, título y navegación por pantallas del evento).
- `src/pages/events/admin/{index,_index}.astro`: portada del panel con el
patrón envoltorio + cuerpo `_`; la guardia se resuelve en el fichero de ruta.
- Tests: `eventsAdmin.test.ts` (sesión, guardia, cliente) y `adminRoutes.test.ts`
(toda ruta del panel llama a la guardia y es SSR; los cuerpos no; no hay /es/).
Solo español, sin variante /es/. Sin cambios de comportamiento fuera de
/events/admin.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
…/events/admin (SPEC v3 §8)
- Lista de eventos con filtro activos/archivados/todos, contadores, avisos y flash.
- /events/admin/nuevo: formulario completo del event.json con botón "simular" (dry_run).
- /events/admin/{slug}: ficha (fase, ventanas, contadores, avisos de coherencia), editor,
clonado y archivado/desarchivado (con "forzar" si no está cerrado ni cancelado).
- Patrón POST → 303 con ?ok=&warn= (readFlash); el envoltorio de ruta procesa el
formulario y el cuerpo _x.astro solo pinta.
- resolveAdminEventRoute: guardia de staff + ficha del evento + 404 de slug.
- Estilos del panel en src/styles/events-admin.css (CSS plano, sin @apply).
- Pruebas de formularios (eventsAdminForms) y guardia de rutas actualizada.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
…a ficha del evento (SPEC v3 §4.2 y §8)
- Tarjeta "Estado" en /events/admin/{slug}: estado actual (y previo si está cancelado),
automatización por fechas, y solo las transiciones que permite §4.2
(statusSequence/stateTargets espejo de EventModules.StatusSequence).
- Flujo en dos pasos: "Previsualizar avisos" (POST admin/state con dry_run) muestra las
consecuencias y un botón de confirmación; "Confirmar" hace la transición real y
redirige con ?ok=estado&warn=…; no_change e invalid_transition se enseñan sin cambiar nada.
- Sweep manual (POST admin/sweep): redirige solo si hubo transición; si no, muestra el
resultado (automation_off) en la página.
- Sin control de estado en eventos archivados.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
…export) en /events/admin/{slug}/participantes (SPEC v3 §6.2 y §8)
Nueva pantalla de participantes del panel, sin JavaScript, sobre la API de
admin de participantes:
- Tabla con filtros (estado, reserva, equipo en formato equipos, búsqueda)
leídos de `?estado=&reserva=&equipo=&q=` y traducidos a la query del
backend; enlace de exportación CSV vía el proxy (`/api/events/{slug}/admin/
participants/export.csv`, conserva text/csv y Content-Disposition).
- Acciones por fila: dar de baja, promover (reserva), pasar a reserva (sin
equipo ni entrega), reincorporar (con casillas de reserva y, en formato
individual, recuperar entrega) y editar (`?editar=<id>` abre la tarjeta).
- Alta manual (solo cuentas de NaN; 404 member_not_found y 409
participant_exists se pintan con el formulario relleno) e importación CSV
multipart con previsualización (`?dry_run=true`) e informe fila a fila.
- Especialidad y nivel son obligatorios en los formularios cuando el evento
los define (el backend responde invalid_specialty / invalid_level si no).
- Eventos archivados: solo lectura (sin formularios).
- `eventsAdminForms.ts`: `doneHref` admite la pantalla de vuelta; helpers
`str`, `on`, `readForm` exportados; textos nuevos de `?ok=` y de avisos.
- Pruebas de filtros y de `handleParticipantsForm` (alta, edición, cambios de
estado, id inválido, importación multipart con y sin dry_run, sin fichero).
Probado de extremo a extremo con wrangler dev + servidor Go + plataforma
falsa (alta, duplicado, cuenta inexistente, filtros, edición, promote/demote,
baja, reincorporación, previsualización e importación real, export CSV,
evento archivado).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
…n /events/admin/{slug}/equipos (SPEC v3 §6.3 y §8)
Nueva pantalla de equipos del panel, sin JavaScript, sobre la API de admin
de equipos:
- Tablero: una columna "Sin equipo" con los inscritos activos sin equipo
(`GET …/admin/participants?team=none`, sin reserva ni bajas) y una
columna por equipo (`GET …/admin/teams`) con origen, bloqueo por entrega,
avisos de tamaño, balance (nivel medio y especialidades) y miembros.
- Mover: cada miembro lleva un selector con los demás equipos y "sin
equipo" (`POST …/teams/{to}/members` o `DELETE …/teams/{from}/members/
{pid}`); los de la columna libre solo tienen "Añadir". Los equipos con
entrega activa no ofrecen mover ni disolver (decisión 19).
- Crear (nombre opcional y casillas con los inscritos libres), renombrar en
la propia columna, disolver, y generar automáticamente con casilla
`keep_manual`, previsualización (`dry_run`) en la página y generación real
con redirección.
- Eventos individuales: aviso en vez de tablero. Archivados: solo lectura.
- `doneHref` elimina avisos repetidos (al mover, el backend puede avisar lo
mismo del origen y del destino); textos nuevos de `?ok=`.
- Pruebas de `handleTeamsForm` (crear con ids filtrados, renombrar,
disolver, mover a equipo y a "sin equipo", validación local, 409 del
backend, previsualización y generación).
Probado de extremo a extremo con wrangler dev + servidor Go + plataforma
falsa (crear con miembros, añadir, sacar, mover entre equipos, renombrar,
previsualizar y generar, disolver, 404 y 409 del backend).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
…slug}/entregas (SPEC v3 §6.4 y §8)
Pantalla de entregas del panel de administración sobre la API §6.4:
- src/lib/eventsAdminSubmissions.ts: tipos de la fila de
GET …/admin/submissions (entrega + owner + owner_emails), etiquetas de
checks, campos editables y handleSubmissionsForm con las acciones
verify (POST …/admin/verify), update (PUT parcial: solo los campos que
trae el formulario), withdraw/restore (POST …/{id}/withdraw|restore),
check (PUT …/{id}/checks/{name} con {pass,reason} o {reset:true}) y
prize (PUT …/{id}/prize-eligibility con {eligible,reason} o
{reset:true}). Validación local de id, nombre de check y valores
true/false sin tocar el backend.
- entregas.astro (guardia + POST + 303) y _entregas.astro (tabla con
propietario y emails, enlaces a las URLs, checks con ✓/✗ y asterisco
si están fijados a mano, puntos automáticos, elegibilidad de premio,
estado activa/retirada; retirar/restaurar por fila; tarjeta
?editar=<id> con los campos visibles según submission.fields, el
forzado o reinicio de cada check con motivo y la elegibilidad de
premio; "Verificar todas"). Sin módulo de entregas → aviso; archivado
→ solo lectura.
- FLASH_LABELS: entrega_editada, entrega_retirada, entrega_restaurada,
check_forzado, check_reiniciado, premio, premio_automatico,
verificado.
- Pruebas: src/tests/lib/eventsAdminSubmissions.test.ts (8).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
La página pública /events mezcla en su agenda de "este mes" los eventos
publicados de GET /api/events (día y hora en Europe/Madrid, enlace a la
ficha, ubicación) con los estáticos de eventos.json, y añade un bloque de
suscripción al feed iCalendar del backend (SPEC v3 §6.1 bis): botón de
Google Calendar (cid=<feed>), enlace webcal:// para Apple y Outlook y la
URL del feed. Cada fila lleva "+ calendario" (plantilla de Google de ese
evento). La ficha /events/{slug} muestra dónde, el enlace, el demo day con
hora y fin, y "Añadir a mi calendario" (Google o .ics del evento).
- src/lib/eventsCalendar.ts: fetchEvents (SSR, tope de 2,5 s, lista vacía
si la API falla), enlaces del feed, googleEventURL, eventDetails
(descripción recortada a 800 caracteres sin perder el enlace),
toAgendaItems y mergeAgenda.
- src/lib/events.ts: apiBase y ssrHeaders exportados; EventInfo gana
location y url, EventDates gana demo_day_end.
- src/lib/agenda.ts: AgendaItem gana time y calendar.
- Proxy: deja pasar text/calendar con la caché del backend.
- i18n: tipos challenge/other, textos del bloque de calendario y de la
ficha (es/en).
- Tests: eventsCalendar.test.ts y el paso del feed por el proxy.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
Espejo del B-26 del backend: un evento sin módulo de inscripción va
draft → published → closed (SPEC v3 §4.1).
- eventsAdminForms.ts: statusSequence añade published a los informativos,
así que el control de estado ofrece publicar, cerrar y retroceder.
- eventsAdmin.ts: etiquetas "Publicado" y "publicado, sin inscripción".
- events.ts: la fase published en el tipo EventPhase.
- Ficha /events/{slug}: panel "Abierto a todos" con el enlace del evento
como CTA si lo tiene (es/en); la página de entrega lo trata como
"todavía no abierto".
- Tests de secuencia y destinos de transición actualizados.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
Pantalla /events/admin/{slug}/votos (SPEC v3 §6.5 y §8): resumen de la
votación (estado, abierta o cerrada, ranking público o solo admin, votos
que cuentan y descartados, votantes, peso del voto), el ranking calculado
al momento por GET admin/leaderboard aunque no esté publicado, y la lista
de votos de GET admin/votes en solo lectura (decisión 23: no se anulan ni
se editan). Los únicos atajos son abrir y cerrar la votación (§4.4), los
dos con previsualización dry_run y confirmación: al cerrar se enseña el
ranking que quedaría congelado y público.
- src/lib/eventsAdminVotes.ts: tipos de las dos respuestas, voteStats y
handleVotesForm (open_preview, open, close_preview, close).
- votos.astro y _votos.astro: ruta y plantilla, con el patrón del resto.
- eventsAdminForms.ts: flash votacion_abierta y votacion_cerrada.
- Tests: eventsAdminVotes.test.ts.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
Pantalla /events/admin/{slug}/auditoria (SPEC v3 §6.6, §6.7 y §8): las
líneas de audit.jsonl del evento con filtros desde/acción/límite (GET
admin/audit), más recientes primero, con actor, sujeto, avisos y el
antes/después desplegable; y la lista de backups (GET admin/backups) con
restaurar en dos pasos: previsualización dry_run, que solo comprueba que
el backup valida, y confirmación. Archivado: sin restaurar.
- src/lib/eventsAdminAudit.ts: tipos, filtros (lo inválido se ignora en
vez de provocar un 400), textos auxiliares y handleAuditForm
(restore_preview, restore).
- auditoria.astro y _auditoria.astro: ruta y plantilla, con el patrón del
resto.
- eventsAdminForms.ts: flash restaurado.
- Tests: eventsAdminAudit.test.ts.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
Causa: nan-system.css (el reset y los estilos de elemento p, a, button…) va sin capa y se carga después de global.css, así que ganaba a cualquier utilidad de Tailwind, que vive en @layer utilities: mx-auto, mt-*, los colores de párrafo, la monoespaciada… se ignoraban en las pantallas escritas con utilidades (eventos, panel admin). Por eso la ficha salía pegada a la izquierda, sin márgenes y en gris. - styles/nan-system.css: reset y estilos de elemento dentro de @layer base (la misma capa de Tailwind, para que utilities quede después). La esquina recta se queda fuera de la capa a propósito: es regla de la casa y gana a cualquier rounded-*. - Ficha del evento rediseñada: columna centrada (max-w-3xl), titular Archivo expandida a escala de ficha, descripción grande, fechas y dónde/enlace en tarjetas a dos columnas, premio con filete violeta, panel de fase con más cuerpo y botones y campos de la casa (.btn, .field) en formularios y CTAs. - RegistrationBar, RegisterForm y LoginForm: mismo tratamiento. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
El backend devolvía "specialties": null y "levels": null en un taller publicado (bug de clone en el servidor, corregido allí) y la pantalla de participantes del panel hacía .length sobre null: el SSR fallaba y la página llegaba en blanco con 200. - admin/[slug]/_participantes.astro: las dos listas pasan por `?? []` una sola vez y los cuatro selects usan esa copia. - [slug]/_index.astro: el mismo guard al pasar las listas a RegisterForm. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
… aviso anterior
La importación es el único POST del panel que escribe y pinta el resultado
en la misma respuesta (hay que enseñar el informe fila a fila), y ahí se
veían dos cosas viejas: los contadores de inscritos/reserva/bajas, que
`resolveAdminEventRoute` carga antes de atender el formulario, y el aviso
`?ok=` de la acción anterior, porque el formulario envía a la URL actual
con su query. Tras importar cinco altas la cabecera seguía diciendo
"2 / 6" y "Participante dado de alta.".
- eventsAdmin: reloadAdminEventView recarga la ficha; si falla se queda la
anterior (un contador viejo es mejor que un 500).
- participantes.astro: la usa cuando la importación ha escrito.
- readFlash acepta el outcome y calla el flash si en este render se ha
atendido un POST; las seis pantallas de /events/admin/{slug} lo pasan.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
La ficha del panel pintaba la fase efectiva como "inscripción abierta" y "votación abierta", pero la fase no garantiza la ventana: la inscripción necesita además estar dentro de sus fechas y la votación que el interruptor esté puesto (SPEC v3 §5). En el evento de pruebas la cabecera decía "inscripción abierta" mientras la lista de ventanas de al lado decía "Inscripción: cerrada". Ahora las etiquetas de esas dos fases solo nombran la fase; quien manda sobre lo abierto y lo cerrado es la lista de ventanas. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
…con mayúscula En v3 las especialidades y los niveles los escribe quien organiza el evento (SPEC v3 §3.2), pero `events.options` solo traduce el vocabulario fijo de v2. En "Mi equipo" del hackatón de pruebas eso mezclaba "Frontend/Senior" con "devops/senior" y "data/mid" en la misma lista. `optionLabel` (en i18n, que es puro y se puede probar) traduce lo que conoce y al resto le sube la inicial; los valores que ya vienen con mayúscula, como "ML/IA", no se tocan. Lo usan la ficha "mi estado" y el formulario de inscripción. Los checks de la entrega siguen con el diccionario a secas: ese vocabulario sí lo fija el backend. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
El backend manda `invalid_url` con el campo en `data.field` (singular, SPEC v3 §6.4 paso 6), mientras que el resto de errores usan la lista `data.fields`. `adminFetch` solo leía la lista, así que al guardar una entrega con una URL mala el panel enseñaba "Error 400: URL no válida" sin decir cuál. Ahora se normalizan las dos formas y el aviso ya sale como "URL no válida · campos: video_url". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
Los atajos de §4.4 dejan abrir la votación en construcción, entrega o votación (con el interruptor cerrado) y cerrarla en votación, así que en estado `voting` con la votación aún cerrada valen los dos. La pantalla pintaba solo uno y ganaba el de cerrar, de modo que desde ese estado —el que produce `voting_not_open` en el sitio público— no había forma de abrir la votación sin salir a la ficha. Ahora se pintan los dos botones cuando ambos aplican. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
…mismo
Tres arreglos en el formulario de reasignar a un ausente (/events/{slug}/me),
vistos probándolo en local:
- La lista de ausentes incluía a quien la mira. El backend rechaza señalarse
a sí mismo (§7.6.3) y el panel solo podía enseñar el error genérico
"No se pudo registrar". Ahora se ofrecen solo los compañeros y, si no queda
ninguno, el formulario no se pinta.
- Al ejecutarse la convocatoria (`filled` o `no_pool`) el equipo cambia, pero
la lista de compañeros venía del servidor y seguía enseñando el equipo
anterior justo debajo del aviso de que había cambiado. Se añade el botón
"Ver el equipo actualizado" para recargar la ficha; con la convocatoria
todavía pendiente no aparece, porque ahí no ha cambiado nada.
- El aviso de `no_pool` daba por hecho equipos de 3. En v3 el tamaño lo
configura quien organiza, así que ahora dice que el equipo sigue con los
miembros que quedan.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
En v3 todas las fechas del evento son opcionales, pero los avisos de la
ficha y de la página de entrega interpolaban `{date}` sin comprobarlo: un
evento sin `submission_close` mostraba "Entrega tu proyecto antes del ."
y uno sin `submission_open`, "Las entregas abren el .".
Cada aviso que nombra una fecha tiene ahora gemelo sin ella, y la página
elige según haya fecha o no. Visto probando el reto solo
`reto-agente-terminal`, creado sin fechas desde el panel.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
Cuatro listas (registration.specialties, registration.levels, submission.checks y submission.prize_requires) llegan como null cuando el slice va nil en el backend: un event.json escrito a mano, o un evento anterior al saneo de applyCreateDefaults. Estaban tipadas string[], así que el compilador no pedía el ?? [] en ninguna lectura. Tiparlas string[] | null no es defensivo: es lo que llega por el cable, y hace que astro check marque en rojo cualquier lectura sin red, incluidas las que pasan por una desestructuración. Al revés, submission.fields NO puede ser null: en el backend es un struct por valor y encoding/json siempre lo serializa como objeto. Los fallbacks que lo trataban como opcional eran código muerto y se van. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
El barrido con regex sobre ev.<campo> era peor que lo que lo sustituye: no
veía una desestructuración (const { checks } = ev.submission), que es el
refactor más natural en un .astro, y protestaba por código correcto
(ev.submission.checks?.map(...)). Con los cuatro campos ya tipados nullable,
quien exige la red es astro check, que sí typechequea el frontmatter.
La guarda de _me.astro se reescribe porque tampoco cazaba lo que decía: la
regla anterior se conformaba con encontrar una llamada, y .join( ya contaba,
así que {[p.specialty, p.level].join(' · ')} pasaba. Ahora cada aparición
vale solo si va envuelta en el helper o si es una condición.
preact-render-to-string pasa a devDependency declarada: entraba de rebote con
@astrojs/preact y funcionaba por el izado de npm, que es un detalle del
gestor de paquetes y no un contrato.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
…blica En v3 el vocabulario lo escribe quien organiza el evento (SPEC §3.2), así que events.options solo traduce el fijo de v2 y el resto se enseña con la inicial en mayúscula. La parte pública ya lo hacía; el panel no, y el mismo dato se veía de dos formas: el organizador leía "devops" donde el participante leía "Devops". Los nueve puntos del panel pasan ahora por dos helpers compartidos, para que haya un solo sitio que probar: las fichas de participante y de miembro de equipo, la celda de la tabla, el resumen de reparto de equipos (que pintaba devops×3) y los cuatro selects de alta y edición. En los selects solo cambia el texto: el value sigue siendo el token que se guarda. Sigue en crudo, a propósito, el editor del vocabulario en la pantalla del evento: ahí se escriben los tokens que van al event.json, y es el único sitio donde el valor literal es lo correcto. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
|
Reviewed in depth. The overall shape is good and well argued in the comments: guards resolved in 1. The nullable-type safety net only covers the public half
The backend does send And the gap is measurable. Dropping the
So in the panel the Worth deciding with 2. SecurityMinor, but the stealth 404 isn't quite stealthy: It grants no access (the backend still authorises), but the goal stated in the Two defence-in-depth notes, both optional:
3. Reuse
Same drift 4. Cleanup
5. TestsSolid and well thought through — swapping the text scan for a type guard in
6. LanguageGoing forward, let's keep code comments and commit messages in English To be explicit about what does not change: the panel's user-facing strings |
…adena
`includes('nan_session')` sobre toda la cabecera Cookie daba por buena
`basura=xx-nan_session-xx`. No concedía acceso (el backend sigue
autorizando) pero rompía el 404 sigiloso de las rutas admin: se podía
distinguir una ruta real de una inventada. fetchStaffSession tenía la
misma comprobación.
Nueva cookieHeaderHasSession() en events.ts que compara el nombre de
cada cookie; hasSessionCookie y fetchStaffSession la usan. Tests con
señuelos en el proxy, en la guardia de staff y en la función.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
… | null EventInfo (web pública) ya tipaba specialties, levels, checks y prize_requires como `string[] | null`, y el compilador obligaba al `?? []`. En el panel eran `string[]`, así que los `?? []` que hay eran disciplina, no garantía. Con el tipo alineado, astro check vigila cualquier lectura nueva sin protección (hoy: 0 errores). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
…rtidos en un solo sitio eventsAdmin.ts era una isla: reimplementaba apiBase (idéntica), ssrHeaders (con un parámetro `extra` que nadie usaba), la regex de segmentos y su predicado, el tipo Windows, LeaderboardRow (que su propio comentario decía "mismo formato que el público"), Owner tres veces como literal y una fmtAdminDate que difería de fmtDate en tres tokens. login-request.ts repetía apiBase y el Origin, y el json() de los dos endpoints era idéntico carácter a carácter. Ahora todo vive en events.ts (safeSegments, Check con forced/reason, LeaderboardView, fmtUTC) y en un apiResponse.ts nuevo (json). Los nombres que ya importaban las pantallas (SubmissionCheck, LeaderboardRow, fmtAdminDate) se conservan como alias o re-export. astro check 0 errores, 858 tests, build ok. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
…hecks en eventsAdminForms Los cinco manejadores de formularios del admin repetían el mismo preámbulo (leer el form, sacar la acción, construir la redirección) y cada uno tenía su propia función de error 400 y sus propias expresiones SAFE_ID/SAFE_CHECK. Ahora todo sale de eventsAdminForms: beginForm devuelve fd, values, action y back; badForm construye la respuesta de validación; SAFE_ID y SAFE_CHECK se exportan una sola vez. Las etiquetas de los checks también salen de una sola lista (CHECK_DEFS), de la que se derivan CHECK_LABELS, CHECKS y SCORING_CHECKS. Sin cambios de comportamiento; los 858 tests siguen pasando. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
…ntallas - Los seis cuerpos del panel de un evento declaraban la misma `interface Props`; ahora la comparten como `AdminScreenProps` en eventsAdmin.ts. - Los cinco cuerpos públicos repetían el bloque "cookie → /me → sesión válida"; ahora lo hace `eventSession(request, slug)` en events.ts. - `ownerKind` (equipo / participante) vivía en _votos.astro y como ternario a mano en _entregas.astro; ahora sale de eventsAdmin.ts. - El filtro "desde" de auditoría usa `isoToLocal` en lugar de un `slice(0, 16)` sin validar: un timestamp con offset se normaliza a UTC como en el resto de formularios. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
…rrada `'adm-pill-warn': false` era una condición constante y la píldora nunca se pintaba en ámbar. Ahora se marca cuando la ventana está cerrada, como en el resto del panel (ámbar = atención). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
…s que el formulario no manda - `resolveAdminEventRoute` es la guarda de seis de las siete rutas del panel y no tenía test: staff + evento → ficha y slug del backend; evento inexistente → 404 por rewrite; sin staff → 404 sin pedir la ficha. - `reloadAdminEventView` devuelve la ficha nueva o, si la recarga falla, la anterior. - El test de ida y vuelta del formulario documenta que location, url y demo_day_end no van en el cuerpo del PUT y sobreviven porque el backend hace un merge por claves. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
…español
El criterio del repo es español; los 61 ficheros del PR ya lo cumplían y
quedaban 50 ficheros anteriores (docs, rate limits, waitlist, community
signup, layout de docs y sus tests) con los comentarios en inglés.
Solo cambian comentarios (JSDoc, `//`, `/* */`, `<!-- -->`, `{/* */}` y CSS):
ni código, ni strings, ni nombres de tests, ni URLs. Se conservan tal cual
los identificadores, las etiquetas JSDoc, las directivas y las cadenas
citadas entre comillas o backticks (mucho copy en inglés de las docs que los
tests comprueban literalmente). Verificado quitando los comentarios de las
dos versiones y comparando el resto: idéntico en los 50 ficheros.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
|
Re-reviewed at Verified rather than taken on trust, and confirmed fixed:
Three things left. 1. §4:
|
The house style is English for everything written for other developers (comments, commit messages, test names); user-facing copy stays in Spanish. 87d2edb went the other way and also pulled nine files unrelated to events into this PR. This reverts it wholesale. This reverts commit 87d2edb. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
Passthrough responses (text/csv, text/calendar) were still going through
the `text || '{}'` fallback, so an empty participants export or calendar
feed reached the operator as a file with `{}` inside. The fallback now
applies only to JSON responses.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
The request-level test from f759bea already covered the decoys through hasSessionCookie; this adds the four direct assertions on the helper so a refactor of it cannot quietly undo the fix. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
… (W-12) Read the new `image_url` from the events API. When present, the event page renders it under the header and passes it to the layout, which uses it as og:image / twitter:image instead of the generic NaN card (the 1200x630 size hints only go out with the generic card). The admin form does not expose the field; the PUT merge keeps it. Add the Gentleman Programming workshop cover under `public/img/events/` (WebP, 1600 px wide, ~16 KB). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
… fallback Review follow-up for the event cover: - The page now renders the cover through safeImage (https only; http is blocked by the CSP and mixed content in production) and safeUrl in dev, where covers point at localhost. Nothing prints the raw field. - NanBase falls back to the generic card on an empty image, not only on undefined. - Source guards for both in eventsUiGuards; the calendar fixture carries image_url; the admin-form comment no longer claims null clears a string. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7
Size of the change
61 files, +5 613 / −153. Of the added lines:
src/tests/, 12 files).astro,.ts,.tsx, CSS)The v3 documentation lives entirely in the backend repository
(
docs/SPEC-v3.mdanddocs/API.md); none is added here. The tests mostlycover the admin panel helpers, which is where the logic is: filters, forms,
team grouping and vocabulary labelling.
Fixes on the events UI v3 (W-01…W-11) after a review with subagents.
null. Four lists(
registration.specialties,registration.levels,submission.checks,submission.prize_requires) arrive asnullwhen the slice is nil, andwere typed
string[]. Nowastro checkis what enforces the guard.Conversely,
submission.fieldscan never benull(a struct by value inGo): the fallbacks treating it as optional were dead code.
ev.<field>didnot see a destructuring and complained about correct code. The
_me.astroguard is rewritten because it did not catch what it claimed either: finding
a call was enough, and
.join(already counted.preact-render-to-stringbecomes a declared devDependency. It came inby accident through npm hoisting, which is not a contract.
organiser read
devopswhere the participant readDevops. Nine spots inthe panel now go through two shared helpers. The vocabulary editor stays raw
on purpose: that is where the tokens that go into
event.jsonare written.Verification
tsc --noEmitclean,astro check153 files / 0 errors and 857 tests, andeach commit on its own as well. The new guards were checked with mutations:
removing a
?? []makesastro checkfail on the destructured const, and thethree forms of the
_me.astroantipattern fail the new guard (two of thempassed the old one).
Left out on purpose
The panel editor does not expose
location,urlordemo_day_end; theGauntlet shows up twice in
/events; the repo has noLICENSE.🤖 Generated with Claude Code
https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7