pablogrois Claude Fable 5 commited on
Commit
c79fec9
·
1 Parent(s): aaec6a5

Reportes de partido para las 28 ligas con ejes (ligas del modelo intactas)

Browse files

- Selector de partidos = ligas del modelo ∪ ligas con artefactos de ejes; equipos y
temporadas de ligas nuevas via /api/ejes/teams (endpoint aditivo, nada existente cambia
— verificado contra baseline: /api/leagues?model_only, seasons y teams idénticos).
- Gating por reporte: previo/post NN solo ligas del modelo (con aviso); ejes pre/firma
para las 28.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

src/racing_reports/api/routes_ejes.py CHANGED
@@ -16,6 +16,15 @@ def options() -> dict:
16
  return {"leagues": ejes_data.available(), "ejes": ejes_data.EJE_LABELS}
17
 
18
 
 
 
 
 
 
 
 
 
 
19
  @router.get("/profiles", dependencies=[Depends(require_auth)])
20
  def profiles(league: str, season: str) -> dict:
21
  try:
 
16
  return {"leagues": ejes_data.available(), "ejes": ejes_data.EJE_LABELS}
17
 
18
 
19
+ @router.get("/teams", dependencies=[Depends(require_auth)])
20
+ def teams(league: str, season: str) -> dict:
21
+ """Equipos con perfil de ejes (para ligas fuera del dataset del modelo)."""
22
+ try:
23
+ return {"teams": ejes_data.teams(league, season)}
24
+ except FileNotFoundError as exc:
25
+ raise HTTPException(status_code=404, detail=f"Sin artefactos de ejes: {exc}")
26
+
27
+
28
  @router.get("/profiles", dependencies=[Depends(require_auth)])
29
  def profiles(league: str, season: str) -> dict:
30
  try:
src/racing_reports/web/templates/home.html CHANGED
@@ -181,7 +181,10 @@
181
  <h3 class="font-semibold">Reporte previo (head-to-head)</h3>
182
  <p class="text-xs text-gray-400 mt-1 mb-3">Zonas, presión, win/loss, local/visitante. Predicción del modelo
183
  <span x-show="nnEnabled" class="text-racing-200">activada</span><span x-show="!nnEnabled">deshabilitada (falta RR_ENABLE_NN_PREDICTIONS=1)</span>.</p>
184
- <button @click="runFull('pre_match')" class="bg-racing-500 hover:bg-racing-600 text-racing-900 font-semibold px-4 py-2 rounded-lg text-sm">Generar previo</button>
 
 
 
185
  </article>
186
 
187
  <article class="bg-ink-700 border border-ink-600 rounded-2xl p-5" :class="nnEnabled ? '' : 'opacity-60'">
@@ -615,6 +618,7 @@
615
  ejLeagues:{}, ejLeagueList:[], ejLeague:'', ejSeason:'', ejSeasons:[], ejLabels:{},
616
  ejX:'elaboracion', ejY:'verticalidad', ejLado:'gen', ejMet:'z', ejData:null, ejError:'', ejLoaded:false,
617
  ejPostMsg:'', ejPostMsgError:false,
 
618
 
619
  async init() { await this.loadLeagues(); await this.loadSeasons(false); await this.loadTeams(); this._refreshHealth(); },
620
  async _getJSON(u){ const r=await fetch(u); if(!r.ok) throw new Error((await r.json().catch(()=>({detail:r.statusText}))).detail); return r.json(); },
@@ -752,20 +756,38 @@
752
  async loadLeagues(){
753
  try {
754
  const q = this.tab==='partidos' ? '?model_only=true' : '';
755
- this.leagues = await this._getJSON('/api/leagues'+q);
 
 
 
 
 
 
 
 
 
 
756
  if (this.leagues.length && !this.leagues.includes(this.league)) this.league = this.leagues[0];
757
  } catch(e){ this.leagues = this.league?[this.league]:[]; }
758
  },
759
  async loadSeasons(reset=true){
760
  try {
761
  this.seasons = await this._getJSON(`/api/seasons?league=${encodeURIComponent(this.league)}`);
762
- if (this.seasons.length && (reset || !this.seasons.includes(this.season))) this.season = this.seasons[0];
763
- } catch(e){ this.seasons = this.season?[this.season]:[]; }
 
 
 
764
  },
765
  async loadTeams(){
766
  try {
767
  this.teams = await this._getJSON(`/api/teams?league=${encodeURIComponent(this.league)}&season=${encodeURIComponent(this.season)}`);
768
- } catch(e){ this.teams = []; }
 
 
 
 
 
769
  // Esperar a que el <select> tenga sus <option> renderizados antes de fijar
770
  // el valor, si no el navegador muestra la primera opción y no Racing.
771
  await this.$nextTick();
@@ -799,6 +821,7 @@
799
  },
