v0.2.0
May 7, 2026
Segundo milestone. Cierra Fase 3.1 (admin & herramientas editoriales) al 100% + bases SEO/ops + revisión integral del proyecto. La estructura del producto está completa para abordar Fase 3.2 (carga de contenido masivo) y los cambios visuales planeados. Resumen del release: ImageUploader admin con Supabase Storage funcional, render público de hero image en los 6 perfiles, refactor del admin /historias y /admin/glosario a backend real (post-mock), bases SEO completas (sitemap.xml, robots.txt, OG images dinámicas, custom 404/500, render público de /historias y /glosario), backup cron Supabase a repo privado verificado, calendario funcional día + mes estilo Revista Palermo, búsqueda híbrida pg_trgm + tsvector, dark mode con toggle Sistema/Claro/Oscuro, sources público citados en perfiles, PedigreeTree 5 generaciones, sistema de contribuciones públicas free-form, community edits + cola moderación. Adicionalmente: doc 27 (QA checklist v0.1.0) y doc 28 (revisión integral pre-cambios visuales con 8 áreas auditadas) escritos para guiar el QA manual y la priorización de mejoras.
Added
- ·Doc 28 — Revisión integral del proyecto v0.2.0 (
docs/turfdex-v2/inter_docs/28-review-v0.2.0.md). 8 áreas auditadas (tech stack al día, parity admin vs schema, parity seed vs admin, scope vs plan original, security audit, documentation audit, code health, competencia refresh). Total findings: 4 P0 / 14 P1 / 33 P2 / abundante info verificado OK. Doc operativo con recomendaciones priorizadas en bloques A-E. Apoyado por staging files temporales en.tmp/review-2026-05-07/(no commiteado). - ·Doc 27 — QA checklist v0.1.0 (pre-Fase 3.2) (
docs/turfdex-v2/inter_docs/27-qa-checklist-v0.1.0.md). ~180 checkboxes organizados en 22 secciones (auth, header, 6 listings, 6 detalles, hero image, calendario, búsqueda, historias, glosario, contribuciones, /sugerencias, admin CRUD, SourceCitationEditor, ImageUploader, admin historia/glosario, colas moderación, SEO, i18n, dark mode, backup, Lighthouse, pendientes conocidos). Cada item con criterio de aceptación + URL exacta. ⚠️ marca los críticos. Documento operativo para Kevin antes de cargar contenido masivo / cambios visuales. Doc 00-README actualizado con entries para los 6 docs que estaban faltando del index (22, 23, 25, 26, 27 + actualizó la frecuencia del 21). - ·Admin `/admin/historias` y `/admin/glosario` reales con backend Supabase — cierra el último item de "sistemas" (los forms estaban mockeados desde Fase 2). Ahora ambos listings + forms persisten en
historiayglosario_terminocon i18n JSONB.- ·Schemas Zod (
lib/admin/schemas/historia.tsyglosario.ts) con I18nText (es required, en/pt/ja optional). Historia incluyesuperRefinepara enforce doc 07 §11: si hayimagen_url, los 3 fields de attribution (imagen_credit + imagen_license + imagen_source_url) son obligatorios. - ·Queries admin (
lib/admin/queries-historia.tsyqueries-glosario.ts) conlistXForAdmin()(incluye drafts/archivados via RLS admin OR) ygetXForEdit(id)para precargar el form. - ·Server actions (
app/admin/historias/actions.tsyglosario/actions.ts) consaveX(input, mode),setXEstado(id, estado),deleteX(id), manejo de unique violation 23505 con error mapeado aslugfield, revalidate de paths públicos es+en. Triggertg_log_editya existente registra audit eneditstable automáticamente. - ·`uploadHistoriaImage` server action separada — historia usa los 4 campos directos en su row (no
image_assetspolymorphic como los 6 entities core). Sube a Storage pathhistoria/{uuid}.{ext}, retorna URL pública. Si el form se cancela sin guardar, queda blob orphan en bucket — tradeoff aceptado (admin-only, 5MB cap). - ·`HistoriaImageField` client component (
components/admin/image/HistoriaImageField.tsx): file picker → upload inmediato →imagen_urlse setea en el state RHF; o pegás URL externa directa (Wikimedia). Cuando hay url, render condicional de los 3 fields de attribution con datalist de licencias comunes. - ·HistoriaForm + GlosarioForm refactoreados a EntityFormShell + FormSection + Field, mismo patrón que CaballoForm. Auto-slug desde
titulo.es/termino.esconslugify(). Botones "Guardar borrador" / "Publicar" en el footer sticky seteanestadoantes de submit. Validación cliente consafeParseantes de viajar al server. - ·Listing pages (
app/admin/historias/page.tsxyglosario/page.tsx):pickEs()helper para extraer título es del JSONB, formatoIntl.DateTimeFormat('es-AR', tz: America/Argentina/Buenos_Aires)paraupdated_at, stats inline (total · publicados · borradores). SinMockDataNoticeni mock arrays. - ·Cleanup:
lib/admin/mock.tsborrado (era el último consumidor deHISTORIASyGLOSARIOmock arrays). Ningún otro código importaba de ahí.
- ·Schemas Zod (
- ·Render público del hero image en los 6 perfiles — completa el flujo end-to-end del ImageUploader. Ahora cuando un admin marca una imagen como hero (en
image_assetsconis_hero=true), aparece automáticamente arriba del perfil público (/caballos/{slug},/jockeys,/studs,/hipodromos,/carreras,/entrenadores).- ·Componente `<HeroImage>` server async en
components/profile/HeroImage.tsx. TomaentityType + entityId + alt + priority. Query viagetHeroImageForEntityenlib/queries/images.tsconcreatePublicClient(RLSimage_assets_public_read using (true)permite SELECT anon). Si no hay hero retorna null — no placeholder genérico, graceful empty. - ·Layout:
<figure>conaspect-[16/9]+next/image fill+sizes="(min-width: 1024px) 1024px, 100vw".prioritypor defecto en perfiles para LCP. Mobile: bleed -mx-4 (full-width corner-to-corner) + sin border-radius. Desktop: rounded-md + container width. - ·Figcaption con doc 07 §11 enforced:
credit · license · fuente ↗(link target=_blank rel=noopener noreferrer). Visible siempre, no hover-only. - ·Wireado en los 6 perfiles públicos justo antes del
<{Entity}Hero>existente. Insertado como server component, se ejecuta en build (SSG viagenerateStaticParams) — costo cero en runtime. - ·`next.config.ts` `images.remotePatterns`: agregado
*.supabase.co/storage/v1/object/public/**(bucket público de imágenes admin) +upload.wikimedia.org+commons.wikimedia.org(URLs típicas que Kevin podría pegar enhistoria.imagen_urlcuando refactoree ese form). Sin**wildcard para mantener allowlist explícito.
- ·Componente `<HeroImage>` server async en
- ·ImageUploader admin con Supabase Storage funcional — cierra el último item de "sistemas" pre-Fase 3.2 (doc 05 §3.1). Hasta hoy Kevin subía imágenes a la consola de Supabase y pegaba URL a mano: friction garantizada cuando empiece a cargar volumen (30+ caballos, 15+ jockeys, etc.). Ahora hay flujo end-to-end desde el admin de cada entidad.
- ·Bucket `images` + storage policies (
supabase/migrations/20260507130000_storage_images.sql): bucket público confile_size_limit=5MB+allowed_mime_types=[jpeg,png,webp]. Policies enstorage.objects: SELECT público (matchea bucket público + claridad explícita), INSERT/UPDATE/DELETE solo sibucket_id='images' and public.is_admin(). Idempotente víaon conflict do update+drop policy if exists. Doc 26 §"storage.rules" lo tenía planeado en migrations: ahora está. - ·Server actions en
app/admin/images/actions.ts:- ·
uploadImage(FormData): valida mime + size, genera path determinístico{entity_type}/{entity_id}/{uuid}.{ext}, sube a Storage, hacegetPublicUrl, siis_hero=trueunsetea heros previos del mismo entity, inserta row enimage_assets. Rollback automático del file de Storage si el insert del row falla (evita orphans). - ·
updateImageMeta({id, credit, license, source_url}): actualiza solo metadata, no toca el archivo. - ·
setImageHero(id): unset hero anterior + set este → 1 hero por (entity_type, entity_id). - ·
deleteImage(id): borra row + extrae path del public URL via regex/\/storage\/v1\/object\/public\/images\/(.+)$/y borra el blob de Storage. - ·Todos llaman
requireAdmin()y revalidan rutas públicas/{entity_path}/{slug}+/en/{...}.
- ·
- ·Componentes en
components/admin/image/:- ·
ImagesSection.tsx(server): wrapper que cargaimage_assetsordenados (is_herodesc,ordinalasc,uploaded_atdesc) y delega al client. - ·
ImagesList.tsx(client): grid 1/2/3 cols con preview thumb 4:3, badge "Hero" sobre la marcada, botones [Marcar hero / Editar / Eliminar (con confirm)] por imagen. Form de upload con file picker + preview blob (revoke object URL al cancelar/saved), checkbox "marcar como hero" defaulteado a true cuando no hay hero previa, datalist de licencias comunes (CC0,CC BY 4.0,CC BY-SA 4.0,CC BY-NC 4.0,Dominio público,Permiso del autor,Uso editorial). Validación cliente del archivo antes del submit (mime + size) para evitar trip al server con files inválidos.
- ·
- ·Schema
lib/admin/schemas/image.tsconimageAssetInputSchema(zod) —creditylicenserequeridosmin(1),source_urlrequeridoz.url(),is_heroboolean default false. Doc 07 §11 enforced en schema + DB (NOT NULL) + UI. - ·Queries
lib/admin/queries-image.tsconlistImagesForEntity(entityType, entityId)ordenando hero primero. - ·Wireado en los 6 admin edit pages (
/admin/{caballos,jockeys,studs,hipodromos,carreras,entrenadores}/[id]):<ImagesSection>debajo de<SourcesSection>. Patrón consistente con SourcesSection (server wrapper + client list). - ·Out of scope: render público de hero en perfiles (separable, no bloquea carga); admin historia/glosario (form todavía mock — ver "Pendiente / Conocido" abajo); galería con reorder ordinal (1 hero por entidad alcanza para MVP); resize / WebP conversion (Next 16
next/imagelo hace on-render, no necesita server-side por ahora).
- ·Bucket `images` + storage policies (
- ·Backup cron de Supabase a repo privado — cierra el último blocker de Fase 1c (doc 10 §"Datos recuperables sin proveedor").
.github/workflows/backup-supabase.ymlcorre cron diario 03:00 UTC (= 00:00 ART, momento de menor uso) + manual dispatch. Tres dumps separados viasupabase db dump: roles (con RLS), schema (DDL), data (INSERT-style). Gzipped y commitados a un pathdumps/YYYY/MM/<ISO-timestamp>.{roles,schema,data}.sql.gzen el repo privadoturfdex-web-backups. Auto-prune de archivos > 90 días para mantener el repo manejable; Kevin puede mover snapshots mensuales a/archivemanual si quiere retención más larga. Failsafe: el step "Verify required secrets" falla con error explícito si faltanBACKUP_REPO_TOKEN/BACKUP_REPO_FULL(los 3 secrets de Supabase ya existen del workflowsupabase-deploy). Setup pendiente de Kevin: crear repo privadoturfdex-web-backups+ PAT fine-grained con scopeContents: Read+Writeapuntando solo a ese repo + 2 secrets nuevos en Actions. Hasta que esté, el cron falla limpio sin tocar nada. - ·OG images dinámicas con `next/og` ImageResponse (built-in Next 16, sin sumar
@vercel/og— feedback_no_unprompted_tech respetado). Cuando alguien pegue/caballos/yatasto,/carreras/gran-premio-carlos-pellegrini,/historias/fooen Slack/WhatsApp/Twitter, ahora aparece card brandeada en lugar del genérico Vercel default.- ·Template compartido en
lib/og/template.tsx: warm cream bg#FAF8F5+ accent stripe bordó#7B1F2Fizquierda + wordmark TURFDEX top + kicker uppercase tracking-wider + título serif XL (auto-resize a 76 si>28chars) + meta lines bottom con border-top sutil. Constraints Satori (sin Tailwind, solo flexbox, sin grid) — todos los estilos inline.OG_SIZE = 1200×630(estándar).fallbackOgTemplate(reason)para cuando la query falla o el slug no existe. - ·8 routes OG generadas:
- ·
/caballos/[slug]/opengraph-image.tsx→ kicker "Caballo · Argentina" + nombre + meta (range años nacimiento-muerte, padre × madre). - ·
/jockeys/[slug]/...→ "Jockey · Argentina" + nombre + meta (range años, nacionalidad). - ·
/studs/[slug]/...→ "Stud · Argentina" + nombre + meta (año fundación, ubicación ciudad+provincia). - ·
/hipodromos/[slug]/...→ "Hipódromo · Argentina" + nombre + meta (año fundación, ubicación). - ·
/carreras/[slug]/...→ "Carrera · Argentina" + nombre_corto/nombre + meta (grade, distancia + superficie, hipódromo). - ·
/entrenadores/[slug]/...→ "Entrenador · Argentina" + nombre + meta (stud principal, victorias documentadas). - ·
/historias/[slug]/...→ "Historia del turf" + título i18n + meta (era, tiempo de lectura). - ·
/glosario/[slug]/...→ "Glosario del turf" + término + meta (sumario corto truncado a 90 chars). - ·
/opengraph-image.tsx(root) — fallback minimal con wordmark gigante para home/listings//sobre//contribuir//calendario. Las páginas individuales tienen versiones más ricas.
- ·
- ·Por convención Next 16,
paramsen los image routes son Promises — todos awaiteados. Las imágenes se generan en build cuando son estáticas (la mayoría), runtime cuando dependen de data fresca. Ningún custom font cargado todavía (system serif + sans bastan); migrar a Inter/serif custom embedded en/publicqueda como iteración futura si la calidad no convence.
- ·Template compartido en
- ·Sitemap.xml + robots.txt dinámicos — invisible pero crítico para que Google indexe la wiki entera. Hasta hoy no había mapa explícito; el crawler se basaba en links internos solamente.
- ·`app/sitemap.ts`: enumera 12 top-level pages (home + 6 listings + calendario + historias + glosario + sobre + contribuir) más una entry por slug de cada entity. 9 queries paralelas via
Promise.all(caballo + jockey + stud + hipodromo + carrera + entrenador + historia + glosario + fechas con meetings). Cada caballo además suma su/pedigree. Cada fecha con ediciones cargadas suma/calendario/YYYY-MM-DD. Hreflang viaalternates.languagespara cada URL (es default sin prefijo, en con/en/).priorityychangeFrequencycalibrados por tipo (home 1.0/weekly, listings 0.8, perfiles 0.7, glosario 0.4/yearly, calendario daily). FailsafesafeSlugs()envuelve cada query — si una falla, el sitemap se genera con las otras + entries top-level (no cae todo el sitemap por un error en una entity). - ·`app/robots.ts`: producción permite
/con disallow de/admin,/auth,/login,/sugerencias+ apunta a/sitemap.xml. Preview/dev (SITE_ORIGIN !== 'https://turfdex.com') bloquea TODO conDisallow: /—dev.turfdex.comy branches Vercel preview NO se indexan, evitando contenido duplicado en SERPs.
- ·`app/sitemap.ts`: enumera 12 top-level pages (home + 6 listings + calendario + historias + glosario + sobre + contribuir) más una entry por slug de cada entity. 9 queries paralelas via
- ·Render público de `/historias` y `/glosario` — cierra el último item de Capa A SEO (doc 04 §"Content surfaces in MVP"). Hasta hoy el admin existía pero las URLs públicas eran
PlaceholderPage. Wiki ahora indexable end-to-end.- ·Listing `/historias` (
app/[locale]/historias/page.tsx): grid de 1/2/3 cols conHistoriaCardpor publicación. Cards con imagen 16:9 (lazy vianext/image), tema chip, era, tiempo de lectura, título serif, sumario clamp-3. Sort:featured_rankasc nullsLast →published_atdesc nullsLast. - ·Detalle `/historias/[slug]`: header con metadata chips + título serif XL + sumario lead + figura imagen con figcaption (credit + license + source link target=_blank), body markdown rendereado, breadcrumbs Inicio › Historias › título, JSON-LD
Article(Schema.org).generateStaticParamsdesdegetHistoriaSlugs()para SSG. OpenGraph type=article con imagen. - ·Listing `/glosario` (
app/[locale]/glosario/page.tsx): índice alfabético sticky + agrupación por primera letra (conunaccentvia NFD strip de combining marks). Cada letra = sección con anchor#letra-X+ lista de términos con sumario_corto. Layout más denso que historias (definiciones cortas). - ·Detalle `/glosario/[slug]`: definición completa rendereada en markdown + sección "Términos relacionados" con chips (referencias_internas) — los chips muestran labels pero por ahora no linkean (
entity_idrequiere resolución a slug que se difere a sesión post-data). Nota explícita en UI explicando el gap. Breadcrumbs + JSON-LDDefinedTerm(Schema.org) coninDefinedTermSetapuntando al glossary completo. - ·Renderer markdown propio en
lib/markdown/render.tsx— sin dependencias (feedback_no_unprompted_tech). Soporta paragraphs, h2/h3, bullets, bold, *italic*,codeinline, text (interno vianext/link, externo target=_blank rel=noreferrer noopener). Devuelve React elements (XSS-safe, nodangerouslySetInnerHTML). NO soporta: imágenes embedded (van porimagen_urlseparado), tablas, code blocks ``...``, blockquotes, HTML inline. Suficiente para wiki-style content; Kevin puede ampliar el subset cuando aparezca pain real. - ·Queries
lib/queries/historia.ts(getHistorias,getHistoriaSlugs,getHistoriaBySlug) ylib/queries/glosario.ts(getGlosarioTerminos,getGlosarioSlugs,getGlosarioTerminoBySlug). Filtranestado='published'. Tipos i18n conpickI18nfallback es → en. - ·i18n: namespaces
HistoriasList,Historia,HistoriaNotFound,GlosarioList,Glosario,GlosarioNotFounden es/en con sub-namespaces para tema, breadcrumbs, lecturaMin (ICU), referenciasTitle, etc. Strings legacytipoHistorias/tipoGlosarioenPlaceholderquedan obsoletas — se removerán cuando se borrePlaceholderPage.
- ·Listing `/historias` (
- ·Páginas custom 404 + global-error 500 con marca Turfdex. Hasta hoy
/cualquier-cosa-rotay/caballos/slug-inexistentecaían en defaults Next sin branding. Ahora:- ·
app/[locale]/not-found.tsx(server, i18n viagetTranslations('NotFound')): kicker "404 — Página no encontrada" + h1 + body explicando posibles causas (slug cambiado / no publicado / URL mal escrita) + CTAs primario "Sumar información" (lleva a/contribuir) y secundario "Volver al inicio" + 3 hint cards a/caballos,/carreras,/calendario. Reutiliza tokens de tema (dark mode-aware automático). - ·
app/global-error.tsx(client, root level — DEBE incluir<html>+<body>porque reemplaza el root layout cuando un error escapa del [locale]/error boundary): copy bilingüe hardcoded ES/EN (no tiene acceso a next-intl context — el root layout falló), botón "Reintentar / Try again" cableado areset(), link a "/" vianext/link. Muestraerror.digestpara debugging si Next lo provee. Estilos inline (no Tailwind compilation context garantizado en este nivel). - ·i18n: namespace
NotFoundconkicker,title,body,ctaHome,ctaContribuir,hints.{caballos,carreras,calendario}en es/en.
- ·
- ·Vista mensual `/calendario/mes/[YYYY-MM]` estilo Revista Palermo + ISR para fechas con data. Cierre del "100%" del calendario.
- ·Vista mensual con grilla 7 cols Lun→Dom (
MonthlyView.tsx). Cada celda del mes muestra el día + por meeting[HIPÓDROMO]+★si tiene G1 + count de carreras. Layout matchea la convención local (Revista Palermo) que es el único calendario funcional argentino consultable. Cells fuera del mes quedanopacity-60para preservar la grilla 7×N completa. Hoy (en hora AR) se highlightea con dot accent. Click en cualquier celda → drill a/calendario/[YYYY-MM-DD](vista día). Multi-hipódromo en mismo día: stacked dentro de la celda (Revista Palermo lo hace igual cuando coinciden ROSARIO + SAN ISIDRO). - ·Toggle Día ↔ Mes (
ViewToggle.tsx) en el header de ambas vistas.role="tablist"+aria-selectedpara a11y. Vista día → toggle linkea al mes correspondiente (date.slice(0, 7)); vista mes → toggle linkea al día de hoy. Mantiene continuidad de navegación. - ·Navegación entre meses con ← Abr 2026 / Jun 2026 → arriba de la grilla (mes corto localizado). Reset de mes vía toggle Día → Hoy.
- ·Query nueva `getMonthSummary(ym)` en
lib/queries/calendario.ts. Una sola query trae todas las ediciones del mes concarrera.grade+hipodromo.{slug, nombre, nombre_corto}joined; agrupa server-side enMap<fecha, Map<hipodromo_slug, { count, hasG1 }>>para emitirCalendarioMonthCell[]listo para render. Filtracarrera.estado === 'published'(admin drafts no contaminan vista pública). - ·Strings nuevas en namespace
Calendario:metadata.titleMonth/descriptionMonth,monthlySubtitle,nav.viewDay/viewMonth/viewToggleLabel. Localizadas en es y en. Formato del mes header viaIntl.DateTimeFormat({ month: 'long', year: 'numeric' })por locale.
- ·Vista mensual con grilla 7 cols Lun→Dom (
- ·`generateStaticParams` en `/calendario/[date]` para ISR. Función nueva
getAllDatesWithMeetings()retorna todas las fechas únicas con ediciones cargadas (4 hoy: 1951-09-15, 2026-09-13, 2026-10-17, 2026-11-14, 2026-12-12 — wait, 5). Esas se generan estáticas en build → primer hit es instantáneo. Las fechas no listadas siguen rendeando dinámicamente — el empty state es barato (1 query agetNextScheduledMeeting). Try/catch defensivo: si la query falla en build, devuelve array vacío → todas dinámicas (no rompe build). Cuando la temporada se cargue completa (~150 fechas/año), serán todas pre-generadas para velocidad y SEO. - ·Calendario funcional mediano — filtros UI, region wiring y JSON-LD `SportsEvent`. Tres ítems atacados en un solo sprint para llegar al "100%" de la vista A.
- ·Filtros UI hipódromo + grade vía URL params (
?hipodromo=palermo&grade=G1). ComponenteCalendarioFilters(client) renderiza chips por cada hipódromo y grade que aparece en el día (derivados server-side desde los meetings antes de filtrar — los chips son "narrowing", no "discovery"). Click en chip →router.pusha la URL filtrada. "Quitar filtros" reset rápido. Las filas de filtro sólo se renderizan si hay 2+ opciones (no tiene sentido un chip "todos / palermo" si Palermo es lo único). El query no se duplica: trae todo el día y filtra en JS sobre el array (la escala — máx ~30 ediciones/día — hace que filtrar 30 rows en memoria sea irrelevante en costo). Los meetings que quedan vacíos tras filtro por grade se omiten (no se renderea card "PALERMO 0 carreras"). El JSON-LD respeta el filtro — sólo emite los SportsEvents visibles. - ·`RegionFilterChip` + `RegionGated` + `RegionAwareList` wireados — última sección de descubrimiento que ignoraba el filtro de región (doc 23 §6) ahora alineada con
/caballos,/jockeys,/studs,/hipodromos,/carreras,/entrenadores. CadaMeetingGroupse envuelve conRegionGated regiones={meeting.hipodromo.regiones}(filtrado por hipódromo, no por edición — la edición hereda la región de su sede).RegionAwareListenvuelve la grilla y, si el filtro activo deja todos los meetings ocultos, renderea el empty state estándar "no hay perfiles cargados para {region} todavía — Ver todos los perfiles" con CTA reset.RegionFilterChipindica el filtro activo arriba de la grilla cuando ≠ INTERNACIONAL. - ·JSON-LD `SportsEvent` por edición (doc 07 lista JSON-LD como non-negotiable para contenido público). Componente
EventsJsonLdserver-side emite un solo<script type="application/ld+json">con el array de todos los eventos visibles del día. Por evento:name(nombre carrera),startDate(YYYY-MM-DDTHH:MM:00-03:00— Argentina UTC-3 sin DST),sport: "Horse racing",eventStatus: EventScheduled,eventAttendanceMode: OfflineEventAttendanceMode,urlal perfil de carrera,location(Place con nombre + URL al perfil del hipódromo),description(distancia + superficie + grade), ycompetitor(caballo ganador si la edición ya corrió). Excluye ediciones canceladas/suspendidas. URLs respetan locale prefix (/en/...para inglés, sin prefijo paraesporlocalePrefix: 'as-needed'). Cuando el campohipodromo.timezonese popule por hipódromo (ya está en schema desde 0003 pero deferido), se cambia el offset hardcodeado porformatInTimeZoneper-meeting para soportar Hipódromo de Tokio o Santa Anita correctamente.
- ·Filtros UI hipódromo + grade vía URL params (
- ·Calendario polish quirúrgico — 5 fixes de pulido sobre el shell vista A. Ataca los detalles que separan "funcional" de "100%" después de Kevin ver el primer render.
- ·Breadcrumbs visibles en
CalendarioView(Inicio › Calendario › 7 mayo 2026). Las strings ya existían (Calendario.breadcrumbs); inconsistencia con el resto del sitio resuelta. Mismo pattern quePedigreeBreadcrumbs: nav inline conaria-label="breadcrumb"y typography uppercase tracking-wider. - ·Empty state con fecha humanizada en lugar del crudo "en 129 días". Lógica en
EmptyState.tsx:daysUntil <= 0→ "hoy",=== 1→ "mañana",<= 6→ "el sábado",> 6→ "el sábado 13 de septiembre" (Intl.DateTimeFormat localizado es-AR / en-US). String del namespaceCalendario.empty.nextOnecambia de{days, plural, ...}a{when}— la decisión de cómo expresar el tiempo queda en el componente, no en ICU. - ·Entrenador + stud ganador en `EdicionRow` post-corrida. Cierra inconsistencia con cross-link audit Fase 2:
edicion.ganador_entrenador_idyedicion.ganador_stud_idya estaban en schema desde 0004 — ahora se pidan en el SELECT del query y se renderean linkeables a/entrenadores/[slug]y/studs/[slug]debajo del jockey. Format: "Ganador: Yatasto con Leguisamo · entrenado por Etchechoury · stud Vacación". Connectors (con,· entrenado por,· stud) externalizados a i18n para que en/es puedan usar fraseo natural distinto. - ·Going (`condiciones_pista`) elevado al header del meeting cuando es uniforme entre todas sus ediciones. Racing Post pattern: si las 12 carreras de Palermo dicen "arena buena", se muestra una sola vez en el header del meeting en lugar de 12 veces en cada fila. Si difieren entre ediciones (ej. cambia el estado a mitad de jornada), no se eleva — queda implícito el manejo per-fila para fase futura. Lógica:
Setsobre loscondiciones_pistano-vacíos; uniform si tamaño 1 y matchea total de ediciones. - ·Quick-link con dot indicator (●) en días que tienen ediciones cargadas.
DateNavhace una sola query batched agetDatesWithMeetings(today, today+7)y pasa elSet<string>a cadaQuickLink. Dot color: accent si quick link inactivo, on-accent si activo (siempre visible). Falla gracefully: si la query rompe, los dots se omiten pero los links siguen funcionando. Le da "respiración" al header sin agregar columnas — en el render con sólo 4 fechas seedeadas (sept-dic 2026), los dots están latentes pero la mecánica está lista para cuando se cargue la temporada.
- ·Breadcrumbs visibles en
- ·`/calendario` funcional — vista A (lista por día agrupada por hipódromo). Hasta hoy
/calendarioera un PlaceholderPage. Ahora es la vista principal de descubrimiento de carreras: para cualquier fecha (/calendario= hoy en hora AR,/calendario/[YYYY-MM-DD]= fecha específica) lista los meetings agrupados por hipódromo, con cada edición mostrando hora, nombre de la carrera (linkeable a/carreras/[slug]), grade chip, distancia, superficie, premio + estado (programada/corrida/suspendida/cancelada). Estadocorridaademás muestraGanador: <Caballo> con <Jockey>linkeados a sus perfiles — la misma URL sirve antes y después de la corrida (ataca el principal pain point del rubro: "previous races much harder to find" — Degree53 horse racing UX review).- ·Investigación competitiva primero per
feedback_competitive_research_before_design: relevé Racing Post racecards, At The Races, Equibase, Revista Palermo (único calendario funcional argentino — grilla mensual con[HIPÓDROMO]+ trofeo + count), TurfDiario, San Isidro. Patrón ganador del rubro: lista por día agrupada por hipódromo con date picker URL-driven + quick links (Today/Tomorrow/+5d) + filter chips por país. Adaptado a Turfdex sin el ruido de bookmaker logos / odds / popups (ventaja por no ser sitio de apuestas). - ·Migration 0014 (
20260507120005_seed_ediciones_2026.sql): seed honesto de las 4 ediciones programadas 2026 de la Cuádruple Corona (Polla 13-sept, Jockey Club 17-oct, Nacional 14-nov, Pellegrini 12-dic), respetando elmes_tradicionalreal de cada G1. Estadoprogramada, sin ganadores. Idempotente víaunique (carrera_id, ano). Cero data inventada — la data de hoy/esta-semana queda con empty state útil hasta que se carguen carreras menores. Decisión documentada vs alternativas (carreras genéricas mockup tipo "Handicap Mayo" rechazadas por contaminar data real, alineado confeedback_quality_barybases-no-negociables§ "datos verificables"). - ·Empty state es feature de primera clase: si la fecha consultada no tiene reuniones, llama a
getNextScheduledMeeting(afterDate)y muestra "Sin reuniones programadas para esta fecha — Próxima reunión: GP Polla en Palermo (2026-09-13) — en 129 días" con CTA directo a esa fecha. Si no hay próximas tampoco, ofrece/contribuirpara que un fan sume fechas históricas. Ataca otra queja del rubro: "no reminders" — al menos te decimos cuándo es lo próximo y te llevamos sin click adicional. - ·Query `lib/queries/calendario.ts` con tres helpers:
getEdicionesByDate(date)(joins edicion → carrera → hipodromo + ganador_caballo + ganador_jockey, filtracarrera.estado='published', agrupa en JS por hipódromo),getNextScheduledMeeting(afterDate)(próximaprogramadaconcarrera.estado='published'),getDatesWithMeetings(start, end)(preparación para futura vista mensual). Todo viacreatePublicClient(anon, RLS) — la página entera es indexable. - ·Componentes nuevos en `components/calendario/`:
CalendarioView(header + nav + lista o empty state),DateNav(server: prev/next + date picker + quick links Hoy/Mañana/+5 días con highlight del día activo),DatePicker(client:<input type="date">conrouter.push(\/calendario/\${value}\)— uncontrolled para no violarreact-hooks/set-state-in-effect),MeetingGroup(card por hipódromo con header linkeado, count plural ICU, ventana horaria primera→última carrera),EdicionRow(fila con monoespaciado para hora, chip grade, distancia + superficie + premio inline, sección ganador condicional),EmptyState(con next-meeting fallback + contribuir CTA). Todos los componentes fetchean traducciones server-side viagetTranslations({ locale, namespace }). - ·Páginas:
app/[locale]/calendario/page.tsx(rebind del placeholder existente) calcula "hoy AR" viaformatInTimeZone(new Date(), 'America/Argentina/Buenos_Aires', 'yyyy-MM-dd')(date-fns-tz ya estaba en deps).app/[locale]/calendario/[date]/page.tsxvalida formatoYYYY-MM-DDcon regex + chequeo de roundtrip antes de renderear (notFound() en input inválido). Ambas congenerateMetadata+getSeoAlternatespara hreflang correcto. - ·i18n: namespace
Calendarioen es/en con sub-namespacesnav,filters,meeting,edicion,empty,breadcrumbs. Plurales ICU para count de carreras y countdown de días. Days of week localizados. Estados de edición (programada/corrida/suspendida/cancelada) con strings propios. - ·Out of scope deliberado (per
feedback_iterate_from_renders— shipear shell primero, iterar después): vista mensual estilo Revista Palermo, vista semanal, filtros por hipódromo + grade (UI ya pensada en messages), JSON-LDSportsEventpor edición (lo agrego cuando Kevin valide el shell). El querygetDatesWithMeetingsya queda listo para alimentar una grilla mensual cuando llegue el momento.
- ·Investigación competitiva primero per
- ·Búsqueda global wireada con backend híbrido (Phase A + Phase B en un solo sprint). Cierra el último gran ítem invisible — el SearchModal Cmd-K era un shell sin wiring desde Fase 1c. Ahora funciona end-to-end y el backend está al nivel "best practice" del rubro para esta escala (Postgres-native, auto-update, future-replaceable).
- ·Migration 0012 (
20260507120003_search_hybrid.sql): índices GIN de expresión sobreto_tsvector('spanish', nombre + bio.es)para las 6 entidades + rewrite desearch_globalRPC. El RPC ahora combina (a)similarity()trigram para typo/partial/accent, (b)ts_rank()sobre tsvector para relevance phrase, (c) boost +0.2 cuandofeatured_rank IS NOT NULL. Match condition es OR entre los dos modos — un typo simple ("Forly") cae al trigram, "cuádruple corona 1951" cae al tsvector. Salida del RPC extendida conbio_snippet(160 chars) +scorecompuesto. Auto-update sin código de sync: Postgres re-indexa los expression GIN automáticamente en INSERT/UPDATE — alineado con pedido explícito de Kevin "se va actualizando de manera automática, no tenemos que actualizarlo nosotros de nuevo". - ·Server action `searchModalAction(query)` en
lib/actions/search-modal.ts— única superficie cliente↔backend. Si mañana migramos a Meilisearch / Typesense / pg_search, reescribimos solo el cuerpo de esta función; el modal cliente no se entera. Decisión arquitectónica para el "probablemente lo terminemos borrando" del pedido original. - ·SearchModal refactor completo (
components/site/SearchModal.tsx): cliente conquerycontrolled, debounce 150ms, abort flag para race-conditions cuando el usuario tipea rápido, status derivado ('idle' | 'too-short' | 'loading' | 'results' | 'no-results') sin setState en effect (react-compiler-friendly). Resultados agrupados por entity_type en orden fijo (caballos, jockeys, studs, hipódromos, carreras, entrenadores) con conteo(N)por grupo aunque sea(0)— feedback de cobertura per doc 19 §3.2. Highlight de matches case+accent insensitive con<mark>(multi-word: cada palabra se busca por separado). Keyboard nav: ↑↓ navega cross-grupo, ↵ abre el seleccionado víauseRouterde next-intl (locale-aware), esc cierra. Hover también selecciona. Auto-scroll del item activo a la vista. Empty state con 3 sugerencias contextualizadas + CTA/contribuir/new-entity?nombre=<query>(passa el texto buscado para prefill). Loading state, hint de comandos al pie, label aria-modal. - ·SiteHeader: cambia de
<SearchModal open={searchOpen} ...>a{searchOpen && <SearchModal ...>}. El conditional mount fuerza fresh state en cada apertura sin necesidad de un useEffect de reset (que violaría react-hooks/set-state-in-effect en React 19). - ·i18n: namespace
Searchreescrito conhintIdle,hintTooShort,loading,noResultsTitle/Tip1/Tip2/Tip3/Cta,groupCount,navHinty los 6 nombres de grupo (groupCaballo,groupJockey, etc.) en es/en. String legacyemptyShellremovido (la espera terminó). - ·Tipo
SearchResultextendido:bio_snippet: string+score: number(compuesto). Compatible con consumidores existentes (searchEntitiesForPickeren/contribuir) — solo agrega campos. - ·Investigación documentada en CHANGELOG: comparé pg_trgm-only / tsvector / pg_trgm+tsvector híbrido / pg_search (ParadeDB) / pg_textsearch / Meilisearch / Typesense / Algolia. Para escala MVP (~50-2K entidades) y requisito "auto-update sin sync layer", Postgres híbrido gana sin discusión. Path a Meili sigue abierto como Phase C cuando aparezca pain real (>5K entidades + phrase queries específicas).
- ·Migration 0012 (
- ·Cross-link audit Fase 2 — 4 fixes de descubrimiento bidireccional (cierra los gaps obvios identificados al mapear las 6 entidades x 6 entidades). Hasta hoy varios FK existentes en DB no se renderizaban → un fan abriendo Yatasto no veía quién lo crió ni quién lo entrena, y la página de una carrera mostraba el jockey ganador pero NO el entrenador ganador (data ya estaba en
edicion.ganador_entrenador_id).- ·Caballo Hero: 3 chips nuevos linkeados —
Criador(stud_criador_id),Stud actual(stud_actual_id, omitido si igual al criador para no duplicar),Entrenador(entrenador_actual_id). Cuando criador = actual, el chip se muestra como "Stud" a secas. QuerygetCaballoBySlugextendida con los 3 joins; tipoCaballoDetailextendido constud_criador,stud_actual,entrenador_actual(todos{slug, nombre} | null). i18n keysCaballo.chip.{stud,studCriador,studActual,entrenador}en es/en. - ·CarreraEdiciones: nueva línea "Entrenador:" debajo del jockey en cada edición ganada.
edicion.ganador_entrenador_idya existía en schema desde 0004 — solo faltaba pedirlo en el SELECT y renderizarlo. QuerygetCarreraBySlugy tipoCarreraEdicionEntryextendidos. i18n keyCarrera.ediciones.entrenadorLabel. - ·Stud → entrenadores trabajando ahí: nueva sección en
/studs/[slug]listando entrenadores dondestud_trabajo_principal_id = stud.idostud_propio_id = stud.id. Cada entrada con vinculo: "Trabajo principal" o "Stud propio" (doc 18b §1). Query inversa conor(...)filter. Sección entera no renderiza si la lista está vacía. Componente nuevocomponents/stud/StudEntrenadores.tsx. i18n keysStud.entrenadores.{title,subtitle,vinculoPrincipal,vinculoPropio}. - ·Jockey → top entrenadores frecuentes: nueva sección en
/jockeys/[slug]con top 5 entrenadores con quienes el jockey llevó más monturas, mostrando # de montas y # de victorias por entrenador. Agregación en JS sobre la marcha (un solo loop sobre los rows de palmares — no segundo round-trip a la DB). Tie-break por victorias desc. Componente nuevocomponents/jockey/JockeyEntrenadoresFrecuentes.tsx. i18n keysJockey.entrenadoresFrecuentes.{title,subtitle,countLine}con ICU plurals.
- ·Caballo Hero: 3 chips nuevos linkeados —
- ·Render público de fuentes citadas en los 6 perfiles (cierra item de Fase 3.1 — el editor admin existía desde commit
575a412pero las fuentes no se veían en público). Bloque "Fuentes" al pie de cada perfil (entre la última sección de datos y elFuenteFooter) listando todas las citas de la entidad. Cada item: badge confield_nameformateado a uppercase con espacios (ej.fecha_nacimiento→FECHA NACIMIENTO) o "Perfil completo" si el field es null, nombre de la fuente (o hostname como fallback si solo hay URL) clickeable a la URL original (target=_blank rel=noreferrer noopener), hostname auxiliar y fecha formateada viaIntl.DateTimeFormatpor locale. La sección entera no renderiza si la entidad no tiene sources cargadas (regla "secciones sin contenido desaparecen", doc 13). Falla resiliente: si la query rompe, loggea a console.error y oculta la sección — los datos secundarios nunca rompen el perfil entero.- ·Query
getSourcesForEntity(entityType, entityId)enlib/queries/sources.tsusacreatePublicClient(anon). Sort: nullsFirst por field_name (perfil completo arriba) + created_at desc dentro de cada bucket. RLSsources_public_read using (true)ya permitía SELECT a anon desde migration 0010 — no se tocó la DB. - ·Componente
components/site/SourceCitations.tsx(server async). TipoentityTyperestringido a las 6 entidades con perfil público. HelperhumanizeFieldNamepara los badges,safeHostnamecon fallback al URL crudo si parsea mal. - ·Wireado en los 6 perfiles públicos:
/caballos/{slug},/jockeys/{slug},/studs/{slug},/hipodromos/{slug},/carreras/{slug},/entrenadores/{slug}. - ·i18n: namespace
SourceCitationsen es/en contitle,subtitle,fullProfileLabel.
- ·Query
- ·PedigreeTree 5 generaciones + página dedicada `/caballos/{slug}/pedigree` (cierra item de Fase 3.1 y matchea estándar industria). Hasta hoy el perfil mostraba solo 1 generación (padre + madre); ahora hay (a) preview inline de 3 generaciones en el perfil del caballo y (b) página dedicada con árbol completo de 5 generaciones (hasta tatara-bisabuelos = 31 nodos máx). Layout estándar del rubro: sire arriba / dam abajo, generaciones izquierda → derecha. Mobile: la grilla mantiene
min-widthfijo +overflow-x-auto, scrolleable con swipe — captura el hueco que dejan los rivales (Pedigree Online, Pedigree Query, Stud Book Argentino siguen siendo desktop-first o tienen mobile en beta). Cada celda con datos linkea al perfil del ancestro si está published; ancestros draft renderizan el nombre sin link + badge "Perfil no publicado"; ancestros desconocidos muestran card dashed con CTA "Sugerir perfil →" en G2 (en G3+ el flujo natural es navegar al ancestro intermedio). G1 destacado con bg accent-soft. Decisión documentada vs doc 12 ("no sub-páginas para profundidad"): pedigree dedicada SUMA SEO independiente para queriespedigree {nombre}(convención del rubro, ej. Stud Book Argentino/ejemplares/pedigree-desarrollado/...) — excepción justificada, no precedente para otras secciones.- ·Migration
20260507120002_pedigree_function.sql: función SQLget_pedigree_tree(root_id uuid, max_depth int)con CTE recursivo respetando RLS (anon ve solo published, admin ve todo viais_admin()). Convención Ahnentafel:n → padre = 2n, madre = 2n+1. Costo ~O(2^depth) ≈ 31 reads para 5 gen, todas por PK con índicescaballo_padre_idx/caballo_madre_idxexistentes. - ·Query helper
lib/queries/pedigree.ts:getPedigreeTree(rootId, maxDepth)(anon, para rutas públicas) +getPedigreeTreeForAdmin(rootId, maxDepth)(server client con cookies — admin ve drafts). TiposPedigreeNodeyPedigreeTreecon lookupbyPosition. - ·Componente
components/caballo/PedigreeTree.tsx(server) renderiza la grilla via CSS Grid: cada celda a profundidaddocupa2^(maxDepth-d)filas para alinear visualmente con sus descendientes. Tipografía decreciente con la profundidad (G1 serif xl → G5 serif sm). Año mostrado en G1-G3, omitido en G4-G5 para densidad. - ·
CaballoPedigree(existente) reescrito como wrapper de preview 3 gen + CTA "Ver árbol completo de 5 generaciones →". - ·Página
/[locale]/caballos/[slug]/pedigree/page.tsxcongenerateStaticParamsdesdegetCaballoSlugs, metadata propia (titlePedigree de {nombre} — Turfdex, description SEO-friendly), breadcrumbs, swipe hint en mobile y back link al perfil.getSeoAlternatespara hreflang correcto. - ·i18n: namespace
Caballo.pedigreeextendido conpreviewSubtitle,fullSubtitle,viewFullTree,noPedigree,draftNotice,fullPageTitle,fullPageMetadataTitle,fullPageMetadataDescription,backToProfile,swipeHint. Strings legacyfirstGenydeferNoteremovidos (la nota de iteración pendiente quedó obsoleta — la iteración se shippeó). - ·Admin form de caballo (
CaballoForm) suma link "Ver pedigree de 5 generaciones (sitio público) →" debajo de los pickers padre/madre cuando el caballo está published, para validación visual sin tener que ir al header. El editor de pedigree sigue siendo los pickers padre/madre existentes (EntityIdSelectconexcludeIdpara evitar self-FK) — autocomplete diferido siguiendo "no tech without pain" (a escala actual ~15 caballos, el<select>nativo alcanza). - ·Investigación de mercado documentada en doc 02 §"Benchmark pedigree": 5 generaciones es estándar industria (Equineline FREE 5-Cross, Pedigree Query); Stud Book Argentino usa página dedicada con QR para vincular pedigrees impresos al digital; Pedigree Online tiene quejas explícitas en reviews ("archaic", "not mobile friendly", "trying to use it on devices smaller than iPad is extremely problematic") — confirma mobile como hueco real que podemos liderar.
- ·Migration
- ·Modo oscuro con detección automática del sistema y toggle en header: paleta editorial cálida (warm near-black
#14110fen lugar de pure black) con bordó "lifted" a#c8485cpara mantener WCAG AA sobre fondos oscuros (el bordó claro#7b1f2fno pasaba contraste en oscuro). Tres estados: Sistema (default — sigueprefers-color-scheme), Claro, Oscuro. Toggle cíclico en el header (entre LocaleToggle y UserMenu) con íconos sun/moon/monitor; tooltip y aria-label traducidos al estado actual. Persistencia enlocalStorage['turfdex-theme']. Anti-FOUC: script inline sincrónico en<head>aplica la clase.darkal<html>antes del primer paint, así el primer frame ya está en el tema correcto. La paleta se implementa via swap de CSS vars (:root.dark { --color-bg: ...; ... }) — todos los componentes existentes (header, footer, fichas de las 6 entidades, admin shell, search modal, drawer móvil, dev banner) se adaptan automáticamente porque ya consumían los tokens viavar(--color-X).color-scheme: light/darktambién seteado para que scrollbars y form controls nativos respeten el modo. Sin librería externa (descartadonext-themespor regla "no tech without pain" — el problema cabía en ~80 LOC). - ·Link "Panel de administración" visible en avatar dropdown cuando el usuario es admin: hasta hoy se entraba a
/adminsolo via URL directa. Ahora elUserMenuchequeaisAdminEmail(email)y, si sos admin, suma un link "Panel de administración" arriba del separador con la "Mis sugerencias". Resuelve fricción real (Kevin abriendo /admin a mano cada vez). - ·Refactor:
ADMIN_EMAILSextraído alib/admin/emails.ts(archivo neutro sin imports server-only) para reusar entrelib/admin/auth.ts(server, redirect en/admin/*) ycomponents/site/UserMenu.tsx(client, decisión de mostrar el link). HelperisAdminEmail(email)para no repetir el.includes(). Source of truth sigue siendo la función SQLis_admin()— el array TS es defense-in-depth UX. - ·Doc operativo en
supabase/README.md§ "Admin access" explicando cómo se hace admin en Supabase (vs el modelo Firebase de custom claims) — qué haceis_admin(), dónde se duplica la lista, cómo sumar otro admin, cuándo migrar a tabla de roles. - ·SourceCitationEditor — admin para citar fuentes (cierra item de quality bar de Fase 3.1). La tabla
sourcesestaba pre-armada desde 0002 pero las fuentes se cargaban via SQL. Ahora cada admin form de entidad tiene una sección "Fuentes" debajo del form principal donde se pueden agregar / editar / eliminar citas inline. Cada fuente vincula unfield_nameopcional (ej:fecha_nacimiento,padre,palmares_1951) — vacío significa "avala el perfil entero" — más URL (required) + nombre (recomendado) + fecha + notas.- ·Schema Zod
sourceInputSchemaenlib/admin/schemas/source.tscon discriminator entre insert y update viaidopcional. - ·Server actions
saveSource(insert/update unificado por id) ydeleteSourceenapp/admin/sources/actions.ts. Ambas resuelven slug del entity target vialooseClienty revalidatePath del perfil público en es y en (ya que las sources eventualmente se renderizan ahí). - ·Query
listSourcesForEntity(entityType, entityId)enlib/admin/queries-source.tscon sort: perfil entero primero (nullsFirst), después por field_name. - ·Componente
SourcesSection(server) que carga las sources y delega al clientSourcesList. Lista con cada fuente como row colapsada (badge field_name + nombre + fecha + URL clickeable) y acciones inline Editar / × (con confirm-twice para borrar). "+ Agregar fuente" abre form inline arriba de la lista. Edit reusa el mismo form en lugar de la row. - ·Wireado en las 6 admin pages de entidad:
/admin/{caballos,jockeys,studs,hipodromos,carreras,entrenadores}/[id]/editahora muestran SourcesSection debajo del form principal. En carrera, queda debajo de EdicionesSection.
- ·Schema Zod
- ·PalmaresEditor — admin para ediciones + inscriptos (cierra item bloqueante de Fase 3.1). Hoy las relaciones
caballo_carreray las filas deedicionse cargaban vía SQL directo; con esto Kevin puede armar el palmarés histórico de cualquier carrera desde admin sin tocar SQL.- ·Schemas Zod:
edicionInputSchema(año, fecha, hora, premio_*, ganador_*, tiempo_ganador, condiciones, observaciones, estado) einscriptoInputSchema(caballo_id, jockey_id, entrenador_id, puesto, tiempo, diferencia, peso_kg, dividendos, notas) enlib/admin/schemas/edicion.ts. - ·Server actions en
app/admin/carreras/[id]/ediciones/actions.ts:saveEdicion(input, mode)con manejo de unique constraint (carrera_id, ano),deleteEdicion(id)con resolve previo del slug para revalidatePath, ysaveInscriptos(edicionId, rows[])que sincroniza la lista (delete missing + upsert resto). Todas conrequireAdmin()yrevalidatePath('/carreras/{slug}'). - ·Queries
lib/admin/queries-edicion.ts:listEdicionesForCarrera,getEdicionForEdit,listInscriptosForEdicioncon joins a caballo/jockey/entrenador para mostrar nombres,getCarreraSummarypara contexto, pickers de jockey y entrenador. - ·Sección "Ediciones" en
/admin/carreras/[id]/edit(debajo del CarreraForm) con tabla de ediciones existentes (año, fecha, estado pill, ganador con cross-link al perfil público, tiempo, link "Editar →") + botón "+ Nueva edición". - ·
/admin/carreras/[id]/ediciones/nuevo— form de edición con datos básicos + ganador. Save → redirect a[edicionId](donde aparece el grid de inscriptos). - ·
/admin/carreras/[id]/ediciones/[edicionId]— form de edición + grid de inscriptos. Grid con scroll horizontal (min-width 1100px), columnas: puesto, caballo (required), jockey, entrenador, tiempo, diferencia, peso_kg, notas, [×]. "Agregar inscripto" suma row vacía. "Guardar inscriptos" hace bulk save vía la action — el algoritmo detecta deletes (rows que ya no están en el array) y hace insert/update del resto. Confirm-twice para borrar la edición completa.
- ·Schemas Zod:
- ·`/sugerencias` — vista user-side de contribuciones: nueva página
/sugerencias(auth gate server-side, robots noindex) con dos secciones — "Correcciones de biografía" lista lospending_editspropios del usuario logueado, "Contribuciones libres" lista lassubmissionspropias. Cada item con status pill (color por estado), tipo, target con cross-link al perfil público, fecha (Intl.DateTimeFormat es-AR), preview del payload truncado a 280 chars + admin notes si fueron escritas. Empty states accionables linkeando a/contribuir. RLS hace su trabajo automático: la query trae solo lo del usuario porque las policies restringenuser_id = auth.uid()/submitter_id = auth.uid(). - ·Helper
listMyContributions()enlib/queries/my-contributions.tsque trae pending_edits + submissions del usuario logueado en paralelo + resuelve nombres/slugs de las entidades target en una sola pasada batched (sin N+1). - ·UserMenu: link "Mis sugerencias" →
/sugerenciasre-habilitado entre el header del email y el botón de cerrar sesión. Cierra el dropdown al click. i18n keyNav.user.mySuggestionsen es/en. - ·Etapa 3 — Roadmap update (3.5): doc 05 §3.1 actualizado para reflejar Etapa 3 cerrada (3.0–3.4), ImageUploader bajado en prioridad con justificación (Kevin carga imágenes via consola Storage mientras carga contenido seed). Memoria
project_turfdex.mdactualizada al estado 2026-05-07. - ·Etapa 3 — Página /sobre real (3.4): reemplaza el placeholder por una página con misión + qué somos / qué no somos (sin betting, sin foros, capa de experiencia sobre datos públicos) + cómo construimos contenido (curaduría editorial + fuentes obligatorias + audit trail) + sección "cómo podés contribuir" con CTA al
/contribuir+ créditos. Server component, i18n via namespaceSobre(es/en). Items con bullets renderizados viat.markuppara soportar bold dentro del JSON sin tener que dividir cada string en 3 keys. - ·Etapa 3 — Hooks contextuales (3.3): enchufamos los puntos de entrada al sistema de contribuciones desde el resto del sitio. Footer de los 6 perfiles ahora monta
ProfileFuenteActions(combo: SuggestBioEdit existente + 2 links nuevos "Sumar info" →/contribuir/extra-info?et=X&eid=Y+ "Reportar error" →/contribuir/error-report?et=X&eid=Y). Los links nuevos sonLinksimples — el auth gate lo maneja la sub-page server-side via redirect a/login?next=. Empty state del SearchModal global ahora tiene footer con link "¿No encontrás algo? Proponé un perfil →" →/contribuir/new-entity(sin nombre porque la search real todavía es shell). PedigreeCard de caballo ahora muestra link "Sugerir perfil →" en la card del padre/madre cuando no existe →/contribuir/new-entity?et=caballo(cierra el escape hatch de doc 14 §C "ancestro NO tiene perfil"). Strings nuevos enContribuir.profileActions,Contribuir.searchEmpty,Contribuir.pedigree(es/en). - ·Etapa 3 — Cola admin /admin/submissions (3.2): nueva sección admin para revisar contribuciones libres.
/admin/submissionslista filtrable por status (pending/in_review/needs_info/applied/rejected) y por type (new_entity_proposal/extra_info/error_report) vía URL search params. Por defecto muestra status=pending, todos los tipos. Filtros como chips con badge de color por status. Tabla con tipo, summary del payload, target (entidad si aplica), submitter UUID corto, fecha./admin/submissions/[id]detalle con: header de status + payload card específico por tipo (NewEntityProposal muestra nombre/descripción/datos extra; ExtraInfo muestra texto formateado; ErrorReport muestra campo + valor_actual + valor_correcto + descripción), source card si hay, admin notes card si ya fue moderada, y panel de acciones lateral con dropdown de status + textarea de admin notes + sección colapsable "vincular entidad" (applied_entity_type + UUID para trazabilidad cuando se aplica la submission al crear/editar la entidad). Botón Guardar disparaupdateSubmissionStatusque actualiza la submission + escribe enmoderation_log. - ·Sidebar admin: grupo "Moderación" ahora tiene 2 entries — "Bio edits" →
/admin/pending-edits(renombrado desde "Sugerencias" para distinguir del nuevo) y "Contribuciones" →/admin/submissions. - ·Etapa 3 — UI pública /contribuir (3.1): nueva página
/contribuir(server, sin auth gate — hub público invitando) con 3 cards que linkean a 3 sub-pages dedicadas:/contribuir/new-entity,/contribuir/extra-info,/contribuir/error-report. Cada sub-page hace auth gate server-side (redirect a/login?next=/contribuir/{tipo}si no logueado), lee query params para prefill (?et=<entity_type>,?eid=<entity_id>,?nombre=<...>,?campo=<...>) y renderiza el form correspondiente. Robots noindex en las sub-pages (no son contenido para SEO). - ·Componentes en
components/contribuir/:NewEntityForm,ExtraInfoForm,ErrorReportForm(client, React Hook Form-style con state local +useTransition+ server action).EntityTargetPickershared para los 2 forms con target — autocomplete debounced (200ms) sobre el RPCsearch_globalfiltrado por tipo, fallback "no encontramos / proponé crear este perfil →" linkeando a/contribuir/new-entity?et=X&nombre=Y.SourceFieldsshared (URL + nombre fuente, recomendado, no obligatorio).SubmissionResultBannershared (idle/submitting/success/error/invalid). - ·Server action
searchEntitiesForPicker(query, type)enlib/actions/search-entity.ts(wrapper que filtra el resultado desearchGlobalpor tipo, per_entity=8). HelpergetEntityDisplay(type, id)enlib/queries/entity-display.tspara resolver(entity_type, entity_id)→{display_name, slug}desde URL prefill server-side, sin asumir que el hook contextual conozca el nombre. Soporta los 8 tipos (6 dim + historia + glosario_termino — los últimos leen detitulo.es/termino.es). - ·i18n: namespace
Contribuircompleto enmessages/{es,en}.json(hub, common, entityType, newEntity, extraInfo, errorReport, picker — ~80 strings). - ·Etapa 3 — Schema submissions + plomería (3.0): nueva tabla
submissions(migration20260507120001_submissions.sql) para contribuciones públicas free-form. Tres tipos vía enumsubmission_type:new_entity_proposal("este caballo/jockey/etc no existe"),extra_info("tengo info adicional sobre tal entidad"),error_report("esto está mal, debería decir esto otro"). Workflow status:pending | in_review | applied | rejected | needs_info. Separada depending_editsa propósito: pending_edits es diff a un field conocido con auto-aplicación, submissions es free-form que el admin lee y traduce manualmente a action (crear entidad / editar campo / cerrar). RLS: usuario logueado inserta solo como sí mismo, lee las propias; admin lee/escribe todas. Indexes para cola admin (status+created), "mis sugerencias" (submitter+created), submissions por entidad. Triggertg_set_updated_at()reusado. - ·Server action pública
createSubmissionenlib/actions/submissions.tscon validación Zod por discriminated union (cada tipo tiene su payload schema). Devuelvesuccess | unauthenticated | invalid | error. Valida que entity_id sea uuid para extra_info/error_report; new_entity_proposal lo deja null (la entidad no existe todavía). - ·Queries admin
listSubmissions({status?,type?})ygetSubmissionByIdenlib/queries/submissions.tscon join manual a la entidad target (mismo patrón que pending-edits.ts). Server action adminupdateSubmissionStatusenapp/admin/submissions/actions.tsque actualiza status + notes + opcionalmente la entidad creada/editada (para trazabilidad), y registra enmoderation_log. NO aplica writes automáticos al pasar aapplied— el admin crea/edita la entidad con el flow normal y deja constancia acá. TiposSubmission,SubmissionType,SubmissionStatus,SubmissionPayload(discriminated union de los 3 payloads) agregados alib/supabase/types.ts. - ·Etapa 2 — community edits replicado a las 6 entidades:
/jockeys/[slug],/studs/[slug],/hipodromos/[slug],/carreras/[slug]y/entrenadores/[slug]ahora montan el mismoSuggestBioEditque caballo en suFuenteFooter. Cobertura de community editing completa para todos los perfiles públicos. Sin código duplicado: el componente, el server action y la cola admin sirven a las 6 entidades víaEditableEntityType(=FavoriteEntityType). MensajesJockey.fuente.contactLine,Carrera.fuente.contactLineyEntrenador.fuente.contactLineactualizados en es/en para no duplicar el call-to-action de "sugerir corrección" (ya lo hace el botón). Stud e hipódromo conservan su contactLine porque pide info específica del rubro (información del stud, info histórica del hipódromo) — no compite con el botón. - ·Etapa 2 — community edits (vertical slice) sobre
/caballos/[slug]: cualquier usuario logueado (Google OAuth) puede sugerir una corrección abio.esdesde el footer del perfil. La sugerencia entra apending_edits(RLS pre-armada —user_id = auth.uid()enforced en INSERT). Modal con textarea pre-llenada con la bio actual, validación cliente (50–5000 caracteres, debe diferir de la actual) + server (re-valida + check de auth). El botón redirige a/login?next=…si no hay sesión. - ·Cola de moderación admin en
/admin/pending-edits(lista todas las pendientes con entidad, slug, fecha) y/admin/pending-edits/[id](review side-by-side con diff bio actual vs propuesta + botones Aprobar/Rechazar). Aprobar = merge deproposed_changes.bio.essobreentity.biopreservando otras locales (en/pt/ja se mantienen) + update entidad +pending_edits.status='approved'conreviewed_by/reviewed_at+ insert enmoderation_log. Rechazar = solo marca status + log con motivo opcional.revalidatePathpost-aprobación invalida el SSG público en es y en. Cadena de auditoría completa:pending_edits(quien propuso) +moderation_log(quien moderó) +edits(escritura real, vía triggertg_log_edit). - ·
UserMenuahora reactivo con sesión: server-rendered link a /login para visitas sin sesión, avatar dropdown (inicial del email + email completo + Cerrar sesión) cuando el usuario está logueado. Subscribe aonAuthStateChangepara reflejar login/logout sin recargar. Reusa ellogoutActionya existente. - ·Componentes nuevos:
components/profile/SuggestBioEdit.tsx(auth-aware client component con modal embebido) ylib/actions/suggest-bio.ts(server action con suEditableEntityType— alias deFavoriteEntityType, las 6 entidades con perfil + bio editable). - ·i18n keys nuevas:
Nav.user.{menuLabel,signedInAs,signOut}y bloqueSuggestBiocompleto enmessages/{es,en}.json. MensajeCaballo.fuente.contactLineactualizado: ya no anuncia el formulario "Wikipedia-style" como pendiente (ahora coexiste como acción primaria). Otras 5 entidades mantienen el mensaje legacy hasta replicar el botón. - ·Bulk CSV importer CLI (
npm run import -- <entity> <archivo.csv> [--dry-run]) para cargar entidades en masa desde CSV. Soporta los 6 tipos públicos. Reusa los mismos Zod schemas del admin como fuente de verdad de validación. Idempotente víaUPSERT ON CONFLICT (slug). Reporta por fila (inserted/updated/error con mensaje específico) y exit code 2 en errores. Templates enscripts/import/templates/{entity}.csvy guía enscripts/import/README.md. Auth viaSUPABASE_SERVICE_ROLE_KEYdel.env.local(bypassea RLS para operaciones bulk;edits.user_idqueda NULL — comportamiento documentado en doc 07). Multiplica la velocidad de carga manual ~10x para sesiones de research. - ·Convenciones del CSV: i18n splitea en columnas con sufijo locale (
bio_es,bio_en, etc.), regiones pipe-separated (LATAM|US|EU), relaciones por slug (padre_slug,madre_slug,stud_criador_slug,hipodromo_slug,serie_slug,stud_trabajo_principal_slug) resueltas pre-import via maps in-memory para evitar N+1. Single-pass: las entidades referenciadas deben aparecer en filas previas o ya estar cargadas — error claro si falta el target. Recarga el map del entity actual después de cada inserción para soportar referencias dentro del mismo CSV (caballos hijo→padre). - ·Devdep
tsx(^4.21) para correr scripts TypeScript directos (necesario para reusar Zod schemas en el CLI)..env.localse carga viatsx --env-filenativo, sin agregardotenv. - ·Admin completo wireado a Supabase para las 6 entidades —
/admin/jockeys,/admin/entrenadores,/admin/studs,/admin/hipodromos,/admin/carrerassiguen el mismo patrón que/admin/caballos: lista real con drafts (RLSis_admin()), form con auto-slug + Zod defensa-en-profundidad + region multi-select, server actions consaveX/setXEstado/deleteXque revalidatePath para el SSG público, audit trail vía DB triggers (sin código en server actions). Cada entidad tiene su Zod schema enlib/admin/schemas/{entity}.ts. Pickers cross-entity (stud para entrenador, hipodromo + serie para carrera) cargados server-side desdelib/admin/queries.ts. - ·Componentes compartidos extraídos a
components/admin/forms/:RegionesMultiSelect(chips toggle, vocabulario doc 23) yEntityIdSelect(FK picker genérico). Reusados por los 6 forms. - ·Admin caballos wireado a Supabase real (CRUD vertical slice) —
/admin/caballosreemplaza la lista mock por query real (incluye drafts via RLSis_admin()),/admin/caballos/nuevoy/admin/caballos/[id]cargan/guardan caballos reales con server actions (saveCaballo,setCaballoEstado,deleteCaballo). Validación con Zod schema (lib/admin/schemas/caballo.ts) defensa-en-profundidad: cliente parsea local antes de viajar al server, server re-valida con el mismo schema. Slug auto-generado desde nombre vialib/admin/slug.ts(NFD + strip diacritics + ASCII lowercase, sin libs externas). El audit trail eneditsy el rename log enslug_historyson automáticos vía triggers de DB (tg_log_edit/tg_track_slug_rename) — el código no los toca.revalidatePath('/caballos')y/caballos/[slug]post-save invalidan el SSG. - ·Form CRUD wireado con React Hook Form (
useForm+useWatch+Controller) sin@hookform/resolvers— validación manual con Zod en submit. Errores se setean por field path. Multi-select de regiones como chips toggle. Pickers de padre/madre/stud cargan desdelistCaballosForPicker/listStudsForPicker(server-side). Botones "Guardar borrador" / "Publicar" seteanestadocorrespondiente y submitean. - ·Ruta real `/entrenadores/[slug]` con hero (icono perfil con gorra de entrenador + chips de nacionalidad, rango de carrera y stud principal cross-link), biografía narrativa, palmarés agrupado por año (cross-link a
/carreras/{slug},/hipodromos/{slug}y/caballos/{slug}por la montura entrenada viacaballo_carrera.entrenador_id), sección "Caballos entrenados" leyendo decaballo_entrenadorcon períodos desde/hasta, JSON-LD Schema.orgPersonconjobTitle: "Entrenador"y página 404 dedicada. - ·Listing
/entrenadoresreemplaza el placeholder por cards reales conEntrenadorCard(icono + nombre + nacionalidad + rango + bio recortada), filtro de región. Juan Carlos Etchechoury seedeado conregiones=[LATAM]como primer entrenador histórico publicado (perfil preliminar — atribuciones a caballos del seed esperan verificación documental). - ·Componentes nuevos en
components/entrenador/:EntrenadorHero,EntrenadorCard,EntrenadorPalmares,EntrenadorCaballos,EntrenadorIcon,PersonJsonLd. Todos Server Components. - ·i18n keys nuevas:
EntrenadoresList,EntrenadorCard,Entrenador,EntrenadorNotFoundenmessages/{es,en}.jsoncon plurales ICU. - ·Cobertura completa de los 6 tipos de perfil públicos:
/caballos,/jockeys,/studs,/hipodromos,/carrerasy ahora/entrenadoressiguen el mismo patrón (hero + chips + bio + sección de cross-link específica + JSON-LD + 404 + listing con region filter). Todas las rutas placeholder de la fase original quedaron reemplazadas por rutas reales. - ·Ruta real `/carreras/[slug]` con hero (icono copa SVG + chips de grade, distancia, superficie, hipódromo cross-link, mes tradicional, edad y categoría/sexo admitido), historia narrativa, sección "Palmarés histórico" año-por-año con cross-link a
/caballos/{slug}y/jockeys/{slug}para cada ganador, sección "Otras carreras de la serie" (Cuádruple Corona) con cross-link a las 3 carreras hermanas, JSON-LD Schema.orgSportsEvent(con location, additionalProperty para distancia/grade/superficie, superEvent para la serie, subEvent[] con últimas 50 ediciones) y página 404 dedicada. - ·Listing
/carrerasreemplaza el placeholder por cards reales conCarreraCard(icono + nombre corto + grade · distancia · superficie · hipódromo + bio recortada), filtro de región. Las 4 G1 (Polla de Potrillos, Jockey Club, Nacional, Pellegrini) seedeadas conregiones=[LATAM]+ bio + narrativa larga. - ·Componentes nuevos en
components/carrera/:CarreraHero,CarreraCard,CarreraEdiciones,CarreraSerie,CarreraIcon,CarreraChips(helper),SportsEventJsonLd. Todos Server Components. - ·i18n keys nuevas:
CarrerasList,CarreraCard,CarreraSuperficie,CarreraSexoAdmitido,Carrera,CarreraNotFoundenmessages/{es,en}.json. - ·Cadena de cross-links de Yatasto cerrada: el palmarés de Yatasto ahora linkea a fichas reales de carrera (Polla, Jockey Club, Nacional, Pellegrini), jockey (Leguisamo) e hipódromo (Palermo, San Isidro). Los 4 perfiles de carrera muestran a Yatasto como ganador 1951 con cross-link al perfil del caballo. Cobertura de SEO interno completa para el seed inicial.
- ·Ruta real `/hipodromos/[slug]` con hero (icono pista oval + chips de ubicación, operador, capacidad, web), historia narrativa, sección "Carreras tradicionales" agrupada por grade (G1, G2, G3, Listed, Otra) con cross-link a
/carreras/{slug}, JSON-LD Schema.orgSportsActivityLocation(sport: "Horse racing" + foundingDate + manager Organization + PostalAddress + maximumAttendeeCapacity) y página 404 dedicada. - ·Listing
/hipodromosreemplaza el placeholder por cards reales conHipodromoCard(icono + nombre corto + ubicación + bio recortada), filtro de región. Palermo y San Isidro seedeados conregiones=[LATAM]. - ·Componentes nuevos en
components/hipodromo/:HipodromoHero,HipodromoCard,HipodromoCarreras,HipodromoIcon,PlaceJsonLd. Todos Server Components. - ·i18n keys nuevas:
HipodromosList,HipodromoCard,Hipodromo,HipodromoNotFoundenmessages/{es,en}.json. La carreranombre_cortotambién se usa en cards para evitar el "Hipódromo Argentino de Palermo" tan largo en grid. - ·Ruta real `/studs/[slug]` con hero (icono SVG haras + chips de fundador, ubicación, hectáreas, web), historia narrativa, sección "Caballos criados" (cross-link a
/caballos/{slug}— el activo de SEO interno más importante del stud), JSON-LD Schema.orgOrganization(con founder + foundingDate + address) y página 404 dedicada. - ·Listing
/studsreemplaza el placeholder por cards reales conStudCard(icono + nombre + ubicación + bio recortada), filtro de región. Haras Vacación seedeado conregiones=[LATAM]. - ·Componentes nuevos en
components/stud/:StudHero,StudCard,StudCaballos,StudIcon,OrganizationJsonLd. Todos Server Components. - ·i18n keys nuevas:
StudsList,StudCard,Stud,StudNotFoundenmessages/{es,en}.json. - ·Forli ahora cross-linkea a su criador:
caballo.stud_criador_idpoblado para Forli → Haras Vacación (atribución histórica documentada). Las demás atribuciones de criador del seed quedan vacías hasta tener fuentes — consistente con el footer "atribuciones de criador requieren verificación documental". - ·Ruta real `/jockeys/[slug]` con hero (silueta SVG + chips de nacionalidad y rango de carrera), biografía, palmarés agrupado por año (cross-link a
/carreras/{slug},/hipodromos/{slug}y/caballos/{slug}por la montura), estadísticas (win/place/show rate sobre el palmarés cargado), JSON-LD Schema.orgPersony página 404 dedicada. Mismo patrón que/caballos/[slug]. - ·Listing
/jockeysreemplaza el placeholder por cards reales conJockeyCard(silueta + nombre + nacionalidad + bio recortada), filtro de región en chip +RegionGated+RegionAwareList(idéntico patrón a/caballos). Leguisamo seedeado conregiones = [LATAM]. - ·Componentes nuevos en
components/jockey/:JockeyHero,JockeyCard,JockeyPalmares,JockeyStats,JockeySilhouette,PersonJsonLd. Ningún componente nuevo de runtime — todo Server Component salvo lo que ya hereda RegionGated/RegionAwareList client-side. - ·i18n keys nuevas:
JockeysList,JockeyCard,Jockey,JockeyNotFoundenmessages/{es,en}.jsoncon plural rules ICU para victorias/carreras y todos los breadcrumbs traducidos. Cross-link Yatasto → Leguisamo (que existía pero apuntaba a placeholder) ahora resuelve a una ficha real. - ·Sistema de patch notes / changelog público (
/changelog) con parser manual de Keep a Changelog y rendering server-side. - ·Footer muestra el número de versión actual como link directo a
/changelog(un solo elemento clickeable, sin etiqueta "Cambios" adicional). - ·
DevBannerarriba del header en todos los ambientes no-producción (preview, development, local), con copy traducido es/en y variante hardcoded ES en el shell admin. - ·Filtro de región en el header (Internacional / LatAm / EE.UU. / Europa / Japón) con persistencia en cookie
tf_region. Picker desktop como dropdown + variante mobile como chips dentro del drawer. - ·Listing
/caballosaplica el filtro client-side: cards no-matcheadas se ocultan, chip "Filtrado por: X" aparece arriba con botón × para resetear, empty state dedicado cuando la región seleccionada deja la lista vacía. - ·Caballos seedeados ahora tienen regiones asignadas: Yatasto / Telescópico / Yataguara =
[LATAM], Forli =[LATAM, US, EU], Selim Hassan =[EU, LATAM]. Filtrado leeregionesdirecto de Supabase (la migration20260506120001ya está aplicada vía CI auto-deploy y los tipos TypeScript regenerados).
Infrastructure
- ·Roadmap (doc 05) refrescado al estado 2026-05-06: Fases 0/1/2 marcadas como cerradas (la Fase 1c absorbió Fase 1+2 originales), sub-bloques 3.1/3.2/3.3 explícitos para distinguir herramientas editoriales (admin + bulk + community edits) vs volumen de contenido vs infra editorial. Referencias actualizadas a Supabase / Next 16 / Edge Functions (antes hablaba de Firestore / Next 15 / Cloud Functions). Pre-launch checklist consolidado (INPI, Vercel env vars, repo backups, rotar service_role, custom auth domain, cookie banner, cacheComponents). Estimaciones de tiempo restante recalculadas (~3–5 meses al lanzamiento). Mapeo Fase 1a/1b/1c (granularidad de CLAUDE.md) → Fase 1+2 original explicitado.
- ·Documento vinculante doc 25 — Versioning + Changelog discipline define SemVer pre-1.0, formato Keep a Changelog, workflow de updates, excepción i18n para entries históricas, y reglas de release ceremony.
- ·Documento vinculante doc 23 — Filtro de región define las 5 decisiones cerradas (scope solo listings, multi-region por entidad, cookie no URL, default Internacional, perfiles siempre completos), vocabulario controlado de 5 buckets (LATAM / US / EU / JP + INTERNACIONAL como filtro UI), modelo de datos (
regiones text[]+ GIN index + constraint check), patrones de filtering, interacción locale × region, y future enhancement de smart suggestion basada en locale. - ·Migration SQL
20260506120001_add_regiones.sqlaplicada a Supabase live vía CI auto-deploy (primer push automático del nuevo workflow). Agrega columnaregiones text[]a las 6 entidades geográficas (caballo, jockey, stud, hipodromo, carrera, entrenador) + constraint check + índices GIN + UPDATE seed para entidades existentes.lib/supabase/database.types.tsregenerado para incluir el nuevo campo.lib/queries/caballo.tsahora haceselect('regiones')directo y se eliminó elSEED_REGIONES_FALLBACKhardcoded. - ·Lección aprendida (workflow
supabase-deploy.yml): NO escribir GitHub secrets a$GITHUB_ENVcuando la password contiene caracteres especiales (&,#). El formatoKEY=VALUEdel env file los interpreta como comentarios después del#y manglea el valor. La solución que andó: pasar password directo al CLI via flag--password "$VAR", sin re-exportarla a env file. Documentado como nota en el workflow. - ·Documento vinculante doc 26 — Supabase-as-code: source of truth + CD documenta el patrón Firebase-style aplicado a Supabase: schema/RLS/índices/funciones SQL viven en
supabase/migrations/(única fuente de verdad), GitHub Actionsupabase-deploy.ymlaplica migrations automáticamente a Supabase live en cada push adevelopment/master, regen de tipos manual documentada, rollback strategy forward-only, lista honesta de risks/limitations. - ·GitHub Actions workflow
.github/workflows/supabase-deploy.ymlcorresupabase db pushen cada push que tocasupabase/**o el workflow mismo. Verifica secrets primero con error claro si falta alguno. - ·GitHub Actions workflow
.github/workflows/ci.ymlmovido al lugar correcto dentro del repo git (antes vivía enE:/Development/hipica-hub/.github/, fuera del repo, por lo que CI nunca corrió desde el rebrand). Ahora corre typecheck + lint en cada PR y push adevelopment/master. - ·Migration SQL
20260506120002_seed_jockey_regiones.sqlaplicaregiones = [LATAM]a Irineo Leguisamo (idempotente). CI auto-deploy la sube en el push. - ·Migration SQL
20260506120003_seed_studs.sqlcrea Haras Vacación (familia de Alvear, criador documentado de Forli) con bio + narrativa larga +regiones=[LATAM]y asignaForli.stud_criador_idal haras. Política conservadora: solo se seedean haras con fuentes verificables; el resto se carga vía admin. - ·Migration SQL
20260506120004_seed_hipodromos.sqlagrega bio + narrativa larga + operador (Hipódromo Argentino de Palermo S.A. / Jockey Club Argentino) +regiones=[LATAM]a Palermo (1876) y San Isidro (1935). Idempotente — solo escribe si los campos están vacíos. - ·Migration SQL
20260506120005_seed_carreras.sqlagrega bio + narrativa larga +regiones=[LATAM]a las 4 G1 de la Cuádruple Corona (Polla de Potrillos, Jockey Club, Nacional, Pellegrini). Idempotente. - ·Migration SQL
20260506120006_seed_entrenadores.sqlsiembra Juan Carlos Etchechoury como primer perfil histórico de entrenador (Argentina, sin atribuciones a caballos hasta verificación documental,regiones=[LATAM]). Política conservadora: el slot existe, las relaciones se cargan después.