# Fermenty — Dominios y navegación

> **Documento reconstruido, no recuperado.** El original se cita como autoridad en
> tres ficheros de código (`routes/panel.php:4`, `routes/publico.php:5`,
> `app/Support/Superficie.php:17`) y no está en el repositorio. Lo que sigue se ha
> reconstruido **leyendo el código**, con la referencia a mano en cada regla.
>
> Consecuencia de cómo se ha hecho: esto describe **lo que el sistema hace hoy**. Donde
> el código y una decisión previa no coinciden, va marcado 🔴 y no se resuelve aquí.
>
> Referencias: `docs/Encaminamiento.md` (§2), `docs/Lexico.md` (§7 La persona).
>
> Destino: `docs/Dominios_Navegacion.md` · Documento vivo · v1 (reconstruido) · 2026-08-03

---

## 1. Las tres superficies

| Nombre | Host / ruta | Quién | Qué es |
|---|---|---|---|
| **PWA** | `app.fermenty.es/` | Usuario | Una tanda, ahora, en la cocina |
| **Panel** | `fermenty.es/panel/` | Usuario | Escritorio. Muchas tandas, planificando |
| **ERP** | `erp.` | Interno | Administración del catálogo. Reservado además para el escalón 3 (Obrador) |
| **API** | `api.fermenty.es/` | Máquinas | Lo que consume la PWA |

**PWA y Panel comparten cuenta, no sesión.** Misma tabla `users`; cookie host-only
(`config/session.php:159`, `SESSION_DOMAIN=null`), así que la sesión de `app.` y la de
la raíz son distintas. Lo declara `routes/panel.php:8` y lo cumple.

**El ERP se retira de rutas** pero quedan restos: `.env:6 ERP_APP_URL` y
`app/Console/Commands/RepararEnlacesBlog.php:36-37`.

### 1 bis · El reparto es por ruta, nunca por dispositivo

`routes/panel.php:6-8`. Ningún `redirect` por User-Agent en todo el código. La
detección **sugiere**, no manda.

### 1 ter · Despacho por host

`bootstrap/app.php:19-32`:

| Host | Fichero | Middleware |
|---|---|---|
| `api.fermenty.*` | `routes/api.php` | `['api','cors']` — stateless |
| `app.fermenty.*` | `routes/mobile.php` | `'web'` |
| resto | `routes/web.php` | `'web'` |

`routes/mobile.php` es carcasa: `Route::view` y closures, sin Eloquent y **sin `auth`
en ninguna ruta** — es guest-first por diseño, y el cliente decide qué exige cuenta
según tenga token (`resources/views/layouts/mobile.blade.php:91-94`).

---

## 2. Autenticación

| | Panel | PWA |
|---|---|---|
| Mecanismo | Sesión (cookie) | Token Sanctum en `localStorage` (`fm_token`) |
| Middleware | `'web'` + `['auth']` (`routes/panel.php:34`) | solo `'web'` |
| Datos | Eloquent directo | `fetch` a `api.` con `auth:sanctum` |

Un solo guard: `config/auth.php:19,40-42`; Sanctum apunta al mismo
(`config/sanctum.php:40`).

**La puerta de login vive en la raíz, fuera de `/panel`** (`routes/web.php:24` →
`routes/auth.php`). Todos los `intended()` post-login van al Panel. La PWA tiene su
propia pantalla contra `POST /v1/login`.

**El acceso al Panel no exige correo verificado**: el grupo lleva `['auth']` sin
`verified` (`routes/panel.php:34`).

**El único puente sesión ↔ token es Google**: `GoogleController.php:21-38` guarda
`?superficie=app` en sesión y `:126` devuelve con `#code=…`, canjeable en
`POST /v1/google-exchange`.

> **Regla que se deriva de esto y que aplica a cualquier pantalla nueva:** una pantalla
> del Panel **no puede consumir `/v1` por `fetch`** — su sesión no vale allí. Va por
> rutas web contra el servicio.

---

## 3. Qué disposición ve cada usuario

Un solo sitio decide: `app/Support/Superficie.php:33-41`.

1. **Preferencia explícita** — cookie `fm_superficie` (`movil` | `escritorio`), sin
   cifrar para que la escriba JS (`bootstrap/app.php:43`). **Manda siempre.**
2. **Heurística de dispositivo** — User-Agent (`DeviceDetector.php:47-51`), refinada
   por la cookie `fm_device` con el viewport real.

**Nunca hay redirección automática.** Solo se ofrece:

- Banner en el Panel, `d-lg-none` (`partials/superficie-banner.blade.php:11-19`)
- Conmutador permanente en el pie público (`partials/landing-footer.blade.php:79-82`)
- Destino profundo por vista: `batches/show.blade.php:6`

Los subdominios son **el resultado** de la elección, no el mecanismo.

---

## 4. URLs y redirecciones