800
  async runPostMatch(){
801
  this.postMatchMsg=''; this.postMatchMsgError=false;
 
802
  if (!this.teamA||!this.teamB){ this._pmErr('Elegí local y visitante.'); return; }
803
  if (this.teamA===this.teamB){ this._pmErr('A y B deben ser distintos.'); return; }
804
  this.postMatchChecking=true;
@@ -820,6 +843,7 @@
820
 
821
  async ejRunPost(){
822
  this.ejPostMsg=''; this.ejPostMsgError=false;
 
823
  if (!this.teamA||!this.teamB){ this.ejPostMsg='Elegí los dos equipos.'; this.ejPostMsgError=true; return; }
824
  if (this.teamA===this.teamB){ this.ejPostMsg='A y B deben ser distintos.'; this.ejPostMsgError=true; return; }
825
  this.ejPostMsg='Usa el último cruce jugado entre ambos (según artefactos de ejes).';
 
181
  <h3 class="font-semibold">Reporte previo (head-to-head)</h3>
182
  <p class="text-xs text-gray-400 mt-1 mb-3">Zonas, presión, win/loss, local/visitante. Predicción del modelo
183
  <span x-show="nnEnabled" class="text-racing-200">activada</span><span x-show="!nnEnabled">deshabilitada (falta RR_ENABLE_NN_PREDICTIONS=1)</span>.</p>
184
+ <button @click="runFull('pre_match')" :disabled="!mlLeagues.includes(league)"
185
+ :class="mlLeagues.includes(league) ? 'hover:bg-racing-600' : 'opacity-40 cursor-not-allowed'"
186
+ class="bg-racing-500 text-racing-900 font-semibold px-4 py-2 rounded-lg text-sm">Generar previo</button>
187
+ <p class="text-xs text-yellow-200 mt-2" x-show="!mlLeagues.includes(league)">Solo para ligas del modelo NN — en esta liga usá los reportes de Ejes de ataque de abajo.</p>
188
  </article>
189
 
190
  <article class="bg-ink-700 border border-ink-600 rounded-2xl p-5" :class="nnEnabled ? '' : 'opacity-60'">
 
618
  ejLeagues:{}, ejLeagueList:[], ejLeague:'', ejSeason:'', ejSeasons:[], ejLabels:{},
619
  ejX:'elaboracion', ejY:'verticalidad', ejLado:'gen', ejMet:'z', ejData:null, ejError:'', ejLoaded:false,
620
  ejPostMsg:'', ejPostMsgError:false,
621
+ mlLeagues: [], ejPartidos: {},
622
 
623
  async init() { await this.loadLeagues(); await this.loadSeasons(false); await this.loadTeams(); this._refreshHealth(); },
624
  async _getJSON(u){ const r=await fetch(u); if(!r.ok) throw new Error((await r.json().catch(()=>({detail:r.statusText}))).detail); return r.json(); },
 
756
  async loadLeagues(){
757
  try {
758
  const q = this.tab==='partidos' ? '?model_only=true' : '';
759
+ let ls = await this._getJSON('/api/leagues'+q);
760
+ if (this.tab==='partidos'){
761
+ this.mlLeagues = [...ls];
762
+ try {
763
+ const o = await this._getJSON('/api/ejes/options');
764
+ this.ejPartidos = o.leagues || {};
765
+ for (const lg of Object.keys(this.ejPartidos)) if (!ls.includes(lg)) ls.push(lg);
766
+ ls.sort((x,y)=> ((x!=='Spanish Segunda Division')-(y!=='Spanish Segunda Division')) || x.localeCompare(y));
767
+ } catch(_e){}
768
+ }
769
+ this.leagues = ls;
770
  if (this.leagues.length && !this.leagues.includes(this.league)) this.league = this.leagues[0];
771
  } catch(e){ this.leagues = this.league?[this.league]:[]; }
772
  },
773
  async loadSeasons(reset=true){
774
  try {
775
  this.seasons = await this._getJSON(`/api/seasons?league=${encodeURIComponent(this.league)}`);
776
+ } catch(e){
777
+ const ej = (this.ejPartidos[this.league]||[]).slice().sort().reverse();
778
+ this.seasons = ej.length ? ej : (this.season?[this.season]:[]);
779
+ }
780
+ if (this.seasons.length && (reset || !this.seasons.includes(this.season))) this.season = this.seasons[0];
781
  },
782
  async loadTeams(){
783
  try {
784
  this.teams = await this._getJSON(`/api/teams?league=${encodeURIComponent(this.league)}&season=${encodeURIComponent(this.season)}`);
785
+ } catch(e){
786
+ try {
787
+ const o = await this._getJSON(`/api/ejes/teams?league=${encodeURIComponent(this.league)}&season=${encodeURIComponent(this.season)}`);
788
+ this.teams = o.teams || [];
789
+ } catch(_e){ this.teams = []; }
790
+ }
791
  // Esperar a que el <select> tenga sus <option> renderizados antes de fijar
792
  // el valor, si no el navegador muestra la primera opción y no Racing.
793
  await this.$nextTick();
 
821
  },
822
  async runPostMatch(){
823
  this.postMatchMsg=''; this.postMatchMsgError=false;
824
+ if (!this.mlLeagues.includes(this.league)){ this._pmErr('El post-partido con modelo solo está disponible para las ligas del dataset NN.'); return; }
825
  if (!this.teamA||!this.teamB){ this._pmErr('Elegí local y visitante.'); return; }
826
  if (this.teamA===this.teamB){ this._pmErr('A y B deben ser distintos.'); return; }
827
  this.postMatchChecking=true;
 
843
 
844
  async ejRunPost(){
845
  this.ejPostMsg=''; this.ejPostMsgError=false;
846
+ if (Object.keys(this.ejPartidos).length && !this.ejPartidos[this.league]){ this.ejPostMsg='Esta liga no tiene artefactos de ejes.'; this.ejPostMsgError=true; return; }
847
  if (!this.teamA||!this.teamB){ this.ejPostMsg='Elegí los dos equipos.'; this.ejPostMsgError=true; return; }
848
  if (this.teamA===this.teamB){ this.ejPostMsg='A y B deben ser distintos.'; this.ejPostMsgError=true; return; }
849
  this.ejPostMsg='Usa el último cruce jugado entre ambos (según artefactos de ejes).';