Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 34 additions & 77 deletions DEMO.md
Original file line number Diff line number Diff line change
@@ -1,92 +1,49 @@
# Demo en vivo: cómo está montada

Referencia de cómo funciona la demo pública, de dónde salen los datos y cómo usar el backend
real si hace falta. **Resumen en una frase:** la demo vive **solo en Vercel** y es
**autosuficiente** (datos de ejemplo embebidos), así que no depende de ningún backend ni sufre
cold-starts.
Referencia de cómo funciona la demo pública: qué hay desplegado, de dónde salen los datos y cómo
usar el backend. **Resumen:** frontend en **Vercel** → backend en **Google Cloud Run** → mock en
**Cloud Run**. El frontend hace fetch server-side, así que no hay CORS de navegador.

## Decisión: datos embebidos en el frontend
## Por qué Cloud Run (y no Render)

El dashboard **calcula todos los agregados en cliente** (KPIs, series, by-product, by-customer)
a partir de las ventas crudas. Eso permitió desacoplar la demo del backend: en vez de depender de
un servicio .NET + mock que en el tier gratuito **se duermen** (Render free duerme tras ~15 min y
provocaba el clásico "No hay datos" / 502 al abrir en frío), el frontend trae un **dataset de
ejemplo embebido** y lo usa directamente.
Render free dormía el backend y el mock (~15 min) y devolvía 502 al despertar, dejando la demo en
blanco. Lo intentamos tapar con workarounds en el frontend (auto-heal + datos embebidos), pero la
solución de raíz fue mover el backend a **Cloud Run**: también escala a cero, pero el arranque en
frío es ~1-2 s y **la petición espera** al contenedor en vez de fallar. Así la demo carga sin el
problema de "no hay datos". Se eliminaron los workarounds del frontend.

Resultado: la demo **carga al instante, siempre**, sin Render, sin cold-start, sin servicios que
mantener despiertos. El backend real sigue existiendo (es el producto), pero es **opcional** para
la demo.
## Dónde está desplegado cada pieza

## Dónde está desplegado el frontend
| Pieza | Hosting | Notas |
|-------|---------|-------|
| Frontend (Next.js) | **Vercel** · `frontend/` | Siempre activo. Auto-deploy en cada push a `main`. `connect-analyzer.vercel.app`. |
| Backend (.NET 10) | **Cloud Run** · `connect-analyzer-api` | Lee del mock, agrega y sirve la API REST. <https://connect-analyzer-api-370913301749.europe-southwest1.run.app> |
| Mock (nginx) | **Cloud Run** · `connect-analyzer-mock` | Sirve `sales.txt` (fixtures). <https://connect-analyzer-mock-370913301749.europe-southwest1.run.app> |

- **Hosting:** Vercel (siempre activo, sin cold-start).
- **URL:** <https://connect-analyzer.vercel.app>
- **Proyecto:** Next.js (App Router) en `frontend/`. Vercel hace auto-deploy en cada push a `main`
(root directory = `frontend`).
## Cómo coge los datos

## Cómo coge los datos de prueba
1. **SSR**: `frontend/app/page.tsx` llama a `fetchDashboard()` (`frontend/app/lib/dashboard.ts`), que
hace `GET <BACKEND_URL>/api/sales` en el servidor. Si el backend no responde, lanza y se muestra
el error boundary (`app/error.tsx`).
2. **Cliente**: `Dashboard` recibe las ventas crudas y **deriva en cliente** KPIs, series, agregados
por producto/cliente y aplica los **filtros** (`frontend/app/lib/analytics.ts`). Sin más llamadas
al backend: los filtros recalculan sobre las ventas ya cargadas.
3. **`BACKEND_URL`** (env var de Vercel) apunta al servicio `connect-analyzer-api` de Cloud Run. En
local por defecto es `http://localhost:5080`; en Docker Compose, `http://backend:8080`.

1. **Dataset embebido:** `frontend/app/lib/sample-sales.json` — las 25 ventas ficticias del mock
(mismas fixtures que `backend/mocks/sap/data/sales.txt`, ya parseadas a JSON).
2. **`fetchDashboard()`** (`frontend/app/lib/dashboard.ts`) decide la fuente:
- Si `BACKEND_URL` apunta a un backend **alcanzable y con datos** → usa esos datos en vivo.
- En cualquier otro caso (sin `BACKEND_URL`, backend caído/dormido, respuesta vacía o timeout)
→ **cae automáticamente al dataset embebido**.
3. El route handler `frontend/app/api/dashboard/route.ts` (`GET`) reexpone esto al cliente
mismo-origen; el componente `Dashboard` deriva KPIs/gráficos del set resultante y aplica los
filtros en cliente.

