Imported from baldeadr/fronteragrande (
AGENTS.md). Install upstream withnpx skills add baldeadr/fronteragrande. Copyright stays with the author.
AGENTS.md — Instrucciones para asistentes de IA
Este archivo es la puerta de entrada para cualquier LLM que trabaje en este proyecto. Léelo completo antes de hacer cualquier cosa. Está escrito en Markdown plano para funcionar con cualquier herramienta (opencode, Claude Code, Cursor, etc.). Es el equivalente técnico de
architecting-a-band/AGENTS.md, adaptado a este proyecto de aplicación web.
1. Qué es este proyecto
- Es una aplicación web tipo base de datos para registrar la escena musical de la frontera grande de Tamaulipas (bandas, DJs, solistas, colectivos, covers y tributos): ciudad, géneros, enlaces de redes, dónde escucharlos/verlos, y actividad detectada desde internet.
- La visión actual: base de datos interactiva y feed públicos de artistas, donde cada proyecto tiene previews de su contenido (miniaturas/videos) y enlaces directos a sus redes (el puente). Diseño mobile-first y con SEO básico, pensando en monetización futura (AdSense/patrocinios) como medio, no como fin.
- Para el alcance y los límites exactos (qué es y qué no es, y cómo crece), ver docs/vision.md (referencia única).
- Es la versión ejecutable de la base de datos documental ESCENA_LOCAL.md del proyecto architecting-a-band (universo artístico del artista). El objetivo es tener stats de la escena y posicionar el proyecto propio frente a la competencia.
- Propietario: ingeniero en mecatrónica con maestría en IA; no escribe código: es arquitecto y director de sistemas de IA. Él decide, el asistente propone y ejecuta lo técnico/documental.
- Idioma de trabajo: español.
- El proyecto debe ser escalable: si funciona a nivel local, crecer a nivel nacional/internacional (el modelo de datos ya lo permite; el mapa de artistas por origen es la siguiente etapa).
2. Rol del asistente de IA
- Eres apoyo técnico: implementas, corriges, documentas y verificas. No tomas decisiones creativas ni definitivas.
- SÍ puedes: modificar el código, la base de datos, añadir adaptadores de scraping, crear/actualizar documentación, proponer mejoras.
- NO puedes: inventar datos de artistas ni cifras sin fuente; tomar decisiones definitivas de producto; cambiar la arquitectura sin proponerlo primero.
- Regla de oro: si una petición contradice la arquitectura o las bases documentadas, señálalo y explica el porqué.
3. Cómo correr y verificar (comandos clave)
# instalar dependencias Python (primera vez)
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
# instalar Node (primera vez, sin sudo)
conda install -c conda-forge nodejs -y
cd web && npm install
# (re)construir la base de datos desde los CSV semilla (insert-if-missing;
# --reescribir pisa filas existentes)
.venv/bin/python scripts/seed_db.py
# regenerar los CSV desde la BD (respaldo/sync con architecting-a-band)
.venv/bin/python scripts/exportar_csv.py
# correr todo (API :8000 + web :3000)
./scripts/dev.sh
# carruseles de RRSS (salida en carrousel/<carpeta>/, stock compartido en carrousel/fuentes/)
python3 scripts/generar_carruseles.py --carrusel {presentacion|artistas|todos} --formato {cuadrado|reel|ambos}
# tests del backend (red de seguridad; usa BD temporal aislada)
.venv/bin/pytest
# verificación de la web (build de producción + lint)
cd web && npm run lint && npm run build
# verificación de la API (debe devolver {"estado":"ok"})
curl -s http://127.0.0.1:8000/api/health
Antes de dar una tarea por terminada, ejecutar siempre:
0. .venv/bin/pytest (la suite debe quedar en verde; vivo en tests/).
curl -s http://127.0.0.1:8000/api/healthy un muestreo de/api/artists,/api/feed,/api/artists/{slug}.npm run lintynpm run builddentro deweb/.
Versión de Python: CI usa 3.12, no la del venv (lección aprendida 2026-09-16).
El workflow .github/workflows/tests.yml (pytest en Python 3.12, en cada push/PR
a main) es la red de seguridad real: un NameError por una anotación sin
importar (def f() -> date | None) pasaba en local porque el venv en 3.14
difiere la evaluación de anotaciones (PEP 649), pero rompía todos los syncs en
CI. Antes de dar por bueno un cambio de código, verificar con Python 3.12 (o
confiar en el workflow) y no solo con .venv/bin/pytest.
Cuidado con el dev server de Next (lección aprendida 2026-08): no correr
npm run build (producción) mientras el npm run dev está en marcha: el build
pisa el .next/ del dev y este queda sirviendo bundles rotos (la página carga
pero no responde ningún control: filtros, chips, búsqueda). Además Next 16 exige
declarar el origen en web/next.config.ts → allowedDevOrigins: ["127.0.0.1", "localhost"]; sin eso el dev bloquea el JS del cliente (cross-origin por
seguridad) y React no hidrata. Si algo no hidrata, revisar el log del dev
(~/.next/dev/logs/next-development.log o el stdout) buscando "Blocked
cross-origin".
4. Arquitectura
Next.js (web/) → FastAPI (backend/main.py) → lib/servicios.py (read-models, ranking)
→ lib/plataformas.py (previews, OCP)
→ lib/repository.py (acceso a datos)
→ db/ (SQLAlchemy) → SQLite/PostgreSQL
→ scraper/ (monitoreo de actividad)
- Capa de datos desacoplada: la web (Next.js) nunca toca SQL; consume la API REST (
backend/main.py). El backend está dividido en capas (SRP): los routers debackend/main.pysolo rutean (sesiones/repos víaDependsdebackend/dependencies.py);lib/repository.pyconcentra toda la SQL en repositorios (ArtistRepository,EventRepository,FeedRepository,LinkRepository,ChecksRepository);lib/servicios.pyarma los read-models (artistas_df,feed_df,metricas_artista,ranking_por_ligas,stats_escena) ylib/helpers.pyqueda como utilidades puras sin SQL (URLs, géneros,indice_alcance). Los scripts reusan los repositorios en vez de SQL suelto. Todo es framework-agnóstico, sin Streamlit. - BD por defecto SQLite (
instance/local_scene.db); para crecimiento,DATABASE_URLde PostgreSQL en.env(ver.env.example). - Modelos:
artists,artist_links,events,activity_checks(snapshots del scraper),feed_items(contenido reciente del feed),metric_snapshots(historial mensual de métricas).artistsincluyebioy foto de perfil como URL (imagen_perfil/imagen_origen/imagen_actualizada). La categoría de proyecto (campo internosegmento, etiqueta de UI "Categoría") tiene taxonomía Banda / Solista / DJ / Colectivo / Covers / Tributo (MC y Productor se registran como Solista por ahora);es_propio(proyecto del propio universo) es un flag booleano con columna semilla propia;nivel(campo interno, etiqueta de UI "Liga") es un flag de catálogo ortogonal asegmento: vacío = escena local base, o uno de los niveles Liga Mayor / Emergente / Leyenda de la Frontera (los catalogados se muestran en un ranking de Ligas aparte del ranking general).ciudades ciudad base única (si un artista opera en otra plaza, va ennotas). La etiqueta de lado de frontera (TM/TX) se decide en display con el mapaweb/lib/ciudades.ts::PAIS_CIUDAD(región cerrada: Tamaulipas + Valle del Río Grande; si aparece un 3.er origen se migra a una columnapaisen BD). Ranking de alcance: índice universal con techos fijos +clasificar_por_indice(metricas)con umbrales documentados (ver sección 6 Ligas);lib/servicios.ranking_por_ligaslas mide dentro de las 4 ligas y a todas a la vez. - Scraper:
scraper/core.pyimplementa la regla de actividad;scraper/adapters/tiene un adaptador por fuente (http.pyyyoutube.pyfuncionales sin API key,tiktok.pyvía oEmbed público sin API key,instagram.py/facebook.pyoEmbed de posts públicos,spotify.pyfuncional si hay credenciales en.env) yimagenes.py(foto de perfil como URL; Spotify vía Web API si hay credenciales en.envcon embed/oEmbed de respaldo, el resto porog:image; IG/FB/TikTok suelen bloquear). Los adaptadores lanzan excepciones de la familiascraper/errors.py(ScraperError) para que callers no acoplen a excepciones concretas; los oEmbeds IG/FB/TikTok comparten caché y lógica enscraper/adapters/oembed.py. La conversión de enlaces a previews y la detección de plataforma están centralizadas enlib/plataformas.py(registroPREVIEWS, patrón Open/Closed: añadir una fuente = añadir su adaptador y su entrada en el registro, sin tocarbackend/main.py). Las jerarquías de plataformas (foto de perfil y enlace puente) están centralizadas enscraper/jerarquias.pyy documentadas endocs/scraping.md— cambiar una jerarquía se hace solo ahí. La sincronización automática de posts FB/IG (artistas que administran su página) vive enbackend/feed_meta.py(OAuth + Graph API, requiereMETA_APP_ID/META_APP_SECRETen.env) yscripts/sync_feed_igfb.py; los tokens de página nunca se exponen en la API. - Bios y géneros con fuente: cadena de fuentes en
scraper/adapters/(bandcamp.py→ bio+tags,soundcloud.py→ bio vía__sc_hydration,youtube.py→youtube_about); Spotify se omite porque no expone bio pública.scripts/proponer_bios.pyrecorre la cadena y escribe directo cuando la fuente está clara (lib/helpers.es_bio_claray sin homónimos: ni advertidos ennotasni enCONFLICTOS_CONOCIDOS), con notaBio de <Plataforma> (YYYY-MM-DD)ennotas; lo ambiguo va al reportedata/bios_pendientes.mdpara curaduría. El CSV se regenera al final (BD = fuente de verdad). Fase 2 pendiente:about/description(FB) ybiography(IG) vía Meta para artistas conectados. - Spotify (MCP + snapshot, clonado de architecting-a-band):
scripts/spotify_mcp_server.pyexpone tools MCP configuradas enopencode.json(en vivo: lo que suena, recientes, top, búsquedas, stats de artistas; autorización única--auth, token enscripts/.spotify_cache.json, no versionado) yscripts/escena_local_snapshot.pytoma snapshots de la escena adata/escena_local_stats.csv(serie temporal, una fila por artista/fecha). Playlist semanal:scripts/generar_playlist_semanal.pyactualiza la playlist pública "Frontera Grande: Descubrimiento Semanal" (selección con rotación y memoria: el guion recoge varias canciones candidatas por artista —PLAYLIST_CANDIDATOS_POR_ARTISTA(5): cadena de fuentes top track → primer tema por lanzamiento → búsqueda por ID—, elige una al azar del catálogo y lee la selección anterior dedata/playlist_seleccion_semanal.json(que el workflow vuelve a commitear al repo cada semana —leer la playlist vía API da 403 en modo desarrollo—) como "memoria" para no repetir artistas ni canciones cuando alcanza; 1ª canción por artista y relleno hastaPLAYLIST_TAMANIO; ID estable endata/playlist_semanal.json), con workflow.github/workflows/sync-playlist.ymlcada lunes. La tarjeta semanal (arte de "cartel de festival", 1080×1440 (3:4, estándar de posts desde 2026-09), determinista, Archivo Black + Inter; desde 2026-09 rota un banco de 3 fondos por semana ISO —radar/ecualizador/doppler (VARIANTES_BANCO), 4 muestras reservadas para playlists futuras—, estilo y decisiones endocs/playlist_semanal.md§7) la generascripts/publicar_playlist_semanal.py(leedata/playlist_seleccion_semanal.json;--dry-runsolo crea el arte). RequierenSPOTIFY_CLIENT_ID/SPOTIFY_CLIENT_SECRET/SPOTIFY_REDIRECT_URIen.env; la playlist necesita además un refresh token de usuario conplaylist-modify-public(--authuna vez) como secretoSPOTIFY_PLAYLIST_REFRESH_TOKEN. Límite 2026: apps en modo desarrollo ya no reciben followers/popularity/géneros/top-tracks (top-tracks = 403) y bloquean crear playlists vía API (403); el script solo puede rellenar una playlist existente (endpoints/items, migración de marzo 2026 —/tracksquedó deprecado y devuelve 403). La playlist se crea manualmente en Spotify y su ID va como secretoSPOTIFY_PLAYLIST_ID(env); el snapshot registra solo presencia con 0/0 hasta solicitar Extended Quota en el dashboard. Dependencias:mcp>=1.9,<2yspotipy(la v2 de mcp cambia la API de FastMCP). - La BD es la fuente de verdad operativa.
data/escena_local.csvydata/eventos.csvson bootstrap + export: se leen solo para crear una BD vacía y se regeneran conscripts/exportar_csv.py(respaldo y sync conarchitecting-a-band). El seed hace insert-if-missing porslug(no pisa filas existentes: la BD tiene estado vivo como imágenes, verificación, actividad y métricas);scripts/seed_db.py --reescribirfuerza un upsert solo para reconstruir.scripts/sync_local.shreconstruye la BD local descartable desde la de producción (Neon).es_propiose lee de la columna CSV homónima (devuelveTRUEsolo para el proyecto propio). - Perfiles duplicados de plataforma (2026-09-05): un artista puede tener varios perfiles oficiales de la misma plataforma (cuentas duplicadas por contratos, disputas o pérdida de acceso que siguen vigentes con reproducciones). Sus métricas se suman en el artista (único número), no se promedian ni se elige solo el primero: Spotify = oyentes mensuales de todos los perfiles (
scripts/actualizar_oyentes_spotify.pyylib/servicios.onboarding_artista); YouTube = suscriptores y vistas de todos los canales (scripts/sync_youtube_stats.py), y el feed ingiere los videos de todos (scripts/actualizar_feed_youtube.py). El índice, el ranking y la tarjeta del perfil usan ese número combinado (la web ya muestra una tarjeta por plataforma). Para no perder el 2.º enlace en el respaldo, el export une las URLs del mismoArtistLink.plataformaen la celda del CSV con el separador" | "(db/export.SEPARADOR_URLS) y el seed las vuelve a separar; una celda puede contener varias URLs separadas por|. Los perfiles secundarios se registran igual que el principal enartist_links(cones_busqueda=False). - Previews y feed como señal de actividad: el feed y los perfiles muestran previews estilo YouTube (YouTube y TikTok con miniatura y play; Instagram, Facebook, Spotify, SoundCloud y Mixcloud embebidos; ver
docs/scraping.md). El feed es la bitácora de actividad del proyecto: un elemento reciente (≤ 6 meses) alimentaestado_activovíascripts/recalcular_actividad.py(actualiza la BD; el CSV semilla se regenera conscripts/exportar_csv.py). Regla: activo ≤ 6 meses · en_duda 6–18 meses o sin señal · inactivo > 18 meses o pausa confirmada (scraper/core.py). La ingesta es solo automática y la hace el propio artista: al conectar su página FB/IG vía Meta Graph API (scripts/sync_feed_igfb.py+ cronscripts/sync_igfb.sh; guía:docs/meta_setup.md), vía el feed de YouTube del onboarding (lib/servicios.onboarding_artista; el sync periódicoscripts/actualizar_feed_youtube.pycorrió a través de.github/workflows/sync-youtube.ymlcada 6 h y toma los últimos 15 videos del RSS por canal —MAX_VIDEOS), o vía los lanzamientos (scripts/sync_lanzamientos.py+ workflowsync-lanzamientos.yml: Spotify por API oficial, Bandcamp/SoundCloud/Beatport/Mixcloud por adaptadores; solo material propio y sin ventana temporal —entra todo el historial, topeSYNC_LANZAMIENTOS_LIMITpor plataforma—, anti-duplicados por URL enFeedRepository.crear_si_nuevo; la des-duplicación normaliza la URL conFeedRepository._url_canonica—los enlaces de YouTube se guardan siempre en forma canónicawatch?v=<id>(YouTube alterna la misma pieza entrewatch?v=y/shorts/<id>según la época, lo que duplicaba los shorts al cambiar de formato;scripts/limpiar_duplicados_feed.pydeja una sola fila por video_id). No existe ingesta manual (el botón "+ Añadir post" yPOST /api/artists/{slug}/feedse eliminaron: no reintroducirlos). - Onboarding del alta con métricas síncronas (2026-09-06): al registrar un proyecto (
POST /api/artists) elonboarding_artistacaptura al momento —sin esperar al GitHub Action— lo que cada plataforma permite sin OAuth del artista: foto de perfil (URL desde las redes), últimos 5 videos de YouTube (RSS público) → feed, oyentes mensuales de Spotify (perfil público), suscriptores/vistas de YouTube (channel_statistics, Data API v3, requiereYOUTUBE_API_KEY; suma todos los canales), seguidores de SoundCloud (scraper/adapters/soundcloud.py::seguidores, api-v2) y seguidores de Mixcloud (scraper/adapters/mixcloud.py::seguidores, API REST pública). IG/FB/TikTok no tienen contador público: sus seguidores llegan solo por OAuth del artista (sync_feed_igfb.py/sync_feed_tiktok.py). Todas las capturas son best-effort (try/exceptpor fuente, nunca rompen el alta). El sync diarioscripts/sync_soundcloud_stats.pymantiene después reproducciones y seguidores de SoundCloud en lote. Se añadió la columnaartists.followers_soundcloud(migración endb/database.py, export/seed incluidos). - Historial mensual de métricas (2026-09-12): tabla
metric_snapshotspara registrar los números por mes (el "por mes" del proyecto). Una fila por captura × plataforma × métrica (valor+fuente); taxonomía idéntica ametricas_artista(ig/fb → seguidores · yt/tt → seguidores/vistas · spotify → seguidores/oyentes_mensuales · bandcamp → reproducciones · soundcloud → seguidores/reproducciones · beatport/mixcloud → seguidores). Captura en cada sync (lib/servicios.registrar_snapshots, dedupe consecutivo: omite si el valor no cambió; se llama sin commit desde los scriptssync_youtube_stats,sync_soundcloud_stats,sync_feed_tiktok,sync_feed_igfb,actualizar_oyentes_spotifyy desde elonboarding_artista; el hook de YT construye las medidas condicionalmente para no inventar ceros). Fila-ancla mensual forzada:scripts/capturar_metricas_mensuales.py+ workflow.github/workflows/metricas-mensual.yml(día 1) escriben conforzar=Truepara que el mes quede registrado aunque no haya sync.metricas_mensualesagrega el último valor por mes con relleno del valor previo (carry-forward) desde la primera captura hasta el mes en curso, marcandoprimera_captura(útil para IG/FB/TikTok, cuyo registro nace al conectar la cuenta).scripts/backfill_metricas.pymigró el historial despotify_listener_snapshots. La API exponeGET /api/artists/{slug}/metricas→{slug, historial, hitos}(cacheado 5 min);hitos_artistaarma las marcas desde el feed (lanzamiento/videoclip) y los eventos (toquín) en el perfil público, en la tarjeta discreta "Números · evolución mes a mes" (web/components/EvolucionMetricas.tsx, patrón<details>de conectar redes; gráfica relativa 100% = inicio de ventana, filtros 3M/6M/12M/Todo, selector de métrica, ejes que no parten de 0). Respaldo mensual exportado endb/export.py→data/metricas_mensuales.csv(el seed no siembrametric_snapshots). Fase 2 (roadmap #26): uplift por hito, decaimiento y anomalías. Cuidado: en los syncs, pasarsessionsolo para artistas persistidos (el helper_actualizar_seguidores_metaregistra snapshots solo siartista.idexiste). - Ranking de alcance y Ligas (2026-08): arquitectura de dos capas con índice universal oculto:
- Índice universal (oculto, solo admin):
calcular_indice_universal()enlib/helpers.py→ índice 0-100 normalizado contra techos de referencia fijos (cada señal contra el nivel mundial: IG/FB/TT/YT seguidores 50M · YT vistas 10B · Spotify oyentes 20M (bajado 2026-09-14 de 50M: el oyente mensual es consumo intencional que no se acumula con autoplay/scroll, el techo viejo lo subvaloraba frente a señales pasivas; simulado: 3 ascensos de liga, reposiciones suaves) · Spotify seguidores 20M · SoundCloud 1B · Bandcamp 1M · Beatport 100K · Mixcloud 50K; el techo de SoundCloud se subió el 2026-09-05 de 10M a 100M y luego a 1B porque las reproducciones de SoundCloud seguían resultando demasiado baratas frente a los techos de YT/Spotify (es más fácil acumularlas que oyentes o vistas) y podían poner a un artista con métricas medias sobre artistas con oyentes/vistas mayores; verTECHOS_REFERENCIA). Combina 70% de la señal dominante (mejor ratio) + 30% de cobertura (media de ratios, contar 0 las ausentes). Regla anti-trampa: si hay consumo registrado, la parcial social IG/FB/TT se limita aconsumo real × 3(FACTOR_COHERENCIA_AUDIENCIA) para que la audiencia comprada o el perfil de influencer no inflen el índice. Anti-shorts (2026-08): como elviewCountdel canal incluye Shorts, las vistas de YouTube cuentan al 60% (FACTOR_CAPACIDAD_VISTAS_YT = 0.6) en la clasificación y en los rankings de alcance (consumo/índice), y el alcance de YT prioriza suscriptores sobre vistas (METRICA_ALCANCE_POR_PLATAFORMA); la cifra cruda que se muestra en el perfil no cambia. Se usa solo como señal de clasificación (umbrales fijos → Ligas). No se expone en la API pública (solo conX-Admin-Token). - Clasificación on-the-fly:
clasificar_por_indice()enlib/helpers.pycon umbrales fijos y documentados (UMBRAL_LIGAS_MAYORES = 60·UMBRAL_EMERGENTE = 50): índice ≥ 60 = "Ligas Mayores"; ≥ 50 = "Emergente"; < 50 = "Escena" (base). Los umbrales no dependen de la composición de la escena (a diferencia de los percentiles viejos). "Leyenda de la Frontera" = flag editorial manuales_leyenda(bool en BD, toggle en panel admin). - Rankings visibles (4 ligas):
lib/servicios.ranking_por_ligas(df)trata la Escena como una liga más: 4 ligas — Escena (base,nivel_calculado=""), Emergente, Ligas Mayores, Leyenda de la Frontera. Cada liga tiene su propia clasificación y, aparte, un ranking universal que las mide a todas a la vez. La posición dentro de cada liga (rank_liga/total_liga) y la global (rank_universal/total_universal) se derivan del índice universal (no se re-normaliza por liga), así todas las ligas son comparables. La API expone enranking:indice,audiencia,consumo,liga,rank_liga,total_liga,rank_universal,total_universal,indice_universaly losrank/totalde compatibilidad (= la liga).ranking_global/ranking_ligasse mantienen como wrappers de compatibilidad. - Criterio editorial (2026-09-12): las Ligas describen reconocimiento, no sostenibilidad económica. Un proyecto Emergente se distingue de la base con audiencia y actividad reales, pero no se asume que viva de su música (no se mide ni se promete); la sostenibilidad suele llegar recién en Ligas Mayores. El matiz completo vive en
docs/vision.md§ "Criterio editorial de las Ligas". - Persistencia:
es_leyendaen BD (artists.es_leyenda);nivelhistórico se mantiene por compatibilidad pero la lógica usanivel_calculado(on-the-fly). - Panel admin (
/admin): incluye togglees_leyendaen edición de artista;POST /api/artists/{slug}aceptaes_leyenda.
- Índice universal (oculto, solo admin):
- Sistema de Insignias: en el perfil del artista (
/artistas/[slug]) se muestra la sección "Insignias" con badges cuadrados (80×80px):- Todos los proyectos compiten por insignias dentro de su liga: categoría, género, ciudad y "Nº X de la Frontera Grande" (las menciones vienen de
menciones_rankingdel subconjunto de su liga, no de toda la escena mezclada). - Los proyectos con Liga (Emergente / Ligas Mayores / Leyenda de la Frontera) muestran además su badge de liga (icono + nombre, color según nivel) junto a sus insignias ganadas.
- Iconos/colores: Escena = círculo púrpura (#9d4edd), Ligas Mayores = estrella dorada (#f5b301), Emergente = rayo verde (#2fb8a6), Leyenda = corona plata gris-azulada (#aab4c8).
- La sección es visible en el perfil público y documentada en la página "Acerca de".
- Todos los proyectos compiten por insignias dentro de su liga: categoría, género, ciudad y "Nº X de la Frontera Grande" (las menciones vienen de
- Ranking unificado y feed (2026-08-30): una sola gráfica en
/stats(web/components/stats/RankingFiltrable.tsx) con chips Escena (default) · Ligas Mayores · Emergente · Leyenda; re-ranquea 1..N dentro del grupo mostrado (la posición oficial vive en la API y en el perfil) y conserva el selector Top y el desglose audiencia/consumo. En todos los chips se ordena por el índice universal (ranking.indice_universal, expuesto en cada tarjeta; comparable entre las 4 ligas), de modo que la posición mostrada coincide conrank_ligadel perfil. El chip Todos marcа a los catalogados con su insignia. Término público "Escena" para la base y es una liga más; "Rookies" quedó solo como clave interna (stats.rookies/ranking_global) por compatibilidad de API. Feed: el avatar del artista es enlace a su perfil y/api/feed(y el feed del perfil) exponenivelpor ítem para mostrar el pill de Liga junto al nombre solo cuando el artista está catalogado. - Coherencia de caché y clasificación (2026-08-31):
nivel/catalogadoy la posición de ranking se calculan del mismodfrecién leído en cada request (el ranking de Escena/Ligas ya no se cachea aparte en la API); así un artista recién ascendido siempre aparece con suranking.ranken el perfil y en su chip del chart (regresión del bug "Emergente sin lugar"). Las respuestas completas sí se cachean 5 min (MemoryCache,lib/cache.py:invalidate_public_cachelimpiaartists:/feed:/stats:/ranking:). Como los syncs escriben en la BD fuera del proceso de la API, los workflows de sincronización llamanPOST /api/admin/cache-invalidate(X-Admin-Token) víascripts/invalidar_cache_api.shpara renovar las respuestas de inmediato; el paso se omite en silencio si no está configurado el secretADMIN_PASSWORDen GitHub Actions. La web mantiene su propia caché de 5 min (fetchconrevalidate: 300e ISR), por diseño.
5. Convenciones del proyecto (crítico)
- Idioma: todo en español (código, mensajes de UI, documentación, commits).
[PENDIENTE]= información que falta por definir; no inventar contenido donde aparece.[PROPUESTA]= propuesta del asistente para que el artista confirme o ajuste.- No inventar datos: cifras de seguidores, reproducciones/vistas, fechas, lanzamientos y logros deben venir de la BD (sembrada desde los CSV semilla) o de investigación con fuente.
- Una sola verdad: la BD es la fuente de verdad operativa; si un dato cambia, actualizar las referencias cruzadas (BD, CSV exportado, documentación, código).
- No escribir código con comentarios innecesarios: seguir el estilo existente (docstrings de módulos/funciones en español, sin comentarios de relleno).
- Bitácora (opcional, patrón del proyecto padre): si se decide llevar historial de decisiones, seguir el formato de
architecting-a-band/BITACORA.md(entradas enbitacora/YYYY-MM.md, no se reescriben).
6. Estado actual
- Fase: web pública EN LÍNEA (agosto 2026): API en Render
https://fronteragrande-api.onrender.com+ web en Vercelhttps://fronteragrande.vercel.app+ PostgreSQL en Neon (ver README.md → Estado). - Decidido: Next.js + FastAPI + SQLAlchemy + SQLite (→ PostgreSQL cuando escale). Sin mapa por ahora (siguiente etapa: PostGIS).
- Web Push: la PWA registra suscripciones VAPID en
push_subscriptions; las altas de artistas y los posts nuevos de Meta disparan avisos, y el admin puede enviar broadcasts. En iPhone requiere instalar primero la PWA. - Hecho: API REST · web mobile-first con directorio, perfiles, feed con previews de YouTube/IG/FB, eventos, panel de stats interactivo (gráficas SVG caseras en
web/components/stats/: actividad temporal, ranking desglosable por red, ecosistema de redes, ciudades apiladas, dona por categoría;GET /api/statsampliado confeed_serie,altas_por_mes,seguidores/reproducciones,cobertura,posts_90dias,por_ciudad,eventos_proximos), identidad Frontera Grande (renombrado;docs/vision.mdes la fuente única de alcance y límites) y página Acerca de (historia, "cómo explorar la plataforma", regla de actividad y fórmula del ranking con KaTeX), fotos de perfil de los artistas desde sus redes (URLs), SEO básico (sitemap/robots/metadatos), PWA instalable (app/manifest.ts+public/sw.jscon cache de la shell y de estáticos de Next, sin tocar la API externa; iconos enweb/public/icons/), registro voluntario de artistas + verificación por OAuth: el botón "Suma tu proyecto" crea el perfil (POST /api/artists) con validaciones (mínimo 1 red, máximo 6, rechazo de URLs duplicadas —tanto repetidas dentro del mismo alta como ya vinculadas a otro proyecto, 409/400—, rate-limit de 5 altas/IP/24 h y cooldown 10 min, leyenda de que los proyectos sin verificar pueden ser eliminados), y al conectar su página FB/IG/TikTok el perfil queda verificado (badge público "Verificado",estado_registro→confirmado (artista, fecha)) y sus posts se sincronizan automáticamente; las notificaciones push de nuevos proyectos solo se envían tras la verificación; ingesta manual eliminada (POST /api/artists/{slug}/feed,FormAgregarFeedyscripts/registrar_feed.pyya no existen: no reintroducirlos) — anti-inflación de números (2026-09-05): las mismas defensas se aplican en TODAS las ediciones (editar_artistaadmin yeditar_perfil_propio): ningún enlace puede repetirse dentro del envío ni estar ya vinculado a otro proyecto (el propietario solo conserva su página FB verificada; los 409 se devuelven también enPUT /api/artists/{slug}yPUT /api/feed/igfb/{slug}/perfil);normalizar_url_para_duplicadoscanonicaliza por identidad de plataforma (Spotifyspotify:artist:<id>, YouTube/channel/<id>→yt:channel:<id>) para que dos escrituras del mismo perfil se detecten aunque la URL cambie (www.,?si=,/intl-es/, barra final); el residual asumido: un perfil oficial de un tercero que aún no esté en la BD no se puede bloquear sin verificación, y un canal de YouTube escrito como@handlevs/channel/UC...no se unifica (falta resolver el ID, que requiere red), backend por capas (routers →lib/servicios.py/lib/plataformas.py→lib/repository.py), suite de tests (.venv/bin/pytest, red de seguridad entests/), directorio pulido: taxonomía de categoría (Banda/Solista/DJ/Colectivo/Covers/Tributo, MC y Productor como Solista), ciudades normalizadas a base única con dropdown, filtros con etiqueta visible y opción "Todas", géneros en chips de selección múltiple y leyenda de actividad bajo los filtros. Panel de administración (/admin, protegido porADMIN_PASSWORDvíaX-Admin-Token): login en sesión, listado conGET /api/admin/artists, edición (PUT /api/artists/{slug}: nombre, ciudad, categoría, géneros, bio, notas, logros, estado de actividad y redes) y eliminación (DELETE /api/artists/{slug}) con confirmación; la lógica de edición vive enlib/servicios.editar_artista(solo aplica campos presentes, conserva enlaces de búsqueda). Valores huérfanos sin fuente marcados[PENDIENTE]ennotas(ej. seguidores IG de isquemia/vaale). Bios y géneros con fuente (Fase 1): cadena Bandcamp→SoundCloud→YouTube enscraper/adapters/(sin API key) yscripts/proponer_bios.pyque escribe directo lo claro con nota de fuente ennotasy deja lo ambiguo endata/bios_pendientes.md(primera corrida 2026-08-16: 3 bios escritas; Distraught y Don Bravo quedaron pendientes por homónimos). Bios y géneros con fuente (Fase 2): para artistas conectados, bio desde Meta (about/descriptionde FB ybiographyde IG víabackend/feed_meta.py) integrada enscripts/sync_feed_igfb.py. Sincronización de TikTok (Business API):backend/feed_tiktok.py(OAuth del creador con PKCE: login/callback/desconectar +user.info.basic+video.list; rota el refresh token y nunca expone tokens),scripts/sync_feed_tiktok.py, columnastt_user_id/tt_refresh_token(migración ligera endb/database.py), botón "Conectar TikTok" en el perfil (web/components/ConexionTikTok.tsx) yverificado=(fb_page_token or tt_refresh_token) and estado_registro. Cierre de datos automáticos (2026-08-19): seguidores de Facebook/Instagram en el sync de Meta (pagina_seguidores/ig_seguidores, campofollowers_count), lanzamientos propios en el feed (scripts/sync_lanzamientos.py+ workflowsync-lanzamientos.ymlcada 6 h: Spotify por API oficial/artists/{id}/albums, Bandcamp por cuadrícula HTML, SoundCloud por api-v2 conclient_iddel bundle, Beatport por__NEXT_DATA__, Mixcloud por API REST pública; todos con anti-duplicados por URL y sin ventana temporal —entra todo el historial de lanzamientos, topeSYNC_LANZAMIENTOS_LIMITpor plataforma—; el perfil organiza ese contenido en pestañas Todo/Posts/Video/Música/Eventos víaweb/components/ContenidoPerfil.tsx), fotos de perfil por API primero (scraper/adapters/imagenes.py: Metapicture/profile_picture_url, YouTube Data API, avatar de TikTok en su sync) con fallback a scraping, métricas de YouTube visibles de nuevo en el directorio (revertido lo ocultado enbe8d644) y reproducciones de SoundCloud (2026-09-04):scraper/adapters/soundcloud.py::reproduccionessuma elplayback_countde todas las pistas públicas vía api-v2 (paginada connext_href; lanzaSoundCloudErrorsi la fuente no se lee, para no inventar un cero) yscripts/sync_soundcloud_stats.py+ workflowsync-soundcloud-stats.yml(diario) llenanreproducciones_soundcloud, que alimenta el ecosistema de redes, las stats y el índice universal. Eventos (fase 2026-08): CRUD de eventos en el panel de admin (pestaña "Eventos": alta/edición/baja víaPOST/PUT/DELETE /api/admin/events, lógica enEventRepository), ingesta automática desde Meta (permisopages_eventsen el scope OAuth +backend/feed_meta.py::pagina_eventos+scripts/sync_eventos_meta.py+ workflowsync-eventos-meta.ymlcada 6 h; los toquines que el artista publica en su página FB se registran eneventssin duplicar por fuente), página pública de eventos pulida (secciones "Próximos" y "Pasados", sin el aviso de "en construcción") y eventos como señal de actividad (lib/servicios.recalcular_actividadactualizaultimo_eventodesde la tablaevents—solo avanza, no regresa— y aplica la regla; lo llaman los sync y los endpoints del admin tras cada cambio). Ligas (2026-08): camponivel(catalogación Liga Mayor / Emergente / Leyenda de la Frontera, ortogonal a Categoría) con ranking de Ligas aparte del general (lib/servicios.ranking_ligas+lib/servicios.ranking_globalseparado; la API exponenivel,catalogadoy el ranking de su grupo por tarjeta/detalle); clasificadorlib/helpers.calcular_indice_universal+clasificar_por_indice(techos fijos de referencia contra el nivel mundial, umbrales fijos: índice ≥ 60 Ligas Mayores, ≥ 50 Emergente; regla anti-trampa: social/consumo ≤ 3); Leyenda: manual connotasde fuente); catálogo inicial sembrado de 22 artistas decurado3_enriquecido.csvvíascripts/importar_curado3.py+ seed: 5 Leyenda (Ramón Ayala, Rigo Tovar, Beto Quintanilla, Carlos y Jose, Fito Olivares), 5 Liga Mayor (Grupo Frontera, Twin Tribes, Duelo, Sandro Malandro, Yahir Saldivar) y 12 sin nivel hasta juntar métricas con fuente; web: filtro "Liga" en el directorio (web/components/Directorio.tsx), insignia (web/components/InsigniaNivel.tsx) en tarjeta y perfil, gráfica "Ranking de Ligas" en/stats(web/components/stats/RankingLigas.tsx), sección "Cómo se clasifican las Ligas" en Acerca de, y edición denivelen el panel de admin (campo "Liga"). - Spotify se mide solo por oyentes mensuales (2026-09-04): el campo
reproducciones_spotify(semilla heredada del CSV, sin fuente real) dejó de ser señal — ya no alimenta el índice universal, los rankings, el análisis, las stats ni la tarjeta del perfil. No hay forma automática de obtener reproducciones/streams totales de Spotify (el perfil público no las expone; la Web API en modo dev devuelve 0/None; Spotify for Artists no tiene API). El perfil muestra solooyentes_mensuales(fuente:scraper/adapters/spotify_public.py);reproducciones_spotifyse conserva en BD/CSV como referencia histórica. - Pendiente: conectar al resto de artistas que administran su página Meta (la app está configurada en producción; 2026-09-13: Meta aprobó la verificación de negocio — queda la App Review (acceso avanzado) de
pages_read_engagement,instagram_basicypages_eventsy pasar la app a modo Live para que cualquier artista no-admin pueda conectarse; hasta entonces solo los roles de la app hacen login (verdocs/meta_setup.md§11); cuandopages_eventsquede aprobado, activarMETA_CON_EVENTOS=trueen Render para pedirlo en el login por defecto; Apex Ultra quedó verificado pero su token de Meta fue invalidado por Facebook —cambio de contraseña/sesión—, hay que reconectar desde su perfil para que el sync de posts y seguidores vuelva a funcionar; la reconexión también concede el permisopages_eventsnuevo para la ingesta de eventos), YouTube resuelto (2026-08-31):YOUTUBE_API_KEYya estaba en GitHub Actions ysync-youtube.ymlcorría, pero la ingesta de videos usabasearch.list(100 unidades de cuota por canal → agotaba la cuota diaria de 10,000 con 72 canales y dejaba el feed sin videos con HTTP 429);latest_videosahora usa el feed RSS público (sin API key ni cuota), así los videos llegan aunque la cuota esté agotada (estadísticas/suscriptores siguen con la Data API), crear el monitor UptimeRobot, disparador del scraper en el panel de admin (el CRUD ya está hecho), auth y permisos adicionales para el registro de artistas (el formulario sigue público pero ahora tiene rate-limit, validación de URLs duplicadas y advertencia de eliminación; CAPTCHA u otro reto si el abuso crece) y la alta en Google Search Console para acelerar la indexación. Caché de la API: definirADMIN_PASSWORD(y opcionalmenteAPI_PUBLIC_URL) como secrets de GitHub Actions para que el paso de invalidación (scripts/invalidar_cache_api.sh) renueve la caché tras cada sync; sin ellos el paso se omite y la web refleja los cambios en ≤ 5 min por su propia revalidación. El dominio ya quedó resuelto (✔ 2026-08:fronteragrande.mxregistrado en Cloudflare, conectado a Vercel y con vencimiento el 2027-08-20; metadatos/sitemap/robots ya usanhttps://fronteragrande.mx). Bios (cierre): la Fase 2 ya escribe bio desde Meta (about/descriptionde FB ybiographyde IG víabackend/feed_meta.py, integrado enscripts/sync_feed_igfb.pycon guardaes_bio_claray nota de fuente); falta curaduría de los pendientes dedata/bios_pendientes.mddesde el admin. Sincronización de TikTok (backend, web y cron hechos):backend/feed_tiktok.py(OAuth del creador con PKCE, scopesuser.info.basic+user.info.stats+video.list),scripts/sync_feed_tiktok.py,.github/workflows/sync-tiktok.yml(cada 6 h, paralelo a Meta) y botón "Conectar TikTok" en el perfil (paralelo a Meta). App en TikTok for Developers (modo sandbox): Apex Ultra quedó conectado y verificado (2026-08-16) y su sync ya trae seguidores y videos al feed (en sandbox funcionanuser.info.statsyvideo.list). Requiere los secretsTIKTOK_CLIENT_KEY/TIKTOK_CLIENT_SECRETen GitHub Actions. Falta: aprobación de la app (video demo + explicación) para producción y conectar al resto del inventario (docs/tiktok.md). Beatport: el adaptador está listo pero no hay artistas con enlace de Beatport en el inventario. Playlist semanal (cierre): falta crear la playlist manualmente en Spotify (no se puede crear vía API en modo desarrollo, 403) y guardar su ID como secretoSPOTIFY_PLAYLIST_ID; el refresh token (--auth) ya está comoSPOTIFY_PLAYLIST_REFRESH_TOKEN, queda la primera corrida del workflowsync-playlist.yml(lunes). Automatización a redes (2026-08): el workflow ya publica la tarjeta a FB/IG automáticamente tras generar la playlist (PROMO_AUTO_PUBLISH=true+FG_PAGE_ID/FG_PAGE_TOKEN/PROMO_IG/API_PUBLIC_URL); solo queda verificar que esos secrets estén en GitHub Actions con la primera corrida. Verificado (2026-09-01): al correr el workflowsync-playlist.ymlel paso "Publicar anuncio en redes" se omite porque faltan los secrets en GitHub Actions (PROMO_AUTO_PUBLISH,PROMO_IG,FG_PAGE_ID,FG_PAGE_TOKEN,API_PUBLIC_URLno existen; solo estánDATABASE_URL,META_*,SPOTIFY_*,TIKTOK_*,YOUTUBE_API_KEY). El token permanente de la página FG se obtiene conscripts/obtener_token_pagina.py(flujo OAuth enbackend/feed_meta.py:GET /api/feed/igfb/fg-login→ callback constate=fg→ guarda el token endata/fg_page_token.txt, NO versionado, y configura el secretFG_PAGE_TOKEN); guía endocs/meta_setup.md§10. Fix 400 de Meta (2026-09-01): el400 Bad RequestdelPOST /{page}/photosprovenía de que la URL de la tarjeta devolvía 404: el workflow genera la tarjeta en el runner de CI, pero esa imagen no existe en la API (donde sí corre la bienvenida de artista), y/api/promos/{slug}.jpgno puede generarla porque el slug de playlist no es un artista. Se añadióPOST /api/admin/promos/upload(X-Admin-Token, valida JPEG y escribe eninstance/promos/) yscripts/publicar_playlist_semanal.pyahora sube la tarjeta a la API (helpersubir_tarjeta_a_api, usa el secretADMIN_PASSWORD) antes de construir la URL pública, para que Meta sí pueda descargarla. Falta: definir esos secrets de Actions (incluidoADMIN_PASSWORD) y correr el workflow de nuevo.[PENDIENTE]plantillas por playlist y banco de hooks en BD: para el futuro ecosistema de playlists temáticas (roadmap #24) se planea una plantilla de arte por tipo (género, época, novedades, ciudad, clásicos…) para que el feed no sea monótono, y migrar el banco de hooks del copy (HOOKS_FB/HOOKS_IGhardcodeados enpublicar_playlist_semanal.py) a una tabla BDhook_bank; ambos documentados endocs/playlist_semanal.md§6. Ligas (cierre): los 12 artistas decurado3_enriquecido.csvsin nivel quedaron así a propósito hasta juntar métricas con fuente —Lirik Dog, Javier Molina, Lobo Bizarro, Cano y Blunt, Cano de Cali, Alexis Mvgler, OutDry, Big Sempa, Kelo McKane (candidatos "Emergente" pendientes de verificar), Radio Supreme, Jorge Tobón y El Mexicano (datos insuficientes); alimentarlos con seguidores/oyentes reales y correrclasificar_nivel, no suponer. - Publicación nativa en Instagram (2026-08-25):
lib/promo_fg.pypublica directamente en IG @fronteragrande al verificar artista, con caption propio (mención clicable@handle, ficha, hashtags) y flujo contenedores/media→/media_publish. Controlado porPROMO_IG=true+PROMO_AUTO_PUBLISH=trueen Render. Requiere permisoinstagram_content_publish(App Review Meta) para producción; funciona en dev mode para admins. Endpoint.jpgpara Instagram enbackend/main.py. - Guard anti página equivocada (2026-09-14): las publicaciones en FB/IG solo salen en la página de Frontera Grande.
lib/promo_fg.pyresuelve/meconFG_PAGE_TOKENantes de postear (guard_error_pagina_fg): publica solo si el id coincide conFG_PAGE_IDo el nombre contiene "Frontera Grande"; si el token pertenece a otra página (p. ej. Apex Ultra) o está vencido, no publica y deja un error claro en el log (el error #283 del token muerto de.envya no genera posts en el lugar equivocado). Además, el OAuth FG (_pagina_por_idconpreferir_nombre) ya no cae a la primera página administrada si no encuentra la de FG → redirige?fg_token=no_admin; yscripts/obtener_token_pagina.pyvalida el token contra/mey aborta antes de fijar el secretFG_PAGE_TOKENsi la página resuelta no es Frontera Grande. Detalle endocs/meta_setup.md§10. - Despliegue (ACTIVO): repo remoto
github.com/baldeadr/fronteragrande(ramamain),render.yaml(blueprint API),vercel.json(soloframework: nextjs; root dirwebpuesto en el dashboard de Vercel),requirements-prod.txt(deps mínimas de la API, sin matplotlib/wordcloud/mcp/spotipy), CORS configurable por env (CORS_ORIGINSenbackend/main.py), driver PostgreSQLpsycopgcon normalización de URL endb/database.py, rutas del seed independientes del directorio (db/seed.py) y Meta OAuth configurado en Render. Re-ejecutar sobre Neon:scripts/actualizar_imagenes.pyyscripts/actualizar_feed_youtube.pyconDATABASE_URLde Neon. Guía:docs/despliegue.md. - El artista no escribe código: las tareas se deben proponer y, si son técnicas, implementarse directamente con verificación.
7. Cómo actualizar la documentación
- Al cambiar arquitectura o modelo de datos, actualizar README.md (índice y modelo), docs/roadmap.md (si afecta a una etapa del roadmap) y este archivo (si afecta a instrucciones de trabajo).
- Mantener consistencia entre documentos y código.
- Preferir editar archivos existentes; crear nuevos solo si aportan valor real.
- Si una instrucción es ambigua o requiere una decisión de producto, preguntar al artista antes de actuar.