**Los nombres de ruta no llevan prefijo** (`dashboard`, `batches.*`), aunque las URLs
sí (`routes/panel.php:10-13,46,74-80`). Así el reparto puede moverse sin tocar vistas.

**Las URLs viejas tienen su 301**, generado en bucle para los nueve prefijos antiguos
(`routes/web.php:41-54`).

**Helper canónico para cruzar de superficie:** `config('app.web_url')`
(`config/app.php:67`), que elimina los prefijos `api|app|erp`. Se usa bien en
`Superficie.php:89-92`.

### 🔴 Cuatro divergencias vivas

1. **`config('app.url')` no sirve para construir enlaces de usuario.**
   `routes/mobile.php:25` manda a `APP_URL` = `api.fermenty.local`, host que se
   despacha a `routes/api.php`, donde esa ruta no existe. **El enlace a suscripción
   desde la PWA está roto.** Debe usar `app.web_url`.
2. **Cuatro vistas leen `config('app.mobile_url')` directamente** en vez de pasar por
   `Superficie`, contra lo que declara `Superficie.php:22-24`:
   `layouts/app.blade.php:291`, `layouts/web.blade.php:13`,
   `fermentos/show.blade.php:10`, `admin/layouts/admin.blade.php:24`.
3. **`RouteServiceProvider.php:15-28` carga `routes/erp.php`, que no existe.** Hoy es
   inerte porque el provider no está registrado (`bootstrap/providers.php:5-7`); el día
   que alguien lo registre, la aplicación rompe. Borrar.
4. **La canónica de tanda.** `Encaminamiento.md` §2 fija `/tandas/{id}` sin prefijo;
   el código sirve `/panel/batches/{batch}`. Uno de los dos tiene que ceder, y afecta
   a los enlaces de los correos. **No se decide aquí.**

---

## 5. Lo que este documento no contiene

El original se citaba también por §4 (redirecciones) y §6 (ámbito de inquilino). Lo
primero está arriba; **lo segundo no se ha podido reconstruir**: no hay evidencia en
código de una columna de inquilino, y `Encaminamiento.md` §2 lo tenía como casilla sin
marcar. Queda pendiente y no se inventa.

Tampoco se ha verificado el estado de dos casillas de `Encaminamiento.md` §2:
**subdominios reservados** y **`noindex` de servidor** en `app.`, `api.` y `erp.`

---

*Documento vivo. Al ser una reconstrucción, cualquier regla de aquí que contradiga una
decisión anterior gana el código pero pierde la discusión: se marca 🔴 y se resuelve
fuera.*

## Sesión compartida PWA ↔ panel (2026-08-29)

**Una sola entrada.** Quien entra en la PWA (app.*) queda también dentro del panel, y al
revés. La PWA sigue usando su token Sanctum en `localStorage`; el panel, su cookie de
sesión. El puente es la **cookie de dominio**: `SESSION_DOMAIN` en el dominio raíz
(`fermenty.local` en local, **`.fermenty.es` en producción** — cambiarlo en el `.env` del
servidor; la cookie pinada a `erp.fermenty.es` del incidente de junio deja de hacer
falta y, de paso, los enlaces al dominio principal ya llevan la sesión).

Tres rutas en `api.*` con el stack de sesión explícito (`SessionController::STACK`; el
grupo `api` es stateless):

| Ruta | Quién la llama | Qué hace |
|---|---|---|
| `POST /v1/session` (Bearer) | `fmLogin()` de la PWA, al entrar | token → abre la sesión web (cookie `Domain=raíz`, remember) |
| `GET /v1/session/token` (cookie) | `fmSsoProbe()` de la PWA, al arrancar sin token | sesión web → token Sanctum `app-sso` + `user` (login_logs `sso`) |
| `DELETE /v1/session` (cookie) | `fmLogout()` de la PWA | cierra la sesión web: salir de la app es salir del panel |

- La PWA hace estas tres llamadas con `credentials: 'include'` (`fmApi.req(…, {cookies:true})`);
  el resto del API sigue sin cookies. `CorsHeaders` devuelve el origen exacto +
  `Access-Control-Allow-Credentials` SOLO a los orígenes de `config/cors.php`; a los demás (y sin Origin),
  ningún `Access-Control-Allow-Origin` (el `*` de antes hacía que el HandleCors global, con
  `supports_credentials`, colgara `Allow-Credentials` encima).
- La sonda es una vez por pestaña (`sessionStorage.fm_sso`), con tope de 1,5 s; Alpine se
  inyecta después (`layouts/mobile.blade.php`), así las pantallas no piden login antes de
  saber si hay sesión. Si la respuesta llega tarde y había alguien, se recarga con el token.
- El panel no tiene que hacer nada: su cookie ya vale en api.*. Salir del panel NO revoca
  el token de la PWA (queda como un segundo dispositivo). Cambiar de cuenta en la PWA
  reemplaza la sesión web (no la hereda).
- Tests: `tests/Feature/SesionCompartidaTest.php`.