**En la demo de Vercel, `BACKEND_URL` está vacío** → siempre sirve el dataset embebido → instantáneo.

### Regenerar el dataset embebido

Si cambian las fixtures del mock y quieres refrescar el JSON embebido, con el backend local
levantado en Mock:

```bash
curl -s http://localhost:5080/api/sales | python3 -m json.tool > frontend/app/lib/sample-sales.json
```

## El backend (opcional): dónde está y cómo usarlo

El backend .NET 10 (`backend/`, arquitectura hexagonal) **no es necesario para la demo**, pero es
el producto real y se puede usar/desplegar.

### En local (con datos en vivo del mock, SAP o Shopify)
## El backend: cómo usarlo

### En local
```bash
docker compose up --build # levanta sap-mock + backend + frontend
# frontend en :3000 → BACKEND_URL=http://backend:8080 (lo pone docker-compose)
docker compose up --build # mock + backend + frontend (frontend en :3000)
```

La fuente se elige con la env var **`SalesSource`** (ver `backend/CLAUDE.md` y `.env.example`):

Fuente de datos por **`SalesSource`** (ver `backend/CLAUDE.md` y `.env.example`):
- `Mock` (por defecto) — lee el `.txt` del servicio `sap-mock`.
- `Sap` — OData real del SAP Business Accelerator Hub (requiere el secreto `Sap__ApiKey`).
- `Shopify` — Admin API real (requiere `Shopify__StoreUrl`, `Shopify__ClientId`,
`Shopify__ClientSecret`).

Los secretos van en un `.env` en la raíz (gitignored); nunca en git.

### Conectar la demo a un backend en vivo

Si algún día quieres que la demo de Vercel use un backend real en vez del dataset embebido:

1. Despliega el backend (+ mock si usas `SalesSource=Mock`) — ver [`DEPLOY.md`](./DEPLOY.md)
(Render Blueprint). Para SAP/Shopify, configura los secretos en el *Environment* del servicio.
2. En Vercel, define la env var **`BACKEND_URL`** apuntando a la URL pública del backend y
**Redeploy**.
3. El frontend usará el backend si responde con datos; si no, seguirá cayendo al dataset embebido.

> Aviso: en hosting gratuito (Render) el backend se duerme y la primera petición tarda en
> despertar. Por eso, para una demo siempre-activa, lo recomendado es **dejar `BACKEND_URL` vacío**
> y servir el dataset embebido.
- `Sap` — OData real del SAP Business Accelerator Hub (secreto `Sap__ApiKey`).
- `Shopify` — Admin API real (`Shopify__StoreUrl`, `Shopify__ClientId`, `Shopify__ClientSecret`).

## Mapa rápido
Los secretos van en un `.env` en la raíz (gitignored), nunca en git.

| Pieza | Dónde | Necesaria para la demo |
|-------|-------|------------------------|
| Frontend (Next.js) | Vercel · `frontend/` | **Sí** |
| Dataset de ejemplo | `frontend/app/lib/sample-sales.json` | **Sí** (fuente por defecto) |
| Backend (.NET 10) | local (docker) u opcionalmente Render · `backend/` | No (opcional) |
| Mock (nginx) | local (docker) u opcionalmente Render · `backend/mocks/sap/` | No (opcional) |
### Desplegar / actualizar en Cloud Run
Ver [`DEPLOY.md`](./DEPLOY.md) (pasos `gcloud run deploy` o `scripts/deploy-cloudrun.sh`). Tras
desplegar, poner `BACKEND_URL` en Vercel con la URL del backend y redeploy.
122 changes: 63 additions & 59 deletions DEPLOY.md
Original file line number Diff line number Diff line change
@@ -1,92 +1,96 @@
# Deploy

