Spaces:
Running
Running
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')"
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 763 |
-
|
|
|
|
|
|
|
|
|
|
| 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){
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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).';
|