alexp97 commited on
Commit
f514b00
·
1 Parent(s): 79bbf00

build(deploy): pipeline Astro (build+inline) + deploy_prod rsconnect + .rscignore

Browse files

- scripts/build.ps1: compila Astro -> scripts/inline_js.py (inline JS + rutas relativas,
Regla de Oro) -> copia a backend/static. Reemplaza build_ui.ps1.
- scripts/inline_js.py: post-proceso del build para sub-paths de ShinyApps.
- scripts/deploy_prod.ps1: build + uv export requirements.txt + traduce .rscignore a
--exclude + rsconnect deploy shiny . --entrypoint backend.main:app (-Name/-AppId
parametrizables; app-id pendiente).
- .rscignore: exclusiones del bundle; **excluye .env** (BYOK, sin secretos en prod).
- dev.ps1: gateway (:8000) + Astro dev (:4321 hot-reload) en ventanas separadas.
- pyproject: rsconnect-python (dev). .gitignore: requirements.txt (generado).
- ejecucion.md §5: despliegue Agro-Stack documentado.
- Verificado: build.ps1 e inline_js.py corren OK; rsconnect importable.

.gitignore CHANGED
@@ -39,6 +39,8 @@ frontend/pnpm-debug.log*
39
  backend/static/
40
  backend/static/*
41
  !backend/static/.gitkeep
 
 
42
 
43
  # --- Data & SQLite ---
44
  data/*
 
39
  backend/static/
40
  backend/static/*
41
  !backend/static/.gitkeep
42
+ # requirements.txt se genera en el deploy (uv export); la fuente es pyproject + uv.lock
43
+ requirements.txt
44
 
45
  # --- Data & SQLite ---
46
  data/*
.rscignore ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Exclusiones para el deploy a ShinyApps.io.
2
+ #
3
+ # IMPORTANTE: rsconnect-python NO lee este archivo por sí solo. Lo consume
4
+ # `scripts/deploy_prod.ps1`, que convierte cada línea en un flag `--exclude` de
5
+ # `rsconnect deploy shiny .`. Nombrar una carpeta basta (rsconnect la expande).
6
+ #
7
+ # Lo que SÍ va en el bundle: backend/ (incl. backend/static = UI Astro compilada),
8
+ # pyproject.toml, uv.lock, requirements.txt y .env.example.
9
+
10
+ # --- Entornos / dependencias (pesados) ---
11
+ .venv
12
+ node_modules
13
+ frontend/node_modules
14
+
15
+ # --- Caches de herramientas ---
16
+ .hypothesis
17
+ .pytest_cache
18
+ .mypy_cache
19
+ .ruff_cache
20
+ __pycache__
21
+ .git
22
+ .astro
23
+
24
+ # --- Fuente del frontend (lo compilado vive en backend/static) ---
25
+ frontend
26
+
27
+ # --- Desarrollo / no necesarios en runtime ---
28
+ tests
29
+ scripts
30
+ docs
31
+ notebooks
32
+ scratch
33
+ supabase
34
+ models
35
+ sample_data
36
+ .playwright-mcp
37
+ .claude
38
+ .agents
39
+ .vscode
40
+ .idea
41
+ .github
42
+
43
+ # --- SECRETOS: el modelo es BYOK (el usuario pone sus llaves por sesión).
44
+ # NO subir .env al bundle. ---
45
+ .env
46
+ .env.local
47
+
48
+ # --- Archivos sueltos ---
49
+ *.png
50
+ *.jpg
51
+ *.jpeg
52
+ *.gif
53
+ *.webp
54
+ *.csv
55
+ *.xlsx
56
+ *.db
57
+ *.sqlite
58
+ *.onnx
59
+ *.pt
60
+ *.log
61
+ *.tmp
docs/ejecucion.md CHANGED
@@ -61,7 +61,7 @@ uv sync
61
 
62
  La UI es **Astro + Tailwind** (Node/pnpm). Compílala a estático con:
63
  ```powershell
64
- .\scripts\build_ui.ps1 # = pnpm install + pnpm build (en frontend/) -> backend/static
65
  ```
66
  El gateway sirve ese build en `/`. Para hot-reload usa `pnpm dev` (ver §3.2).
67
 
@@ -86,7 +86,7 @@ cp .env.example .env
86
 
87
  ```powershell
88
  # 1) Compilar la UI Astro -> backend/static (una vez, o tras cambios de UI)
89
- .\scripts\build_ui.ps1
90
  # 2) Levantar el gateway (sirve la UI en / y la API en /api)
91
  .\scripts\run_backend.ps1
92
  ```
@@ -168,29 +168,35 @@ docker compose up --build # gateway en :8000 (sirve API + UI Astro compilada)
168
  > - Verifica `uv run ruff check .` y `uv run python -m pytest` en verde antes de desplegar.
169
  > - No se versionan secretos: configúralos en el panel de cada plataforma.
170
 
171
- ### 5.2 Configuración del Entorno de Despliegue
172
 
173
- ```bash
174
- # UI -> ShinyApps.io (token desde https://www.shinyapps.io/admin/#/tokens)
 
 
175
  uv run rsconnect add --account <cuenta> --name <cuenta> --token <TOKEN> --secret <SECRET>
176
  ```
177
 
178
- ### 5.3 Ejecución del Despliegue
179
 
180
- ```bash
181
- # UI (Shiny) -> ShinyApps.io
182
- uv run rsconnect deploy shiny ./frontend --name <cuenta> --title AgroVision-MVP
183
-
184
- # Backend -> Render (detecta backend/Dockerfile vía render.yaml)
185
- # Conecta el repo en el panel de Render o usa la API con RENDER_API_KEY.
186
  ```
 
 
 
 
 
187
 
188
  ### 5.4 Verificación Post-Despliegue
189
 
190
- - [ ] URL pública de la UI accesible vía HTTPS.
191
- - [ ] `https://<backend-en-render>/api/status` responde `200`.
192
- - [ ] CORS entre la UI (ShinyApps.io) y el backend (Render) configurado (`ALLOWED_ORIGINS`).
193
- - [ ] Recargar la UI (F5) no rompe la sesión (Shiny es ASGI nativo en ShinyApps.io).
194
 
195
  ---
196
 
 
61
 
62
  La UI es **Astro + Tailwind** (Node/pnpm). Compílala a estático con:
63
  ```powershell
64
+ .\scripts\build.ps1 # = pnpm build + inline_js.py + copia a backend/static
65
  ```
66
  El gateway sirve ese build en `/`. Para hot-reload usa `pnpm dev` (ver §3.2).
67
 
 
86
 
87
  ```powershell
88
  # 1) Compilar la UI Astro -> backend/static (una vez, o tras cambios de UI)
89
+ .\scripts\build.ps1
90
  # 2) Levantar el gateway (sirve la UI en / y la API en /api)
91
  .\scripts\run_backend.ps1
92
  ```
 
168
  > - Verifica `uv run ruff check .` y `uv run python -m pytest` en verde antes de desplegar.
169
  > - No se versionan secretos: configúralos en el panel de cada plataforma.
170
 
171
+ ### 5.2 Modelo de despliegue (Agro-Stack)
172
 
173
+ Un solo servicio: el **gateway FastAPI** (`backend.main:app`) sirve la **UI Astro compilada** en `/`, la **API** en `/api` y (opcional) el Shiny legacy en `/shiny`. Se despliega a **ShinyApps.io** con `rsconnect` (como app ASGI), aplicando la **Regla de Oro** (JS inline + rutas relativas vía `scripts/inline_js.py`).
174
+
175
+ Registrar el token (una vez):
176
+ ```powershell
177
  uv run rsconnect add --account <cuenta> --name <cuenta> --token <TOKEN> --secret <SECRET>
178
  ```
179
 
180
+ ### 5.3 Ejecución del Despliegue (script)
181
 
182
+ ```powershell
183
+ # Primer deploy (crea la app en ShinyApps.io):
184
+ .\scripts\deploy_prod.ps1 -Name <cuenta>
185
+ # Redeploy (cuando ya exista el app-id):
186
+ .\scripts\deploy_prod.ps1 -Name <cuenta> -AppId <id>
 
187
  ```
188
+ `deploy_prod.ps1` hace: (0) compila la UI (`build.ps1`), (1) genera `requirements.txt` (`uv export`), (2) traduce `.rscignore` a flags `--exclude`, (3) `rsconnect deploy shiny . --entrypoint backend.main:app`.
189
+
190
+ > **BYOK / seguridad:** el `.env` **NO** viaja en el bundle (está en `.rscignore`); en producción las llaves las pone el usuario por sesión (cabeceras `X-User-*`).
191
+ > **Aún no hay app-id** para AgroVisión: el primer deploy con `-Name` lo crea; luego reutiliza el id con `-AppId` para redeploy.
192
+ > **Alternativa:** backend en **Render** con `backend/Dockerfile` (etapa Node compila Astro y el gateway sirve `/`).
193
 
194
  ### 5.4 Verificación Post-Despliegue
195
 
196
+ - [ ] URL pública accesible vía HTTPS; `/` carga la UI Astro.
197
+ - [ ] `https://<app>/api/status` responde `200`.
198
+ - [ ] Recargar (F5) no rompe la app: la SPA usa **hash-routing** + **rutas relativas** (Regla de Oro), así que el slug dinámico de ShinyApps no produce 404.
199
+ - [ ] Las credenciales se ingresan en la pestaña *Credenciales* (BYOK; no hay secretos en el bundle).
200
 
201
  ---
202
 
pyproject.toml CHANGED
@@ -34,6 +34,7 @@ dev = [
34
  "pytest>=8",
35
  "ruff>=0.6",
36
  "playwright>=1.45",
 
37
  ]
38
 
39
  [tool.uv]
 
34
  "pytest>=8",
35
  "ruff>=0.6",
36
  "playwright>=1.45",
37
+ "rsconnect-python>=1.28", # deploy a ShinyApps.io (scripts/deploy_prod.ps1)
38
  ]
39
 
40
  [tool.uv]
scripts/build.ps1 ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # scripts/build.ps1 — Pipeline de build de la UI: Astro -> inline -> backend/static
2
+ # (lo que sirve el gateway FastAPI en /). Uso: .\scripts\build.ps1
3
+ $ErrorActionPreference = "Stop"
4
+ $root = Split-Path -Parent $PSScriptRoot
5
+ Set-Location $root
6
+ # venv fuera de OneDrive (para uv) — ver docs/ejecucion.md
7
+ $env:UV_PROJECT_ENVIRONMENT = "$env:LOCALAPPDATA\agrovision-venv"
8
+
9
+ Write-Host "=== Build AgroVisión UI (Astro -> backend/static) ===" -ForegroundColor Cyan
10
+
11
+ # 1) Compilar Astro
12
+ Write-Host "`n[1/3] Compilando Astro (pnpm build)..." -ForegroundColor Yellow
13
+ Push-Location "$root\frontend"
14
+ pnpm install
15
+ pnpm build
16
+ $buildOk = ($LASTEXITCODE -eq 0)
17
+ Pop-Location
18
+ if (-not $buildOk) { Write-Error "pnpm build falló"; exit 1 }
19
+
20
+ # 2) Post-procesar: inline de JS y rutas relativas (Regla de Oro para ShinyApps)
21
+ Write-Host "`n[2/3] Post-procesando index.html (inline_js.py)..." -ForegroundColor Yellow
22
+ uv run python scripts/inline_js.py
23
+ if ($LASTEXITCODE -ne 0) { Write-Error "inline_js.py falló"; exit 1 }
24
+
25
+ # 3) Copiar el build a backend/static (preservando .gitkeep)
26
+ Write-Host "`n[3/3] Copiando frontend\dist -> backend\static..." -ForegroundColor Yellow
27
+ $static = "$root\backend\static"
28
+ New-Item -ItemType Directory -Force -Path $static | Out-Null
29
+ Get-ChildItem $static -Exclude ".gitkeep" -Force | Remove-Item -Recurse -Force
30
+ Copy-Item -Path "$root\frontend\dist\*" -Destination $static -Recurse -Force
31
+ if (-not (Test-Path "$static\.gitkeep")) { New-Item -ItemType File "$static\.gitkeep" | Out-Null }
32
+
33
+ Write-Host "`n=== Build completado. Sirve con: .\scripts\run_backend.ps1 -> http://127.0.0.1:8000/ ===" -ForegroundColor Green
scripts/build_ui.ps1 DELETED
@@ -1,17 +0,0 @@
1
- # Compila la UI Astro y la copia a backend/static (servida por el gateway FastAPI en /).
2
- # Uso: .\scripts\build_ui.ps1
3
- Set-Location (Split-Path -Parent $PSScriptRoot)
4
-
5
- Push-Location frontend
6
- pnpm install
7
- pnpm build
8
- Pop-Location
9
-
10
- $static = "backend/static"
11
- New-Item -ItemType Directory -Force -Path $static | Out-Null
12
- # Limpia el estático anterior conservando .gitkeep
13
- Get-ChildItem $static -Exclude ".gitkeep" -Force | Remove-Item -Recurse -Force
14
- Copy-Item -Recurse -Force "frontend/dist/*" "$static/"
15
- if (-not (Test-Path "$static/.gitkeep")) { New-Item -ItemType File "$static/.gitkeep" | Out-Null }
16
-
17
- Write-Host "UI Astro compilada -> backend/static (sirve el gateway en http://127.0.0.1:8000/)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/deploy_prod.ps1 ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # scripts/deploy_prod.ps1 — Despliegue de AgroVisión a ShinyApps.io (Agro-Stack).
2
+ #
3
+ # El entrypoint es el gateway FastAPI (backend.main:app), que sirve la UI Astro
4
+ # compilada en / y la API en /api. El bundle es el repo (`.`) MENOS las exclusiones
5
+ # de `.rscignore` (rsconnect-python no lee .rscignore solo: aquí se traducen a --exclude).
6
+ #
7
+ # BYOK: el `.env` (secretos) NO viaja en el bundle (está en .rscignore); en producción
8
+ # las credenciales las pone el usuario por sesión (cabeceras X-User-*).
9
+ #
10
+ # Aún no hay app-id para esta app: pásalo cuando exista para hacer redeploy.
11
+ # Primer deploy: .\scripts\deploy_prod.ps1 -Name <tu_cuenta>
12
+ # Redeploy: .\scripts\deploy_prod.ps1 -Name <tu_cuenta> -AppId <id>
13
+ # Registrar token (una vez):
14
+ # uv run rsconnect add --account <cuenta> --name <cuenta> --token <TOKEN> --secret <SECRET>
15
+ param(
16
+ [string]$Name = "<TU_CUENTA_SHINYAPPS>",
17
+ [string]$AppId = "" # vacío = primer deploy (crea la app); luego usa el id para redeploy
18
+ )
19
+ $ErrorActionPreference = "Stop"
20
+ $root = Split-Path -Parent $PSScriptRoot
21
+ Set-Location $root
22
+ $env:UV_PROJECT_ENVIRONMENT = "$env:LOCALAPPDATA\agrovision-venv"
23
+
24
+ Write-Host "=== Deploy AgroVisión -> ShinyApps.io ===" -ForegroundColor Cyan
25
+
26
+ # 0) Compilar la UI (Astro -> backend/static)
27
+ Write-Host "`n[0/3] Compilando la UI..." -ForegroundColor Yellow
28
+ & "$PSScriptRoot\build.ps1"
29
+
30
+ # 1) requirements.txt para ShinyApps (no usa uv; instala desde requirements.txt)
31
+ Write-Host "`n[1/3] Generando requirements.txt (uv export)..." -ForegroundColor Yellow
32
+ uv export --no-dev --no-hashes --no-emit-project -o requirements.txt
33
+ if ($LASTEXITCODE -ne 0) { Write-Error "uv export falló"; exit 1 }
34
+
35
+ # 2) Construir los --exclude desde .rscignore (ignora comentarios y vacíos)
36
+ $patterns = @()
37
+ if (Test-Path ".rscignore") {
38
+ $patterns = Get-Content ".rscignore" |
39
+ ForEach-Object { $_.Trim() } |
40
+ Where-Object { $_ -and (-not $_.StartsWith("#")) } |
41
+ Select-Object -Unique
42
+ } else {
43
+ Write-Warning ".rscignore no encontrado: el bundle incluiría TODO el repo."
44
+ }
45
+ $excludeArgs = @()
46
+ foreach ($p in $patterns) { $excludeArgs += "--exclude"; $excludeArgs += $p }
47
+ Write-Host ("[2/3] {0} patrones de exclusión cargados desde .rscignore" -f $patterns.Count)
48
+
49
+ if ($Name -eq "<TU_CUENTA_SHINYAPPS>") {
50
+ Write-Warning "Falta tu cuenta de ShinyApps. Uso: .\scripts\deploy_prod.ps1 -Name <cuenta> [-AppId <id>]"
51
+ Write-Warning "Y registra el token primero (ver cabecera de este script)."
52
+ exit 1
53
+ }
54
+
55
+ # 3) Desplegar (entrypoint = gateway FastAPI que sirve la UI Astro + /api + /shiny)
56
+ Write-Host "`n[3/3] Desplegando (entrypoint backend.main:app)..." -ForegroundColor Yellow
57
+ $appIdArgs = @()
58
+ if ($AppId) { $appIdArgs = @("--app-id", $AppId) }
59
+ & uv run python -c "from rsconnect.main import cli; cli()" deploy shiny . `
60
+ --entrypoint backend.main:app --name $Name @appIdArgs @excludeArgs
61
+
62
+ Write-Host "`n=== Despliegue completado ===" -ForegroundColor Green
scripts/dev.ps1 CHANGED
@@ -1,8 +1,14 @@
1
- # Arranca backend + UI en ventanas PowerShell separadas (local, sin Docker).
2
- # Uso: ./scripts/dev.ps1
 
 
 
 
3
  Start-Process powershell -ArgumentList "-NoExit", "-File", "`"$PSScriptRoot\run_backend.ps1`""
4
  Start-Sleep -Seconds 2
5
- Start-Process powershell -ArgumentList "-NoExit", "-File", "`"$PSScriptRoot\run_ui.ps1`""
6
 
7
- Write-Host "Backend: http://127.0.0.1:8000/api/status"
8
- Write-Host "UI Shiny: http://127.0.0.1:8001"
 
 
 
1
+ # Arranca el stack de DESARROLLO en ventanas PowerShell separadas (local, sin Docker):
2
+ # - Gateway FastAPI (:8000) — API en /api (+ UI compilada en / si existe backend/static).
3
+ # - Astro dev server (:4321) — UI con hot-reload; proxea /api -> :8000.
4
+ # Uso: .\scripts\dev.ps1
5
+ $root = Split-Path -Parent $PSScriptRoot
6
+
7
  Start-Process powershell -ArgumentList "-NoExit", "-File", "`"$PSScriptRoot\run_backend.ps1`""
8
  Start-Sleep -Seconds 2
9
+ Start-Process powershell -ArgumentList "-NoExit", "-Command", "Set-Location '$root\frontend'; pnpm dev"
10
 
11
+ Write-Host "API: http://127.0.0.1:8000/api/status"
12
+ Write-Host "UI (Astro, dev): http://localhost:4321/ (hot-reload)"
13
+ Write-Host "UI (compilada): http://127.0.0.1:8000/ (tras .\scripts\build.ps1)"
14
+ Write-Host "UI Shiny (legacy): .\scripts\run_ui.ps1 -> http://127.0.0.1:8001"
scripts/inline_js.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Archivo: inline_js.py
3
+ Fecha de modificación: 04/06/2026
4
+ Autor: Equipo AgroVisión
5
+
6
+ Descripción:
7
+ Post-procesa el HTML del build de Astro para que funcione bajo **sub-paths dinámicos**
8
+ (como los slugs `/_w_xxxx/` de ShinyApps.io). Inyecta inline los scripts que Astro
9
+ hubiera emitido en `/_astro/*.js` y normaliza las rutas de assets (favicon, JS) de
10
+ **absolutas a relativas**, ya que el host sirve la app desde una ruta distinta de la raíz.
11
+ Es la implementación de la "Regla de Oro" de `docs/doc_guia/plan_replication.md`.
12
+
13
+ Nota: nuestra UI ya usa scripts `is:inline` y `inlineStylesheets: 'always'`, por lo que
14
+ normalmente no hay `/_astro/*.js` externos; este script es idempotente y a prueba de
15
+ futuro (si se añade una isla/componente que genere JS externo, queda inlineado).
16
+
17
+ Acciones Principales:
18
+ - Inyecta el contenido de los scripts `/_astro/*.js` directamente en el HTML.
19
+ - Convierte las rutas absolutas de favicon a relativas.
20
+ - Normaliza cualquier referencia absoluta restante a `/_astro/`.
21
+
22
+ Estructura Interna:
23
+ - `inline_scripts`: inyecta el código JS externo inline.
24
+ - `fix_favicon_paths`: relativiza las rutas de iconos.
25
+ - `fix_absolute_asset_refs`: relativiza las rutas absolutas restantes.
26
+ - `main`: orquesta el post-proceso sobre `frontend/dist/index.html`.
27
+
28
+ Entradas / Dependencias:
29
+ - Biblioteca estándar (`re`, `pathlib`).
30
+
31
+ Salidas / Efectos:
32
+ - Reescribe `frontend/dist/index.html` in place.
33
+
34
+ Ejecución:
35
+ uv run python scripts/inline_js.py
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import re
41
+ import sys
42
+ from pathlib import Path
43
+
44
+ DIST = Path("frontend/dist")
45
+ HTML_FILE = DIST / "index.html"
46
+
47
+
48
+ def inline_scripts(html: str) -> str:
49
+ """
50
+ Reemplaza cada `<script ... src="/_astro/...">` por su contenido inline.
51
+
52
+ Args:
53
+ html (str): HTML de entrada.
54
+
55
+ Returns:
56
+ str: HTML con los scripts de Astro embebidos inline.
57
+ """
58
+
59
+ def _replace(match: re.Match) -> str: # type: ignore[type-arg]
60
+ src = match.group(1)
61
+ rel = src.lstrip("/") # la URL parte de "/"; el archivo es relativo a DIST
62
+ js_path = DIST / rel
63
+ if js_path.exists():
64
+ content = js_path.read_text(encoding="utf-8")
65
+ return f'<script type="module">{content}</script>'
66
+ print(f" [WARN] JS no encontrado: {js_path}", file=sys.stderr)
67
+ return match.group(0)
68
+
69
+ pattern = (
70
+ r'<script\s[^>]*type=["\']module["\'][^>]*'
71
+ r'src=["\'](\/_astro\/[^"\']+)["\'][^>]*>\s*</script>'
72
+ )
73
+ return re.sub(pattern, _replace, html)
74
+
75
+
76
+ def fix_favicon_paths(html: str) -> str:
77
+ """Convierte rutas absolutas `/favicon.*` a relativas `./favicon.*`."""
78
+ html = re.sub(r'href="\/favicon\.', 'href="./favicon.', html)
79
+ html = re.sub(r"href='\/favicon\.", "href='./favicon.", html)
80
+ return html
81
+
82
+
83
+ def fix_absolute_asset_refs(html: str) -> str:
84
+ """Convierte cualquier referencia absoluta restante a `/_astro/` en relativa."""
85
+ html = html.replace('href="/_astro/', 'href="./_astro/')
86
+ html = html.replace("href='/_astro/", "href='./_astro/")
87
+ html = html.replace('src="/_astro/', 'src="./_astro/')
88
+ html = html.replace("src='/_astro/", "src='./_astro/")
89
+ return html
90
+
91
+
92
+ def main() -> None:
93
+ """
94
+ Aplica el post-proceso sobre `frontend/dist/index.html` in place.
95
+
96
+ Raises:
97
+ SystemExit: si no existe el HTML del build (código 1).
98
+ """
99
+ if not HTML_FILE.exists():
100
+ print(f"[ERROR] No se encontró {HTML_FILE}. Ejecuta 'pnpm build' (en frontend/) primero.")
101
+ sys.exit(1)
102
+
103
+ html = HTML_FILE.read_text(encoding="utf-8")
104
+ original_len = len(html)
105
+
106
+ html = inline_scripts(html)
107
+ html = fix_favicon_paths(html)
108
+ html = fix_absolute_asset_refs(html)
109
+
110
+ HTML_FILE.write_text(html, encoding="utf-8")
111
+
112
+ remaining = re.findall(r'["\']/_astro/', html)
113
+ if remaining:
114
+ print(f"[WARN] Aún quedan {len(remaining)} referencias a /_astro/ sin convertir.")
115
+ else:
116
+ print(f"[OK] inline_js.py completado ({original_len} -> {len(html)} bytes).")
117
+ print(" No quedan referencias absolutas a /_astro/ en index.html.")
118
+
119
+
120
+ if __name__ == "__main__":
121
+ main()
uv.lock CHANGED
@@ -38,6 +38,7 @@ dependencies = [
38
  dev = [
39
  { name = "playwright" },
40
  { name = "pytest" },
 
41
  { name = "ruff" },
42
  ]
43
 
@@ -70,6 +71,7 @@ requires-dist = [
70
  dev = [
71
  { name = "playwright", specifier = ">=1.45" },
72
  { name = "pytest", specifier = ">=8" },
 
73
  { name = "ruff", specifier = ">=0.6" },
74
  ]
75
 
@@ -1568,6 +1570,15 @@ wheels = [
1568
  { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" },
1569
  ]
1570
 
 
 
 
 
 
 
 
 
 
1571
  [[package]]
1572
  name = "platformdirs"
1573
  version = "4.10.0"
@@ -1867,6 +1878,15 @@ wheels = [
1867
  { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
1868
  ]
1869
 
 
 
 
 
 
 
 
 
 
1870
  [[package]]
1871
  name = "pyparsing"
1872
  version = "3.3.2"
@@ -2017,6 +2037,22 @@ wheels = [
2017
  { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
2018
  ]
2019
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2020
  [[package]]
2021
  name = "ruff"
2022
  version = "0.15.15"
@@ -2113,6 +2149,15 @@ wheels = [
2113
  { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" },
2114
  ]
2115
 
 
 
 
 
 
 
 
 
 
2116
  [[package]]
2117
  name = "setuptools"
2118
  version = "82.0.1"
@@ -2443,6 +2488,32 @@ wheels = [
2443
  { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
2444
  ]
2445
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2446
  [[package]]
2447
  name = "uvicorn"
2448
  version = "0.48.0"
 
38
  dev = [
39
  { name = "playwright" },
40
  { name = "pytest" },
41
+ { name = "rsconnect-python" },
42
  { name = "ruff" },
43
  ]
44
 
 
71
  dev = [
72
  { name = "playwright", specifier = ">=1.45" },
73
  { name = "pytest", specifier = ">=8" },
74
+ { name = "rsconnect-python", specifier = ">=1.28" },
75
  { name = "ruff", specifier = ">=0.6" },
76
  ]
77
 
 
1570
  { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" },
1571
  ]
1572
 
1573
+ [[package]]
1574
+ name = "pip"
1575
+ version = "26.1.2"
1576
+ source = { registry = "https://pypi.org/simple" }
1577
+ sdist = { url = "https://files.pythonhosted.org/packages/01/91/47e7d486260f618783899587af63ccf7980fb60245c3e63dd4571c6b57ad/pip-26.1.2.tar.gz", hash = "sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605", size = 1840799, upload-time = "2026-05-31T17:33:58.56Z" }
1578
+ wheels = [
1579
+ { url = "https://files.pythonhosted.org/packages/5d/95/6b5cb3461ea5673ba0995989746db58eb18b91b54dbf331e72f569540946/pip-26.1.2-py3-none-any.whl", hash = "sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab", size = 1813144, upload-time = "2026-05-31T17:33:56.772Z" },
1580
+ ]
1581
+
1582
  [[package]]
1583
  name = "platformdirs"
1584
  version = "4.10.0"
 
1878
  { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
1879
  ]
1880
 
1881
+ [[package]]
1882
+ name = "pyjwt"
1883
+ version = "2.13.0"
1884
+ source = { registry = "https://pypi.org/simple" }
1885
+ sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" }
1886
+ wheels = [
1887
+ { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" },
1888
+ ]
1889
+
1890
  [[package]]
1891
  name = "pyparsing"
1892
  version = "3.3.2"
 
2037
  { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
2038
  ]
2039
 
2040
+ [[package]]
2041
+ name = "rsconnect-python"
2042
+ version = "1.29.0"
2043
+ source = { registry = "https://pypi.org/simple" }
2044
+ dependencies = [
2045
+ { name = "click" },
2046
+ { name = "pip" },
2047
+ { name = "pyjwt" },
2048
+ { name = "semver" },
2049
+ { name = "typing-extensions" },
2050
+ { name = "uv" },
2051
+ ]
2052
+ wheels = [
2053
+ { url = "https://files.pythonhosted.org/packages/4b/2d/e1c992e5f63421cdc01a9398a67c62c886b711085d16c01abee7335354ca/rsconnect_python-1.29.0-py2.py3-none-any.whl", hash = "sha256:187960581cd5f277409b94d01d30e47e731d89eb28bb62b74aea551b921f7f6d", size = 116926, upload-time = "2026-04-29T19:10:42.708Z" },
2054
+ ]
2055
+
2056
  [[package]]
2057
  name = "ruff"
2058
  version = "0.15.15"
 
2149
  { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" },
2150
  ]
2151
 
2152
+ [[package]]
2153
+ name = "semver"
2154
+ version = "3.0.4"
2155
+ source = { registry = "https://pypi.org/simple" }
2156
+ sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730, upload-time = "2025-01-24T13:19:27.617Z" }
2157
+ wheels = [
2158
+ { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" },
2159
+ ]
2160
+
2161
  [[package]]
2162
  name = "setuptools"
2163
  version = "82.0.1"
 
2488
  { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
2489
  ]
2490
 
2491
+ [[package]]
2492
+ name = "uv"
2493
+ version = "0.11.19"
2494
+ source = { registry = "https://pypi.org/simple" }
2495
+ sdist = { url = "https://files.pythonhosted.org/packages/67/f0/6254502aebfdc0a9df6069269a126dd58252ac29d2d6cdf4777cea3e90b5/uv-0.11.19.tar.gz", hash = "sha256:f56f5bf853626a30423052d7ee00bf5cc940a08347d6ee7ede96862d084054a5", size = 4213580, upload-time = "2026-06-03T22:37:15.976Z" }
2496
+ wheels = [
2497
+ { url = "https://files.pythonhosted.org/packages/1a/73/be32c2f6ba30fa9d8b3baceb478107cc23722d4aaab87145a332e4985185/uv-0.11.19-py3-none-linux_armv6l.whl", hash = "sha256:c729f56ffef9b945053412c839695e8a0b13758aa15b7763e95a7dd539a6f522", size = 23620003, upload-time = "2026-06-03T22:37:53.017Z" },
2498
+ { url = "https://files.pythonhosted.org/packages/fd/ed/3aefe4a4ca4ac9204c6745670dbe12f4add69194d40f5abd1c7bd45ba9af/uv-0.11.19-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a98495b9dd67287d8c1a0786f98cb037a50f0ee6c3d648572edaa7137aabc277", size = 23183211, upload-time = "2026-06-03T22:37:20.699Z" },
2499
+ { url = "https://files.pythonhosted.org/packages/5b/eb/5d1469f9e709d56066f292978711fbf1f805b7fb46f901d3c1f260fd9908/uv-0.11.19-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7fdd881cd6d80782afcf8c1d446dd15a42985167fd812b763d38ba1e4a8d944d", size = 21754003, upload-time = "2026-06-03T22:37:05.027Z" },
2500
+ { url = "https://files.pythonhosted.org/packages/7b/93/109b5ee6678f54492f94fdef74149643eaa1f2f4716906a2a10816b31247/uv-0.11.19-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:7222f45b5541551057bfc2e3021f113800704f665c119fdf3ea700c6c4859b21", size = 23518832, upload-time = "2026-06-03T22:37:28.794Z" },
2501
+ { url = "https://files.pythonhosted.org/packages/08/0c/8c59bbcf78e94ca9994256920efa99d1c4dc9d0b966eb62ebba075585a16/uv-0.11.19-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:2e0e0b8ad59ec56f1440d6e4313b64a1d8119275dcec73d19eef33c43f99428c", size = 23163128, upload-time = "2026-06-03T22:37:23.226Z" },
2502
+ { url = "https://files.pythonhosted.org/packages/89/d6/69caf9e6f11c84b5fb92df190b46fbecb7dc6645ae891c6ed66d7aaaa310/uv-0.11.19-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4aa17ffd719daf37b7a6265efd3ee4922a8ddaabaf0406d2b28c7e5ce2f20ff", size = 23164395, upload-time = "2026-06-03T22:37:18.11Z" },
2503
+ { url = "https://files.pythonhosted.org/packages/d6/83/0c2242b77c51ac33a0ddd8b06790429a0b8b9623974c9594ab2b0070ec47/uv-0.11.19-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32d7988c0dfb6f90941f201c871a4478e96e4f2a32bdb2256d62a78ee20593fc", size = 24541708, upload-time = "2026-06-03T22:37:08.093Z" },
2504
+ { url = "https://files.pythonhosted.org/packages/54/10/b1404fc52c0eddc3655f57a8b76e79dcf8dd02568382272f17e2fa68c4bb/uv-0.11.19-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2d663bacb97e2e8412d1c26eace28c7ebbde9d6f5d7d78760fafd114d693817f", size = 25575501, upload-time = "2026-06-03T22:37:47.526Z" },
2505
+ { url = "https://files.pythonhosted.org/packages/7c/17/4cda5994195ba9ce1f6971d40d5f2ceec58e2a79030d9052b3bf322557b1/uv-0.11.19-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:574f5dd4f31666661ea6386d3b91c5f0e8b84a8cae98ebba447c4674f2e6a4c7", size = 24827200, upload-time = "2026-06-03T22:37:34.039Z" },
2506
+ { url = "https://files.pythonhosted.org/packages/5a/74/2bd8b51e1d76210fd424ae55ec3f34ded5a10eeff3dd38aeb03c816a0af2/uv-0.11.19-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:731d9fab8db5d41590af64236d03f8069c8da665fd0f9493b85985f19c86cd90", size = 24872664, upload-time = "2026-06-03T22:37:11.301Z" },
2507
+ { url = "https://files.pythonhosted.org/packages/06/b1/44b0764f656bbdd0728118610a63f2feddd9cbe450f974d80c5bb56aad34/uv-0.11.19-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:301fd78309fc545c2cec2bfcc61a6bbdde876856c6d2041502737cf44085c178", size = 23617890, upload-time = "2026-06-03T22:37:44.796Z" },
2508
+ { url = "https://files.pythonhosted.org/packages/d2/25/312fa33cd4c34e7618f86cad0c9fdb312d8fef2e7fc61944c1a2f1bf1256/uv-0.11.19-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:62b0b35a51d3034ff30ecd0f381e9bbc20d5b335754f54b098da29424d551ceb", size = 24267220, upload-time = "2026-06-03T22:37:39.425Z" },
2509
+ { url = "https://files.pythonhosted.org/packages/8d/25/13856aeff9e14c98ee3e1ceae4d209301cbdeabde93abcd758433601dc82/uv-0.11.19-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:65e932720daed1af1f720a0ff5f9b33ee5f7ad97488dcceceb85154fc1323b82", size = 24376177, upload-time = "2026-06-03T22:37:50.276Z" },
2510
+ { url = "https://files.pythonhosted.org/packages/45/7d/590b3ab420e03504cf658d2981e1fcb4af60f3858d42da1d4d8740141dd9/uv-0.11.19-py3-none-musllinux_1_1_i686.whl", hash = "sha256:8f90b6687a480d154595aa619fb836a9a20d00ce37293db8099aad924f2b18f9", size = 23808336, upload-time = "2026-06-03T22:37:26.086Z" },
2511
+ { url = "https://files.pythonhosted.org/packages/9e/8e/40acebd4ea419c870930580623e8367e23d810a0ecb8cc2f44d852a27293/uv-0.11.19-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:28b0d612a766eb25756dbaa315433b726e93affa467d29a2682cc317547952ba", size = 25080747, upload-time = "2026-06-03T22:37:13.886Z" },
2512
+ { url = "https://files.pythonhosted.org/packages/9c/d3/4037b2acb2bb73b1a3ee47a1d23864ecc503f5840387afd29f621d4fd2ec/uv-0.11.19-py3-none-win32.whl", hash = "sha256:aa6a7e8d07b33ad22f4732848ebb1d9486503973c248d6e632c06ce4339fe347", size = 22459533, upload-time = "2026-06-03T22:37:36.741Z" },
2513
+ { url = "https://files.pythonhosted.org/packages/d4/43/f374fad7ad94e4a8c47cf09f00d803c76c6cc7f225668c41f4e2fb5de000/uv-0.11.19-py3-none-win_amd64.whl", hash = "sha256:480fc34a8d0967af6a90b3f99a6e5687cd5c6e29528de96bec04d6e305a59363", size = 25143888, upload-time = "2026-06-03T22:37:42.169Z" },
2514
+ { url = "https://files.pythonhosted.org/packages/18/98/d2db53ae036528b0a9407529ef175ee200b01f626c9c160978784c8af870/uv-0.11.19-py3-none-win_arm64.whl", hash = "sha256:50e4d4796ca1a6da359a4f723a0fea86640c381d3ff4fa759a41badd7cb52dee", size = 23601290, upload-time = "2026-06-03T22:37:31.393Z" },
2515
+ ]
2516
+
2517
  [[package]]
2518
  name = "uvicorn"
2519
  version = "0.48.0"