End-to-end deployment of the live demo: **backend + mock on [Render](https://render.com)** (Free tier,
no credit card required), **frontend on [Vercel](https://vercel.com)**. Since the frontend (`page.tsx`)
fetches the backend server-side, the demo flow is `Vercel (Next.js Server Component) → Render (backend)
→ Render (mock)` and **no browser CORS is involved**.
End-to-end deployment of the live demo: **backend + mock on [Google Cloud Run](https://cloud.google.com/run)**
(free tier, no cold-start of the "no data" kind), **frontend on [Vercel](https://vercel.com)**. The
frontend (`page.tsx`) fetches the backend server-side, so the flow is
`Vercel (Next.js Server Component) → Cloud Run (backend) → Cloud Run (mock)` and **no browser CORS is
involved**.

> Why Cloud Run instead of Render: Render's free Web Services sleep after ~15 min and return 502 while
> waking, which left the demo blank. Cloud Run scales to zero too, but cold starts are ~1-2 s and the
> request **waits** for the container instead of failing — so the demo just works.

## Prerequisites

- A Render account (sign in with GitHub, no card needed for the Free plan).
- A Vercel account (same).
- (Optional, only for real SAP data) A free API key from the
- The `gcloud` CLI installed and authenticated (`gcloud auth login`).
- A Google Cloud project with billing enabled (the Cloud Run free tier covers this demo).
- A Vercel account.
- (Optional, only for real SAP data) a free API key from the
[SAP Business Accelerator Hub](https://api.sap.com).

## 1. Deploy backend + mock with one Render Blueprint

The repo ships a [`render.yaml`](./render.yaml) Blueprint that declares both Web Services.

1. Go to <https://dashboard.render.com> → **New +** → **Blueprint**.
2. Connect your GitHub account and pick the `aitorevi/connect-analyzer` repository.
3. Render reads `render.yaml`, shows the two services it is about to create
(`connect-analyzer-mock` and `connect-analyzer-api`, Free plan, Frankfurt) → **Apply**.
4. The first build takes ~5 min per service. Watch the logs from each service page.
## 1. Deploy backend + mock to Cloud Run

Render assigns:
- Mock: `https://connect-analyzer-mock.onrender.com` (or a suffix if that name is taken).
- API: `https://connect-analyzer-api.onrender.com`
Both pieces ship a Dockerfile and listen on `8080`. `gcloud run deploy --source <dir>` builds the
image with Cloud Build and deploys it. Run the steps below (or use
[`scripts/deploy-cloudrun.sh`](./scripts/deploy-cloudrun.sh), which does the two deploys and wires
`SapMock__BaseUrl` for you).

> If Render had to suffix the mock name, edit `SapMock__BaseUrl` in the **connect-analyzer-api**
> service's environment to the actual mock hostname, then **Manual Deploy** → **Clear build cache & deploy**.
```bash
# Once: select the project and enable the APIs
gcloud config set project <YOUR_PROJECT_ID>
gcloud services enable run.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com

# 1a. Mock (nginx serving the pipe-delimited Latin-1 .txt)
gcloud run deploy connect-analyzer-mock \
--source backend/mocks/sap \
--region europe-southwest1 \
--port 8080 --allow-unauthenticated

# Grab its URL
MOCK_URL=$(gcloud run services describe connect-analyzer-mock \
--region europe-southwest1 --format='value(status.url)')

# 1b. Backend (.NET 10), pointed at the mock, SQLite in /tmp (ephemeral, re-seeded on start)
gcloud run deploy connect-analyzer-api \
--source backend \
--region europe-southwest1 \
--port 8080 --allow-unauthenticated \
--set-env-vars "SalesSource=Mock,SapMock__BaseUrl=${MOCK_URL},Sqlite__Path=/tmp/sales.db"
```

### Smoke-test
Smoke-test (URLs are printed by the deploys / `gcloud run services describe`):

```bash
curl -s https://connect-analyzer-mock.onrender.com/sales.txt | head -3
curl -s https://connect-analyzer-api.onrender.com/api/sales | head
curl -X POST https://connect-analyzer-api.onrender.com/api/sales/refresh # forces re-ingestion
curl -s <mock-url>/sales.txt | head -3
curl -s <api-url>/api/sales | head
curl -s <api-url>/api/sales/by-product
```

The first call may take 30-50 s if the service was sleeping (Free Web Services nap after ~15 min
without traffic).

### Optional: switch to real SAP data
### Optional: real SAP / Shopify instead of the mock

By default the backend uses the mock (`SalesSource=Mock`). To pull from the real SAP S/4HANA OData
sandbox instead, in the **connect-analyzer-api** service:
Set `SalesSource` and the matching secrets on the `connect-analyzer-api` service (then it redeploys):

1. **Environment** tab → set `Sap__ApiKey` (Secret) to your Business Accelerator Hub key.
2. Change `SalesSource` to `Sap`.
3. **Manual Deploy** → **Deploy latest commit**.
```bash
# SAP
gcloud run services update connect-analyzer-api --region europe-southwest1 \
--set-env-vars SalesSource=Sap --set-env-vars Sap__ApiKey=<your-key>
# Shopify
gcloud run services update connect-analyzer-api --region europe-southwest1 \
--set-env-vars SalesSource=Shopify \
--set-env-vars Shopify__StoreUrl=<url>,Shopify__ClientId=<id>,Shopify__ClientSecret=<secret>
```

> The `Sap__ApiKey` variable is declared in `render.yaml` with `sync: false`, so Render does NOT
> populate it from the Blueprint — you must set it manually in the dashboard. Never commit the key.
(For real secrets, prefer Secret Manager + `--set-secrets` over `--set-env-vars`.)

## 2. Deploy the frontend (Vercel)

In the Vercel dashboard: **Add New… → Project → Import the GitHub repo `aitorevi/connect-analyzer`**.

- **Root Directory**: `frontend` (the Next.js app lives in this subfolder).
- **Root Directory**: `frontend`.
- **Framework Preset**: Next.js (auto-detected).
- **Environment Variables** — `BACKEND_URL` is **optional**:
- **Leave it unset** (recommended) → the dashboard serves its bundled sample data
(`frontend/app/lib/sample-sales.json`): instant, always-on, no backend needed. See [`DEMO.md`](./DEMO.md).
- Or set `BACKEND_URL` = your backend URL (e.g. `https://connect-analyzer-api.onrender.com`) to
use live data; the frontend falls back to the bundled data if it's unreachable.

Click **Deploy**. Vercel builds with `next build` and serves the dashboard.
- **Environment Variables**: `BACKEND_URL` = the `connect-analyzer-api` Cloud Run URL (from step 1b).

> The backend (steps 1) is **not required** for the demo — the frontend is self-sufficient. Deploy
> it only if you want live data (incl. real SAP/Shopify) behind the dashboard.
Click **Deploy**. The frontend fetches the backend server-side and renders the dashboard; the
client derives KPIs/series and applies the filters.

## 3. Optional: lock CORS to your Vercel origin

`page.tsx` fetches the backend **server-side**, so the browser never calls the backend directly — CORS
is not exercised today. If you later add a client-side fetch, restrict the backend's CORS origin in the
**connect-analyzer-api** service environment:
`page.tsx` fetches the backend **server-side**, so the browser never calls the backend directly —
CORS is not exercised today. If you later add a client-side fetch, restrict the backend's CORS origin
on the `connect-analyzer-api` service:

```
Cors__AllowedOrigins__0 = https://<your-frontend>.vercel.app
```

Never widen to `AllowAnyOrigin`.

## Cold-start note

The free Render Web Services sleep after ~15 min of inactivity. The first request after that
cold-starts in 30-50 s. Two ways to mitigate if it matters for the demo:

- Visit the URL yourself a few seconds before showing it to someone (the dashboard `page.tsx` does
two fetches that warm both backend and mock in one go).
- Set up a tiny cron pinger (UptimeRobot, cron-job.org, etc.) hitting `/api/sales` every ~10 min.
Still on the free plan; just gentle keep-alive traffic.
4 changes: 2 additions & 2 deletions DEUDA-TECNICA.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ cachea el access token durante la vida del proceso y no reacciona a 401 del endp

### 7. SQLite del backend vive en `/tmp` _(prioridad media)_

`docker-compose.yml` y `render.yaml` apuntan `Sqlite__Path` a `/tmp/sales.db` porque la
`docker-compose.yml` y el despliegue en Cloud Run apuntan `Sqlite__Path` a `/tmp/sales.db` porque la
imagen `dotnet/aspnet:10.0` corre como `$APP_UID` (no-root) desde el commit
`7d621bf` y `/app` (el `WORKDIR`) pertenece a root, así que el adaptador no puede
crear el fichero ahí. `/tmp` es escribible por cualquier usuario pero **se borra al
Expand All @@ -97,7 +97,7 @@ reiniciar el contenedor**, lo que tira la persistencia ingerida.
- Volumen Docker dedicado (`volumes: - sales-db:/var/lib/connect-analyzer`) y
`Sqlite__Path=/var/lib/connect-analyzer/sales.db`, ajustando ownership en el
Dockerfile (`mkdir … && chown $APP_UID`).
- En Render, cambiar a un disco persistente del servicio en vez de `/tmp`.
- En Cloud Run, montar un volumen (p.ej. un bucket GCS o Cloud SQL) en vez de `/tmp`.
- **Detectado:** durante la verificación end-to-end del MVP de Shopify (2026-05-31).

### 8. Manejo global de excepciones
Expand Down
Loading
Loading