En esta página 00-day-brief 0%

00-day-brief.md

00 — Day brief · 2026-08-18 · ZAGUÁN

Decisiones ALS-2 BRIEF

CampoValor
Fecha2026-08-18
NombreZAGUÁN
ComplejidadNivel 1 (no L2 consecutivo tras MARGEN)
SectorInmobiliaria de barrio (Usera / Vallecas / Entrevías / Orcasur)
TipoWizard de criterios + comparador de 3 fincas + ficha + mesa AGENTE JWT
PlataformaWeb responsive (wizard/comparador desktop · pasos mobile · mesa agente)
RegistroR-IN — configurador usable, no web de agencia clásica
ShellS-WIZ — chrome de pasos, sin top-nav de marketing
HomeH-WIZARD — la entrada es el paso 1, no un hero
FlujoF-CMP — el núcleo es comparar 3 columnas, no una cola
Por qué no MARGENNo S-TOP / H-GALLERY / F-STORY; no editorial; no lookbook
Por qué no CESTANo S-RAIL / H-TABLE / F-LEAD; no ONG; no «otra cola»
Por qué no CANTERAS-WIZ ≠ S-BOTTOM; F-CMP ≠ F-ONB; inmobiliaria ≠ parte de obra

Terna unicidad

R-IN · S-WIZ · H-WIZARD · F-CMP

0 códigos iguales a N−1 (MARGEN). ≥2 distintos vs N−2 (CESTA) y N−3 (RONDA). Ventana 4 días tenía 2× R-CL (CESTA, MARGEN) → hoy R-IN.

Ground (CRON §5.6)

  • Familia: G-CONCRETE
  • Hex: #E3E1DC
  • Por qué: hormigón fresco / reja. No es G-GALLERY #FFFFFF (MARGEN), G-MOSS (CESTA), G-CHROME (RONDA), G-WATER (DERIVA) ni G-MUNICIPAL (PIZARRA). No es beige por inercia: el suelo es gris mineral frío.

Mood visual (Paper)

  • Candidatos: mineral (primer instinto), rusted, industrial, chapel, nocturnal
  • Elegido: rusted — no el primer instinto mineral (agencia caliza previsible)
  • Paleta: #E3E1DC hormigón · #1F1D1B grafito · #B54A2A óxido · #6A6560 hierro · #F3F1EC caliza (cards) · #C9C4BB junta
  • Tipo: Instrument Serif display · Sora UI (evita Cormorant / Public Sans / Spectral / Manrope / Zilla / Mulish / Newsreader / Outfit / Lora / Karla)

Alcance L1 must

  1. Wizard 3 pasos: zona+tipo → presupuesto+habitaciones → musts (ascensor, terraza, mascotas, reforma)
  2. Comparador 3 columnas con datos reales (precio, m², planta, gastos, extras)
  3. Ficha de finca
  4. Login + mesa AGENTE JWT (DRAFT → PUBLISHED)
  5. Alta de finca + success
  6. Empty (0 matches) / loading / error / mobile wizard
  7. ≥12 UX + ≥10 UI

Microcopy duro

  • Comparar ≠ reservar. El comparador no pide ni confirma una visita.
  • Criterio ≠ oferta. El presupuesto del wizard no es una oferta vinculante.
  • Publicado ≠ visitado. La finca en mesa no implica que alguien haya entrado.
  • ZAGUÁN no llama. La agencia llama; el producto no agenda slot (≠ F-BOOK / ≠ F-LEAD).

Supuestos

  • S1: Una agencia de barrio lleva 8–20 fincas activas; la demo tiene 5 publicadas + 1 DRAFT.
  • S2: Quien busca piso compara 2–3 anuncios en pestañas; no rellena un lead para «que le llamen».
  • S3: La mesa es 1 agente; no un CRM de captación.
  • S4: No hay pasarela, señal, ni contrato. El producto es criterio + comparación + catálogo.

Hipótesis

IDSeñal
H1Un wizard de 3 pasos + 3 columnas reduce «abrir 6 tabs de Idealista»
H2Separar comparar de reservar evita la expectativa de cita confirmada
H3JWT solo en agente basta; quien busca no necesita cuenta

00-paper-reference.md

Paper reference · ZAGUÁN

URLhttps://app.paper.design/file/01M09V13JEANPFFD6DS1RCN29G
File ID01M09V13JEANPFFD6DS1RCN29G

UX-count: 12 UI-count: 12

UX-00 Cover · UX-01 Stakeholders · UX-02 Personas · UX-03 JTBD · UX-04 Stories · UX-05 Journey · UX-06 Blueprint · UX-07 Site map · UX-08 Flujos · UX-09 Datos+permisos · UX-10 Métricas · UX-11 Research UI-00 Tokens · UI-01 Wizard 1 zona · UI-02 Wizard 2 presupuesto · UI-03 Wizard 3 musts · UI-04 Comparador 3 cols · UI-05 Ficha · UI-06 Login · UI-07 Mesa · UI-08 Alta · UI-09 Success · UI-10 Empty · UI-11 Error

UX process (prefijo UX- · mínimo 12)

IDNombreContenido
UX-00CoverPortada ZAGUÁN · eslogan «Tres fincas. Un criterio.» · terna R-IN · S-WIZ · H-WIZARD · F-CMP · L1 inmobiliaria de barrio · mood rusted · fecha 2026-08-18
UX-01StakeholdersBUSCADOR anónimo (Marcos), AGENTE (Lucía), PROPIETARIA (Inés), pareja, vecinos de Usera / Vallecas / Entrevías / Orcasur — no lead, no visita confirmada
UX-02PersonasMarcos Vidal (31, pareja, 2 hab Usera, tope 950 €) · Lucía Ferrer (46, agente, local Marcelo Usera) · Inés Roldán (58, propietaria del 3º de Amparo)
UX-03JTBDJob «ver 3 fincas a la vez con mi criterio» + job «publicar el piso sin CRM» + job «que se compare el piso, no que se reserve online»
UX-04StoriesMust: wizard 3 pasos, comparador 3 cols, ficha, login AGENTE, mesa DRAFT→PUBLISHED, alta + success, empty/error, mobile wizard
UX-05JourneyZona → presupuesto → musts → comparar 3 → abrir ficha → (AGENTE) login → mesa → nueva finca → publicar
UX-06BlueprintFrontstage wizard/comparador/ficha · backstage llamada de Lucía e Inés con las llaves · sistemas Nest/Neon/JWT
UX-07Site map / IAPúblico wizard-first + auth AGENTE; S-WIZ pasos, sin top-nav de marketing
UX-08FlujosF-CMP: criterio → 3 columnas; F-DESK: login → lista → alta → PATCH PUBLISHED
UX-09Datos + permisosUser AGENTE · Listing · Comparison (opcional); JWT en GET mesa / POST / PATCH; público no ve DRAFT
UX-10MétricasNorth star % sesiones que llegan a 3 columnas y abren ≥1 ficha; comparar no cuenta como visita
UX-11Research notesComprobado / supuesto / hipótesis / decisión — sin entrevistas de campo inventadas

UI producto (prefijo UI- · mínimo 10)

IDNombreFlujo
UI-00TokensDS rusted / hormigón: hormigón, grafito, óxido, hierro, caliza, junta; Instrument Serif + Sora
UI-01Wizard 1 zonaH-WIZARD paso 1: zona + tipo (Usera / Vallecas / Entrevías / Orcasur · ALQUILER / VENTA)
UI-02Wizard 2 presupuestoPaso 2: tope de renta + habitaciones. Criterio ≠ oferta
UI-03Wizard 3 mustsPaso 3: ascensor, terraza, mascotas, reforma
UI-04Comparador 3 colsF-CMP: precio, m², planta, gastos, extras. Comparar ≠ reservar
UI-05FichaFinca: dirección, datos, foto, disclaimer «ZAGUÁN no llama»
UI-06LoginAcceso JWT AGENTE a la mesa
UI-07MesaLista de fincas DRAFT / PUBLISHED + CTA nueva
UI-08AltaForm de finca (dirección, zona, renta, musts, cover)
UI-09SuccessAcuse de publicación + aviso «publicado ≠ visitado»
UI-10Empty0 matches al criterio / mesa vacía
UI-11ErrorFallo de red, reintentar

Regla: wizard de N pasos → N boards UI solo para esos pasos, además de success/empty/error/loading/staff. ZAGUÁN es wizard: el núcleo es zona → presupuesto → musts → 3 columnas y mesa → alta → publicar.

Loading (skeletons de 3 columnas / hold de wizard) y mobile wizard (~390px, variante de UI-01…03) se especifican en docs 16 y 17; no ocupan un board extra del inventario de 12. [DECISIÓN]

Paper se construye en paralelo a esta suite. Los nombres de la tabla son el inventario planificado; el file ID ya existe.


1. Mapa de canvas (bandas)

§BandaPropósitoArtboards clave
1UX PROCESSModelo de servicio de inmobiliaria de barrioUX-00…UX-11
2DESIGN SYSTEMTokens rusted + chrome de pasos + card calizaUI-00
3PUBLIC WIZARDCara buscador: 3 pasos + comparador + fichaUI-01, UI-02, UI-03, UI-04, UI-05, UI-10
4AGENTEFlujos autenticadosUI-06, UI-07, UI-08, UI-09
5STATESResilienciaUI-10, UI-11

Layout canvas (referencia de diseño): origen (0,0) · gaps ~100–120px · UX en 3×4 · UI pública en fila W1 → W2 → W3 → comparar → ficha · mesa en fila login → lista → alta → success · mobile wizard debajo de UI-01. [DECISIÓN] Bandas jerárquicas D-P0-04 / D-P1-04.


2. Mapeo Paper → Angular

ArtboardRuta appComponente
UI-00 Tokenstokens Tailwind + fuentes
UI-01 Wizard 1 zona/ZonaPage (paso 1)
UI-02 Wizard 2 presupuesto/criterio/presupuestoPresupuestoPage
UI-03 Wizard 3 musts/criterio/mustsMustsPage
UI-04 Comparador/compararCompararPage
UI-05 Ficha/fincas/:slugFincaPage
UI-06 Login/loginLoginPage
UI-07 Mesa/mesaDeskPage
UI-08 Alta/mesa/nuevaNewListingPage
UI-09 Success/mesa/ok/:slugOkPage
UI-10 Empty/comparar 0 matches · /mesa 0 filasempty accionable
UI-11 Errorwizard / comparar / mesabanner error + retry

[COMPROBADO] Rutas del encargo. No hay /visita, /reservar, /lead, search-first ni hero 2-col.

Loading y mobile no tienen artboard propio en el inventario de 12: el loading es skeleton sobre UI-04 / UI-01; el mobile es la misma ruta / a 390px. Ver docs 16 y 17.


3. Tokens de diseño en Paper

TokenValorUso
Hormigón / ground#E3E1DCFondo de página, G-CONCRETE
Grafito#1F1D1BTexto fuerte, wordmark, cuerpo
Óxido#B54A2AAcento, progreso del wizard, PUBLISHED, CTA
Hierro#6A6560Meta, labels, nav idle, planta, gastos
Caliza#F3F1ECCards de finca / columnas (no el ground)
Junta#C9C4BBBordes, rules, skeletons
DisplayInstrument Serif 56/40/28Titulares, wordmark, dirección de finca
UISora 16/14Body UI, labels, form, pasos, comparador

[COMPROBADO] Hex y tipo salen de docs/00-day-brief.md. Mood elegido: rusted, no el primer instinto mineral (agencia caliza previsible).


4. Checklist de densidad (anti thin-frames)

CriterioUXDSPublic wizardAGENTEStates
Jerarquía tipográfica visible
Microcopy real (no lorem)
Tokens rusted aplicados
Datos de seed creíblesPersonas5 fincas + fotos1 DRAFT en mesaEmpty realista
Media / iconografíaCoverFotos de fincaBadges DRAFT/PUBLISHEDSkeletons
Chrome S-WIZ presenteSpecUI-01, UI-02, UI-03
Comparador 3 cols (no tabla, no feed)SpecUI-04
Pasos del wizard visiblesSpecUI-01…03

5. Media

  • assets/hero.jpg — atmósfera de zaguán / portal de finca de barrio, reja y hormigón
  • assets/listings/amparo.jpg — Calle Amparo 14, 3º D, Usera (patio, sin ascensor)
  • assets/listings/albufera.jpg — Av. de la Albufera 212, 5º, Vallecas (terraza, ascensor)
  • assets/listings/yeserias.jpg — Paseo de Yeserías 8, bajo, Usera (mascotas OK)
  • assets/listings/sicilia.jpg — Calle Sicilia 9, 2º, Entrevías (reforma, ascensor)
  • assets/listings/alegria.jpg — Calle de la Alegría 3, ático, Orcasur (terraza, sin ascensor)

[COMPROBADO] Los seis archivos existen en el case 2026-08-18-zaguan/assets/. El DRAFT merced-2 no tiene foto pública.


6. Enlaces

01-project-definition.md

01 — Definición de proyecto — ZAGUÁN

1. Identidad

CampoValor
NombreZAGUÁN
SignificadoEl zaguán es el umbral del edificio de barrio: reja, hormigón, buzones. Ni el portal de anuncios ni el hall de una agencia de centro. Se entra con un criterio; se salen tres fincas.
Eslogan”Tres fincas. Un criterio.”
Una fraseWizard de criterios + comparador de 3 fincas + ficha + mesa AGENTE JWT.
SectorInmobiliaria de barrio (Usera / Vallecas / Entrevías / Orcasur)
TipoWeb L1 — wizard + comparador 3 cols + ficha + mesa AGENTE — Nivel 1, configurador usable
PlataformaWeb responsive (wizard/comparador desktop · pasos mobile · mesa agente)
Mercado demoEspaña · Madrid · Usera / Vallecas / Entrevías / Orcasur (una sola agencia, no red multi-distrito)
Idiomaes-ES
Fecha caso2026-08-18
TernaR-IN · S-WIZ · H-WIZARD · F-CMP

[COMPROBADO] Nombre, terna, nivel, sector, ground y paleta salen de docs/00-day-brief.md.

2. Problema

Principal (hipótesis de diseño)

[HIPÓTESIS] Quien busca piso en el sur de Madrid no rellena un lead para «que le llamen»: abre seis pestañas de Idealista y compara de memoria. El presupuesto se lee como oferta, el anuncio publicado se lee como «ya lo he visitado» y el botón «pedir visita» se lee como cita confirmada. [SUPUESTO] Una agencia de barrio lleva 8–20 fincas activas; no opera un CRM de captación ni una pasarela de señal.

Secundarios

ProblemaQuién lo sufreEfecto
Seis pestañas, cero criterio escritoBUSCADOR (Marcos)Elige por foto, se pasa de tope, vuelve a empezar
El tope de renta se lee como ofertaAmbos«Has ofertado 950» cuando solo filtró
Publicado se lee como visitadoAGENTE (Lucía) + propietariaInés pregunta «¿ya ha venido alguien?»
Comparar se lee como reservarPropietaria (Inés) + MarcosEspera una llamada que el producto no hace
Mesa de fincas = WhatsApp + ExcelAGENTEEl DRAFT se cuela en el escaparate

Supuestos (no investigación primaria propia)

  • S1: Una agencia de barrio lleva 8–20 fincas activas; la demo tiene 5 publicadas + 1 DRAFT. [SUPUESTO]
  • S2: Quien busca piso compara 2–3 anuncios en pestañas; no rellena un lead para «que le llamen». [SUPUESTO]
  • S3: La mesa es 1 agente; no un CRM de captación. [SUPUESTO]
  • S4: No hay pasarela, señal, ni contrato. El producto es criterio + comparación + catálogo. [SUPUESTO]

Hipótesis de producto

IDHipótesisSeñal de validación (futura)
H1Un wizard de 3 pasos + 3 columnas reduce «abrir 6 tabs de Idealista»% sesiones que llegan a /comparar con 3 cols
H2Separar comparar de reservar evita la expectativa de cita confirmada↓ «pensé que ya tenía visita»
H3JWT solo en agente basta; quien busca no necesita cuenta0 cuentas de buscador; comparación completa sin login

No se afirman estadísticas de portales inmobiliarios inventadas. Todo lo anterior es razonamiento de diseño etiquetado.

3. Propuesta de valor

ParaValor
BUSCADOR (Marcos Vidal)Deja el criterio en 3 pasos; ve 3 fincas a la vez; abre la ficha sin crear cuenta.
AGENTE (Lucía Ferrer)Lista las fincas, da de alta una nueva, publica DRAFT → PUBLISHED sin un CRM de 40 campos.
PROPIETARIA (Inés Roldán)El 3º de Amparo se compara en un sitio; no convierte ZAGUÁN en agenda de visitas.
Barrio / agenciaCanal mínimo: criterio visible + comparación + cierre de catálogo, sin lead ni señal.

No es ZAGUÁN

ExcluidoPor qué
Lead / «te llamamos» / cuenta buscadorS2 + H3: llega a comparar, no a dejar el móvil
Reserva / confirmación de visitaComparar ≠ reservar (≠ F-BOOK / ≠ F-LEAD)
Semanario / lookbook de crónicasEso fue MARGEN (H-GALLERY / F-STORY)
Tabla de necesidades / compromiso de cajaEso fue CESTA (H-TABLE / F-LEAD)
Parte de obra / wizard de oficioEso fue CANTERA (S-BOTTOM / F-ONB)
Feed / cronología de «pisos nuevos»Terna H-WIZARD, no H-FEED
Search-first / catálogo facetado como homeTerna no es H-SEARCH; la home es el paso 1
Pasarela, señal, contrato, hipotecaS4
Hero 2-col + 3 cards de «cómo buscamos»Anti-patrón AP-12; contradice H-WIZARD

4. Objetivos

Negocio / caso de estudio

  • Demostrar vertical slice L1 inmobiliario wizard + comparador con Listing + JWT AGENTE + Comparison opcional.
  • Portfolio coherente: Paper (12 UX + 12 UI) + docs + app runnable.
  • Terna R-IN · S-WIZ · H-WIZARD · F-CMP frente a MARGEN (galería/masthead), CESTA (tabla/rail) y CANTERA (dock de parte).

Usuario

RolObjetivo medible en demo
BUSCADORLlegar a 3 columnas en < 45 s; abrir una ficha en 1 tap más
AGENTEPublicar una finca DRAFT → PUBLISHED en < 2 min desde la mesa
PROPIETARIAVer su finca en el comparador sin encontrar un CTA de reserva

No objetivos v1 (explícitos)

  • Lead, teléfono del buscador, «te llamamos»
  • Reserva de visita / calendario / slot
  • Cuenta de buscador, favoritos, alertas
  • Pasarela, señal, contrato, scoring hipotecario
  • Multi-agente, comisiones, captación
  • Mapa de fincas, street view, tour 3D
  • Portal multi-agencia / scrap de Idealista
  • App nativa ni notificaciones push

5. Roles y permisos (resumen)

AcciónPúblico (BUSCADOR / PROPIETARIA)AGENTE
Completar wizard de criterio
Ver comparador (PUBLISHED)
Leer ficha PUBLISHED
Ver finca DRAFTNo (404) (mesa)
Pedir / confirmar visitaNo (no existe)No (fuera de app)
Login JWTNo (no cuenta buscador)
Listar fincas de mesaNo (401)
Crear fincaNo
Publicar / despublicarNo
Persistir ComparisonImplícito en POST compare (opcional)Lectura Could L+1

[DECISIÓN] Un solo rol autenticado: AGENTE. El buscador es anónimo. La propietaria no es un rol de sistema: es persona / stakeholder que usa la cara pública (o habla con Lucía fuera).

6. Métricas (modelo, no instrumentadas en v1 salvo base)

TipoMétricaDefinición
North Star% de sesiones que llegan a /comparar con 3 columnas y abren ≥1 fichaCompletud de criterio + comparación, no pageviews
Activación buscadorCompletar los 3 pasos del wizardH1
Activación agente1.er PATCH a PUBLISHEDCierre de catálogo
ExpectativaReclamaciones «ya tenía visita» / «habéis llamado» / «he ofertado»H2 + microcopy
IntegridadGET público no devuelve DRAFTPublicado ≠ visitado (y borrador invisible)
SaludError rate API listings/compare4xx/5xx

7. Alcance funcional v1 (L1)

MóduloIncluido
Wizard 3 pasosZona+tipo → presupuesto+habitaciones → musts (ascensor, terraza, mascotas, reforma)
Comparador3 columnas con precio, m², planta, gastos, extras
Ficha de fincaDirección, datos, foto, disclaimer
AuthPOST /api/auth/login → JWT AGENTE
MesaGET lista DRAFT + PUBLISHED, PATCH :id { status }
AltaForm + POST /api/listings + success
Estados UIEmpty (0 matches), loading skeleton, error de red
Mobile wizardMismos 3 pasos a ~390px
Seed1 AGENTE, 5 fincas PUBLISHED, 1 DRAFT

8. Criterios de aceptación de producto

  1. Un visitante puede completar el wizard sin autenticación y ver hasta 3 fincas comparadas.
  2. Un visitante puede abrir una ficha sin cuenta y sin encontrar un CTA de reserva o «te llamamos».
  3. La UI dice explícitamente que comparar no es reservar, que el criterio no es oferta, que publicado no es visitado y que ZAGUÁN no llama.
  4. Sin token, GET /api/mesa/listings y POST/PATCH de listings responden 401.
  5. GET /api/listings devuelve solo PUBLISHED; el DRAFT merced-2 no aparece.
  6. GET /api/listings/:slug de un DRAFT responde 404 (no 200 + aviso).
  7. Un AGENTE puede iniciar sesión y ver la mesa con DRAFT y PUBLISHED.
  8. El AGENTE puede crear una finca y publicarla (DRAFTPUBLISHED).
  9. La home es el paso 1 del wizard: no hay hero 2-col + 3 cards como superficie principal.
  10. Fotos reales en las 5 fincas publicadas (assets del case).
  11. El comparador muestra precio, m², planta, gastos, extras en 3 columnas.
  12. Rutas públicas y de mesa solo las del doc 03.

9. Stack y artefactos

CapaDetalle
FrontendAngular + Tailwind · puerto 4200
BackendNestJS · puerto 3018
DBNeon PostgreSQL · project curly-recipe-23023272
AuthJWT (AGENTE)
DiseñoPaper 01M09V13JEANPFFD6DS1RCN29G
Repo app/Users/cristian/orca/zaguan-app/ · GitHub Criscode2022/zaguan-app

[COMPROBADO] Puerto, Neon, Paper file ID, repo y path de app constan en el encargo.

10. Riesgos y mitigaciones

RiesgoImpactoMitigación v1
Expectativa de visita confirmadaReclamaciones «ya tenía cita»Copy «Comparar ≠ reservar»; sin botón Reservar
Expectativa de oferta vinculante«Has ofertado 950»Copy «Criterio ≠ oferta» en paso 2 y comparador
Publicado se lee como visitadoInés pregunta por visitasCopy «Publicado ≠ visitado» en success y mesa
El producto «llama»Expectativa F-BOOK«ZAGUÁN no llama. La agencia llama.»
DRAFT filtrado a compararFinca a medias públicaGET público filtra status=PUBLISHED; slug DRAFT → 404
Home percibida como landing de agenciaPérdida de craft / H-WIZARDPaso 1 es la home; no hero 2-col
Confundir con MARGEN (galería editorial)Expectativa de crónicaNo hay masthead de periódico ni agenda
Confundir con CANTERA (parte de obra)Expectativa de «enviar el día»F-CMP, no F-ONB; 3 columnas, no parte
Leftover pieces / issues / events en repoApp enseña otro productoContrato de esta suite gana; I01 de build
PII de buscadorNo aplica v1Buscador no deja datos; AGENTE es la única cuenta
0 matches al criterioAbandonoEmpty «ninguna finca encaja; afloja un must»

11. Glosario

TérminoDefinición en ZAGUÁN
CriterioConjunto zona + tipo + tope + hab + musts; no es una oferta
Comparador3 columnas de fincas; no es una reserva
Finca / listinglistings: anuncio de piso en alquiler (demo)
MustFiltro duro o blando: ascensor, terraza, mascotas, reforma
MesaSuperficie AGENTE: lista + alta + publicar
PublicarPasar listing a PUBLISHED; no significa que alguien haya visitado
BorradorDRAFT; el barrio no lo ve
BUSCADORVisitante anónimo que compara
AGENTEUsuario autenticado que opera la mesa
CalizaSuperficie de card #F3F1EC, no el ground
JuntaRule #C9C4BB entre columnas y campos
Chrome de pasosShell S-WIZ: wordmark + 1 / 2 / 3, sin top-nav de marketing
ZaguánUmbral: se entra con criterio, se salen tres fincas

12. Decisiones de diseño (cierre de brief)

IDDecisiónAlternativa descartada
D1Home = paso 1 del wizard (H-WIZARD)Hero 2-col + 3 cards (AP-12) · galería (MARGEN) · tabla (CESTA) · search (PIZARRA)
D2Shell chrome de pasos (S-WIZ)Masthead de periódico · rail de iconos · dock flotante · sidebar CRM
D3Registro industrial rusted (R-IN)Mineral / agencia caliza (primer instinto) · gallery · botanical
D4Flujo F-CMP (criterio → 3 columnas)F-STORY, F-LEAD, F-ONB, F-BOOK, F-SEARCH
D5Mood rustedMineral (vetado como primer instinto), gallery MARGEN, moss CESTA
D6Un rol autenticado AGENTEMulti-rol buscador / propietaria / admin / captador
D7Estados solo DRAFT / PUBLISHEDRESERVED / VISITED / OPTIONED (CRM inmobiliario)
D8Comparador de solo lecturaForm de visita, calendario, «te llamamos»
D9Buscador sin cuentaLead, alerta, favoritos obligatorios
D10PATCH /api/listings/:id { status }Subruta /publish como único contrato
D11Ground G-CONCRETE #E3E1DCG-GALLERY, G-MOSS, G-CHROME, G-WATER, G-MUNICIPAL
D12Success de publicación en /mesa/ok/:slugToast único sin pantalla
D13tipo = ALQUILER | VENTATipo de inmueble (ático/bajo) vive en extras / planta
D14Comparison persistida de forma opcionalObligar cuenta para comparar

13. Relación con el día anterior

CaseTernaPor qué ZAGUÁN no lo copia
MARGEN (N−1)S-TOP / H-GALLERY / F-STORYNo editorial, no lookbook, no masthead, no agenda
CESTA (N−2)S-RAIL / H-TABLE / F-LEADNo ONG, no tabla de necesidades, no compromiso, no rail
RONDA (N−3)S-DOCK / H-FEED / F-BOOKNo gaming, no cronología, no dock, no sillas
CANTERAS-BOTTOM / F-ONBS-WIZ ≠ S-BOTTOM; F-CMP ≠ F-ONB; inmobiliaria ≠ parte de obra

[COMPROBADO] El brief fija terna R-IN · S-WIZ · H-WIZARD · F-CMP. 0 códigos iguales a N−1 (MARGEN). ≥2 distintos vs N−2 (CESTA) y N−3 (RONDA). Ventana 4 días tenía 2× R-CL (CESTA, MARGEN) → hoy R-IN. Ground G-CONCRETE #E3E1DC, no G-GALLERY.

02-ux-research-strategy.md

02 — Estrategia de investigación UX — ZAGUÁN

Importante: este documento contiene artefactos de diseño y razonamiento secundario.
No hay entrevistas de campo primarias ni estadísticas inventadas presentadas como dato medido.
Etiquetas: [COMPROBADO] en producto, [SUPUESTO], [HIPÓTESIS], [DECISIÓN].

1. Objetivos de investigación (del caso)

ObjetivoMétodo en este casoSalida
Entender actores de la agencia de barrioModelado de stakeholders + personas§3–4
Definir job de “ver 3 fincas con mi criterio” y de publicarJTBD + stories Must§5
Mapear fricción Idealista / WhatsApp → comparaciónJourney + service blueprint§6–7
Traducir a requisitos L1 wizard + comparadorMatriz hallazgo → requisito → feature§8

2. Fuentes y límites

Fuentes admisibles (secundarias / operativas)

  • Conocimiento general de búsqueda de alquiler en Madrid sur, portales (Idealista / Fotocasa) y agencias de barrio con escaparate en Marcelo Usera.
  • Analogía operativa con wizard de criterios + mesa JWT de la serie daily.
  • Restricciones ALS-2: no lead, no reserva, no galería editorial, no tabla de necesidades, no parte de obra.

Límites éticos de verdad

ProhibidoPermitido
“El 68% de buscadores de Usera abre 6 tabs” sin fuente[SUPUESTO] el buscador llega con ‘quiero ver tres a la vez’”
Citas de entrevistas ficticias como campo realQuotes de persona etiquetadas como constructo de diseño
Precio medio medido “890 € el 3º de Amparo” como KPI de mercadoRenta de seed de producto, etiquetada como demo
Horario oficial de una agencia concreta en Marcelo UseraLocal de seed; Lucía es persona de diseño
“Lucía existe y nos dijo…”Lucía Ferrer es persona de diseño

3. Stakeholders

StakeholderInfluenciaInterésNecesidad principal
BUSCADOR / pareja (Marcos)Baja formalMuy altaDejar criterio y ver 3 fincas sin cuenta
AGENTE / agencia (Lucía)AltaMuy altaPublicar fincas sin CRM enterprise
PROPIETARIA (Inés)Media–altaAltaQue el 3º de Amparo se compare; no gestionar visitas aquí
Pareja de MarcosIndirectaAltaQue el tope de 950 € se respete en las 3 columnas
Vecindario (Usera / Vallecas / Entrevías / Orcasur)Baja v1MediaAparecer como zona, no como “sur genérico”
Portal (Idealista / Fotocasa)Competencia / canal paraleloNo clonar el portal; sí sustituir las 6 pestañas
Captador / “te llamamos”Baja v1BajaFuera: comparar ≠ lead

Mapa de poder (resumen)

  • Decisor de adopción: la agente (quien publica el escaparate) y, en difusión, el boca a boca del local.
  • Usuario frecuente de comparación: BUSCADOR (wizard + 3 columnas + ficha).
  • Usuario frecuente de publicación: AGENTE (mesa).
  • Riesgo de rechazo: si la home no es el paso 1; si el comparador parece un formulario de visita; si un DRAFT se filtra.

4. Personas

P1 — Marcos Vidal · BUSCADOR

CampoDetalle
Edad / contexto31 años; pareja; busca 2 hab en Usera; tope 950 €; el móvil es el escaparate
DigitalMedia–alta; tiene Idealista en 6 pestañas; odia el “te llamamos en 5 minutos”
GoalsDejar zona y tope, ver 3 fincas a la vez, abrir la que encaje, no crear cuenta
PainsSe pasa de presupuesto sin darse cuenta; no sabe si hay ascensor hasta el quinto anuncio; le llaman por dejar el móvil
Quote de diseño“Quiero ver tres a la vez. No me llaméis.”
Seed demoSin email de producto; llega por enlace o por el wordmark

Escenario: Abre ZAGUÁN, elige Usera + ALQUILER, tope 950 €, 2 hab, no exige ascensor, compara Amparo 14 y Yeserías 8, abre la ficha del 3º. No se registra.

[DECISIÓN] Marcos no tiene cuenta. Crear usuario de buscador sería fricción y contradice H3 / S2.

P2 — Lucía Ferrer · AGENTE

CampoDetalle
Edad / contexto46 años; agente de barrio; local en Marcelo Usera; lleva 8–20 fincas
DigitalMedia–alta; desktop para alta; móvil para comprobar el comparador publicado
GoalsVer el estado de cada finca, dar de alta una nueva, publicar sin “captar lead”
PainsWhatsApp + Excel + el portal; miedo a publicar un borrador; Inés pregunta si “ya ha venido alguien”
Quote de diseño“Si está en DRAFT, el barrio no lo ve. Publicado no significa que nadie haya subido.”
Email demolucia@zaguan.barrio / password123

Escenario: Login → mesa → ve Calle de la Merced 2 en DRAFT → termina → Publicar → success con aviso de que publicado no es visitado.

[COMPROBADO] Credencial demo sale del encargo. Nombre, edad y local son constructo de diseño. [DECISIÓN]

P3 — Inés Roldán · PROPIETARIA

CampoDetalle
Edad / contexto58 años; propietaria del 3º D de Calle Amparo 14; vive aún allí / lo alquila [SUPUESTO de escenario]
DigitalMedia; WhatsApp con Lucía; desconfía de “pide visita online”
GoalsQue el piso se compare con otros dos; que la visita la dé ella cuando Lucía llame
PainsCada anuncio se convierte en “¿a qué hora vengo?”; el portal enseña el piso al lado de 40 de Chamberí
Quote de diseño“Que se compare el piso. La visita la doy yo cuando Lucía llame.”

Escenario: Abre el comparador (o se lo enseña Lucía), ve Amparo 14 al lado de Yeserías 8. No hay botón Reservar.

[DECISIÓN] Inés no es rol de sistema. No hay login PROPIETARIA en L1. Evita convertir ZAGUÁN en CRM de captación.

Anti-personas

QuiénPor qué no es target v1
Inversor que quiere 40 fincas y yieldS1: 8–20 activas; no es portal
Buscador que deja el móvil “para que le llamen”S2 / H2: no hay lead
Community manager de portal con likesS4; rompe F-CMP
Hipotecado que quiere simulador y tasaciónS4
Jefe de producto de CRM inmobiliario (40 estados)S3: mesa de 1 agente
Lectora de MARGEN que pregunta “qué hay esta semana”Otro job, otro día

5. JTBD y user stories

Job principal (BUSCADOR)

Cuando busco piso en el barrio y ya tengo tope y zona,
quiero dejar el criterio y ver tres fincas a la vez,
para no abrir seis pestañas ni dejar el móvil a un captador.

Job principal (AGENTE)

Cuando entra un piso o termino las fotos,
quiero listar las fincas, dar de alta la que falta y publicarla,
para que el barrio vea solo lo cerrado y yo no confunda borrador con escaparate.

Job principal (PROPIETARIA)

Cuando mi piso está en alquiler,
quiero que se compare con otros dos del barrio,
para que se entienda el precio sin que ZAGUÁN reserve la visita.

Jobs secundarios

JobRol
Distinguir publicado de borradorAmbos
Ver planta, gastos y extras en la misma fila visualBUSCADOR
Saber que el tope no es una ofertaBUSCADOR
Publicar sin confirmar una visitaAGENTE
Ocultar un borrador al comparadorAGENTE
Entender que nadie ha subido aúnPROPIETARIA

Stories Must (v1)

IDStoryAC
US1Como buscador, quiero elegir zona y tipoPaso 1 /; chips USERA / VALLECAS / ENTREVIAS / ORCASUR
US2Como buscador, quiero fijar tope y habitacionesPaso 2; copy «criterio ≠ oferta»
US3Como buscador, quiero marcar mustsPaso 3: ascensor, terraza, mascotas, reforma
US4Como buscador, quiero ver 3 fincas comparadasPOST /api/compare → 3 cols; sin CTA de reserva
US5Como buscador, quiero abrir la fichaGET :slug; foto real; disclaimer «no llama»
US6Como buscador, no quiero crear cuentaCero forms de registro en cara pública
US7Como AGENTE, quiero entrar con email/passwordJWT + redirect /mesa
US8Como AGENTE, quiero listar DRAFT y PUBLISHEDGET /api/mesa/listings JWT
US9Como AGENTE, quiero crear una fincaPOST /api/listings → DRAFT
US10Como AGENTE, quiero publicarlaPATCH { status: "PUBLISHED" } + success
US11Como cualquiera, quiero empty / errorUI-10, UI-11
US12Como buscador en móvil, quiero hacer el wizard a ~390pxVariante UI-01; cuerpo ≥16px; targets ≥44px

MoSCoW (v1 L1)

PrioridadÍtems
MustWizard 3 pasos, comparador 3 cols, ficha, login, mesa, alta, success, empty/error, mobile wizard
ShouldBadge DRAFT/PUBLISHED en mesa; pie «ZAGUÁN no llama»; mismatch badges en columnas
CouldPersistir Comparison; despublicar a DRAFT; ordenar mesa por updatedAt; loading skeleton
Won’tLead, visita, pasarela, multi-agente, mapa, cuenta buscador

6. Journey (BUSCADOR → AGENTE)

FaseActorAcciónTouchpointEmoción [HIPÓTESIS]
1 EntrarMarcosAterriza; el paso 1 es la home/ chrome S-WIZClaridad, “esto no es un portal”
2 AcotarMarcosUsera + ALQUILER → 950 €, 2 hab → musts sueltos/criterio/presupuesto · /criterio/mustsControl (el tope no es oferta)
3 CompararMarcosVe Amparo 14 · Yeserías 8 · (tercera)/comparar 3 colsAlivio de no abrir 6 tabs
4 MirarMarcosAbre ficha del 3º de Amparo/fincas/amparo-14Reconocimiento del patio
5 No reservarMarcosNo hay botón; anota la direcciónFuera de appControl (H2)
6 PublicarLucíaLogin, ve DRAFT Merced 2, publica/mesa/mesa/nueva → okOficio, no euforia de “ya hay visita”
7 ComprobarLucíaAbre comparador en incógnito: el DRAFT no está/ públicaConfianza

Momentos de verdad

  1. Paso 1 en el primer viewport — si hay que “descubrir la marca”, se rompe H-WIZARD. [HIPÓTESIS]
  2. Comparador sin botón de reserva — si aparece “Pedir visita”, H2 falla.
  3. DRAFT invisible en público — prueba de integridad de catálogo.
  4. Success de mesa dice que publicado no es visitado — evita la mentira de la visita.
  5. Tope etiquetado como criterio — si el paso 2 dice “tu oferta”, H2/microcopy fallan. [HIPÓTESIS]

7. Service blueprint (resumen)

CapaElementos
Frontstage BUSCADORChrome de pasos, wizard 1–3, comparador, ficha
Frontstage AGENTELogin, mesa, alta, success
BackstageLlamada de Lucía a Inés, juego de llaves, visita en el zaguán (fuera de app)
SistemasNest API :3018, Neon curly-recipe-23023272, JWT, Angular wizard
SoportesSeed 1 AGENTE + 5 PUBLISHED + 1 DRAFT + fotos, Paper, docs
Fallos0 matches; 401 sin token; 404 slug DRAFT; red caída → UI error

Fallos de servicio y respuesta de diseño

FalloEvidencia de UIRecuperación
0 fincas al criterioUI-10 Empty comparador“Ninguna finca encaja con este criterio. Afloja un must o sube el tope.”
Mesa vacíaUI-10 Empty mesa“Aún no hay fincas. Da de alta la primera.”
API caídaUI-11 ErrorReintentar
Latencia compareLoading (doc 16)Skeleton de 3 columnas caliza
Form inválidoInline fieldNo navegar a success ni a comparar
Intento de leer DRAFT por URL404“Esa finca no está publicada.” [H3 / integridad]
Paso 2 sin zonaBloqueoVolver a / ; no se salta el wizard

8. Matriz hallazgo → requisito → feature

HallazgoTipoRequisitoFeature v1
Llega con “quiero ver tres a la vez”[SUPUESTO] S2Wizard es la homeUI-01 / H-WIZARD
Seis pestañas no escriben el criterio[HIPÓTESIS] H13 pasos + 3 columnasPOST compare
Comparar se lee como reservar[HIPÓTESIS] H2Cero CTA de visitaUI-04 / UI-05 solo lectura
Tope se lee como oferta[DECISIÓN]Copy irrenunciableUI-02 + comparador
Borrador se filtra[SUPUESTO]Público = PUBLISHED404 en DRAFT
Publicado ≠ visitado[DECISIÓN]Copy irrenunciableSuccess + mesa
Agencia 1 persona[SUPUESTO] S3Mesa = lista, no CRMUI-07 / UI-08
Buscador no necesita cuenta[HIPÓTESIS] H3Auth solo AGENTELogin + guards API
L1 compacto, no portal[DECISIÓN]Un rol AGENTE; 5+1 seedSin cuenta BUSCADOR, sin mapa

9. Preguntas abiertas (no bloquean v1)

IDPreguntaCómo se resolvería después
Q1¿Comparison se persiste siempre o solo si el POST lo pide?[DECISIÓN v1] persistencia opcional; el contrato devuelve { criteria, listings[3] }
Q2¿Musts son filtros duros o score blando?[DECISIÓN v1] duros si están ON; si 0 matches, empty. Could: relajar y marcar mismatch
Q3¿VENTA en el seed?No. El chip existe; empty honesto si se elige VENTA
Q4¿Despublicar vuelve a DRAFT o hay UNLISTED?[DECISIÓN v1] PATCH a DRAFT basta
Q5¿Gastos comunitarios son oficiales de cada finca?No. Seed demo. [SUPUESTO de catálogo]
Q6¿El local de Marcelo Usera es una agencia real?No. Atrezzo de persona. No se afirma como negocio existente

10. Plan de research futuro (si hubiera campo real)

MétodoMuestra orientativaPregunta
Test de usabilidad wizard5–6 buscadores 25–40¿Llegan a 3 columnas en < 45 s?
Shadowing de mesa1 agente de barrio¿La mesa sustituye Excel + WhatsApp?
Revisión de expectativa de visitaClics en comparar vs “¿cuándo subo?”Validar H2
Wizard mobile 3905 sesiones¿Los 3 pasos se completan con el pulgar?

Estos métodos no se han ejecutado. No se reportan hallazgos como si lo hubieran sido.

11. Síntesis

ZAGUÁN se diseña como configurador de barrio que compara y publica, no como portal de leads, no como agenda de visitas y no como lookbook editorial.
La investigación del caso es constructiva y etiquetada: personas, JTBD y journey son artefactos de diseño; las métricas de validación quedan para uso real futuro.

03-information-architecture.md

03 — Arquitectura de información — ZAGUÁN

1. Principios de IA

PrincipioAplicación
Wizard firstLa home es el paso 1. No hay landing de marca por delante.
Criterio → 3 columnas → fichaProfundidad 2 desde la columna hasta la finca; no hay modal de reserva
Público vs AGENTEWizard, comparador y ficha abiertos; mesa solo autenticada
Lenguaje de dominioCriterio, finca, must, mesa, zaguán — no “lead”, “SKU”, “pieza”, “plaza”
Borrador invisibleEl barrio no ve DRAFT; 404 si adivina el slug
Comparar se miraColumna no tiene cupo, botón ni form de visita (≠ F-BOOK)
Chrome de pasos siempre a manoS-WIZ: wordmark + 1 / 2 / 3; no masthead, no rail, no dock

2. Sitemap

/                                      Wizard paso 1 · zona + tipo (público)     H-WIZARD
/criterio/presupuesto                  Wizard paso 2 · tope + habitaciones
/criterio/musts                        Wizard paso 3 · ascensor / terraza / mascotas / reforma
/comparar                              Comparador 3 columnas (público)
/fincas/:slug                          Ficha de finca (público si PUBLISHED)
/login                                 Login JWT AGENTE
/mesa                                  Lista de fincas (auth AGENTE)
/mesa/nueva                            Alta de finca (auth AGENTE)
/mesa/ok/:slug                         Success post-publicación (auth AGENTE)
/**                                    → redirect /

Árbol por audiencia

AudienciaNodos relevantes
BUSCADOR / PROPIETARIAZona → Presupuesto → Musts → Comparar → Ficha
AGENTELogin → Mesa → Nueva → Ok (y wizard público para comprobar)
AmbosWordmark → / (AGENTE autenticada: enlace discreto Mesa)

[COMPROBADO] Nueve rutas del encargo L1. No se inventan /visita, /reservar, /lead, /favoritos, /mesa/:id.

3. Navegación

Chrome de pasos (S-WIZ)

SlotDestinoVisibilidadNotas
Wordmark ZAGUÁN/ (reinicia criterio)TodosInstrument Serif; no logotipo ilustrado
1 Zona/Todos en wizardEstado activo en paso 1
2 Presupuesto/criterio/presupuestoSi hay zonaBloqueado si falta paso 1
3 Musts/criterio/mustsSi hay topeBloqueado si falta paso 2
Soy agente / Mesa/login o /mesaPie discreto, no mastheadAcceso AGENTE

[DECISIÓN] El chrome de pasos es la única navegación primaria de la cara pública. Encaja L1 compacto. No hay masthead (MARGEN), no rail (CESTA), no dock (RONDA / CANTERA).

Desktop ≥960px: wordmark a la izquierda; pasos 1—2—3 a la derecha o centrados; rule junta bajo el chrome; óxido en el paso activo.
Mobile ~390px: wordmark + “1 de 3”; los labels Zona / Presupuesto / Musts se compactan. [DECISIÓN] En 390 no se introduce hamburger de marketing.

Pública (además del chrome)

ElementoDestinoNotas
Chip de zonaestado del paso 1Usera · Vallecas · Entrevías · Orcasur
Siguientepaso N+1Primario óxido
Atráspaso N−1Secundario
Columna de finca/fincas/:slugFoto + dirección + datos
Disclaimerno navegaTexto
“Soy agente”/loginPie, Sora 13 hierro

[DECISIÓN] No hay /fincas índice. El catálogo se compara, no se lista. Un listado facetado sería H-SEARCH / L+1.

AGENTE (autenticado)

ElementoDestinoVisibilidad
Mesa/mesaAGENTE
Nueva finca/mesa/nuevaAGENTE
Badge rolAGENTE
NombreLucía FerrerAGENTE
Salirlimpia token → /loginAGENTE
Publicaren form o en fila de mesaAGENTE · no hay /mesa/:id de ficha CRM

[DECISIÓN] L1: el PATCH vive en la fila de la mesa y/o al final del form de alta. No se añade /mesa/:id como Must.

En mesa el chrome de pasos desaparece: la trastienda no finge un wizard. Wordmark + Mesa + Salir bastan. [DECISIÓN]

4. Inventario de contenido

PantallaContenidos
Paso 1 /Chrome S-WIZ, titular, chips de zona, tipo ALQUILER/VENTA, Siguiente, empty de estado, “Soy agente”
Paso 2Tope de renta (input o slider), habitaciones (1 / 2 / 3+), disclaimer «criterio ≠ oferta», Atrás / Siguiente
Paso 3Toggles ascensor, terraza, mascotas, reforma, Atrás / Comparar
ComparadorCriterio resumen, 3 columnas (foto, dirección, precio, m², planta, gastos, extras), disclaimer «comparar ≠ reservar», empty, loading, error
FichaCover, dirección, zona, precio, m², planta, gastos, extras, cuerpo, «ZAGUÁN no llama», volver al comparador
LoginTitular mesa, email, password, submit, error
MesaLista: dirección, zona, renta, status, updatedAt, acciones, CTA Nueva, empty, error
AltaCampos dirección, zona, tipo, renta, m², hab, planta, gastos, musts, cover, cuerpo, submit
SuccessMensaje, slug, «publicado ≠ visitado», CTA ver finca / volver a mesa

5. Taxonomía

PublishStatus (Listing)

Status APILabel UISemántica
DRAFTBorradorEl barrio no lo ve
PUBLISHEDPublicadoVisible en compare y ficha; no implica visita

Orden de mesa: updatedAt descendente (lo último tocado primero).
[DECISIÓN] No hay RESERVED / VISITED / OPTIONED: S3, mesa de 1 agente.

Zona

APILabel UISeed
USERAUseraamparo-14, yeserias-8, merced-2
VALLECASVallecasalbufera-212
ENTREVIASEntrevíassicilia-9
ORCASUROrcasuralegria-3

Tipo

APILabel UISeed
ALQUILERAlquilerLas 6 fincas
VENTAVentaNinguna en seed → empty honesto

[DECISIÓN] tipo no es ático/bajo: eso vive en floorLabel y extras.

Listing (líneas seed)

slugZonaDirecciónHabRentaStatus
amparo-14UseraCalle Amparo 14, 3º D682890 €PUBLISHED
albufera-212VallecasAv. de la Albufera 212, 5º743980 €PUBLISHED
yeserias-8UseraPaseo de Yeserías 8, bajo622820 €PUBLISHED
sicilia-9EntrevíasCalle Sicilia 9, 2º8131050 €PUBLISHED
alegria-3OrcasurCalle de la Alegría 3, ático551760 €PUBLISHED
merced-2UseraCalle de la Merced 2, 1º izq702870 €DRAFT

[COMPROBADO] Cinco publicadas + un DRAFT cubren el must del brief. El DRAFT extra es para que la mesa no sea una lista de solo publicados. [DECISIÓN]

Musts (booleanos)

APILabel UISemántica
elevatorAscensortrue = hay ascensor
terraceTerrazatrue = terraza (patio de manzana se anota en extras, no es terraza)
petsMascotastrue = se admiten
renovationReformatrue = reformado / reforma hecha

6. Modelo mental vs UI

Modelo mentalRepresentación
“Busco en Usera”Paso 1, chip Usera
“No paso de 950”Paso 2, tope; copy criterio ≠ oferta
“Sin ascensor no subo”Paso 3, toggle
“Enséñame tres”/comparar 3 cols
“El de Amparo”Columna → /fincas/amparo-14
“¿Pido visita?”No hay UI. Copy: comparar ≠ reservar. ZAGUÁN no llama
“Lo estoy fichando”Badge Borrador en mesa
“Ya está en el escaparate”Badge Publicado; entra en compare
“¿Ya ha venido alguien?”Success / mesa: publicado ≠ visitado

7. Query string y estado de criterio

El wizard persiste el criterio en el cliente (sessionStorage zaguan_criteria) y lo reenvía en POST /api/compare.

Únicos query admitidos en URL:

RutaQueryUso
/login?next=/mesaRedirect post-login
/compararopcional eco de criterioDeep-link Could; no Must

[DECISIÓN] Sin ?q= ni chips de zona en una home-listado. Eso sería H-SEARCH.

Campos del criterio (cliente + POST compare):

zona, tipo, maxRent, rooms, elevator, terrace, pets, renovation

Paso 2 no avanza sin zona + tipo. Paso 3 no avanza sin maxRent + rooms. Comparar no corre sin los tres pasos. [DECISIÓN]

8. Rutas API alineadas a IA

UIAPI
Listado público / filtrosGET /api/listings (query opcional)
ComparadorPOST /api/compare
FichaGET /api/listings/:slug
LoginPOST /api/auth/login
Mesa listGET /api/mesa/listings JWT
AltaPOST /api/listings JWT
Publicar / despublicarPATCH /api/listings/:id { status } JWT

[COMPROBADO] Contrato del encargo (Listing, AGENTE, compare). No se documentan visitas, leads ni POST /api/visits como Must.

9. Decisiones de IA descartadas

IdeaPor qué no en L1 v1
/explorar + grid de fincasRompe H-WIZARD / F-CMP
Área “mis favoritos”Cuenta de facto; contradice H3
/mesa/:id ficha CRML1 compacto; acciones en lista + form de alta
Search / mapa como homeH-SEARCH / H-MAP de otros días
Feed de “recién publicados”H-FEED de RONDA
Tabla de fincas como homeH-TABLE de CESTA
Lookbook editorialH-GALLERY de MARGEN
/visita/:slugRompe H2 / microcopy Comparar ≠ reservar
Wizard de 4 pasos de publicaciónF-ONB de CANTERA; el alta es 1 pantalla

04-user-flows.md

04 — Flujos de usuario — ZAGUÁN

Convenciones

  • Actor: Guest (BUSCADOR / PROPIETARIA) | AGENTE
  • Éxito: resultado observable
  • Errores: UI + código HTTP cuando aplica
  • Flujo canónico: F-CMP (criterio 3 pasos → 3 columnas → ficha)
  • Flujo de mesa: login → lista → alta → publicar

F1 — Wizard paso 1 · zona + tipo (BUSCADOR)

/  → H-WIZARD paso 1
   → chips zona · tipo ALQUILER | VENTA
   → sessionStorage.zaguan_criteria { zona, tipo }
   → Siguiente → /criterio/presupuesto
   → pie “Soy agente” → /login
PasoAcciónSistema
1Aterriza; el paso 1 es el primer focoRender home
2Elige una zona (obligatorio)Criterio cliente
3Elige tipo (default ALQUILER)Criterio cliente
4SiguienteRouter + persist

Éxito: criterio tiene zona + tipo; avanza al paso 2.
AC: no hay que hacer scroll de “marca” para llegar a los chips. No hay hero 2-col.

Errores / estados

CasoComportamiento
Siguiente sin zonaInline: “Elige una zona.” No navega
VENTA (sin seed)Se permite elegir; el empty llega en F4
Primera cargaChrome S-WIZ + chips; no spinner de marca

F2 — Wizard paso 2 · presupuesto + habitaciones (BUSCADOR)

/criterio/presupuesto
   → si no hay zona → redirect /
   → input/slider maxRent · chips 1 / 2 / 3+
   → disclaimer “Esto es un criterio, no una oferta.”
   → Atrás / Siguiente → /criterio/musts
PasoAcciónSistema
1Lee el topeCriterio.maxRent
2Elige habitacionesCriterio.rooms
3Lee el disclaimerUI

Éxito: maxRent + rooms persistidos.
Rango demo: 500–1500 €, step 10. Default sugerido 950 (Marcos). [DECISIÓN]

CasoComportamiento
maxRent vacío o < 500Inline; no avanza
rooms vacíoInline; no avanza
Entrada directa a la URLRedirect a / si falta zona

F3 — Wizard paso 3 · musts (BUSCADOR)

/criterio/musts
   → si falta tope → redirect /criterio/presupuesto
   → toggles elevator, terrace, pets, renovation (default OFF)
   → Atrás / Comparar → POST /api/compare → /comparar
PasoAcciónSistema
1Enciende solo lo que es mustCriterio booleanos
2CompararPOST compare

Éxito: criterio completo; navega a 3 columnas o empty.
AC: OFF significa “me da igual”, no “sin ascensor”. [DECISIÓN]

Default OFF = no filtrar por ese must. ON = filtro duro.


F4 — Comparador 3 columnas (BUSCADOR)

/comparar → POST /api/compare { zona, tipo, maxRent, rooms, elevator, terrace, pets, renovation }
          → { criteria, listings[≤3] }
          → 3 cols  |  empty  |  error  |  skeleton
          → click columna → /fincas/:slug
PasoAcciónSistema
1Lee el resumen de criterio (zona, tope, hab, musts)criteria echo
2Recorre precio, m², planta, gastos, extraslistings[3]
3Abre fichaRouter
4Lee “Comparar no es reservar.”UI

Éxito: hasta 3 fincas PUBLISHED alcanzables, o empty accionable.
AC columnas: cada una muestra precio, m², planta, gastos, extras. Cero botones Reservar / Te llamamos.

Ranking [DECISIÓN]

  1. Solo PUBLISHED.
  2. Filtros duros: tipo, maxRent (renta ≤ tope), musts ON.
  3. Preferencia: misma zona, luego rooms exactas, luego renta más cercana al tope por debajo.
  4. Devuelve hasta 3. Si hay más de 3, top-3. Si hay 1–2, se muestran huecos vacíos (no se inventan fincas). Si hay 0, UI-10.

Guión Marcos (demo): zona USERA, ALQUILER, 950 €, 2 hab, musts OFF → amparo-14 (890) + yeserias-8 (820). Tercera columna vacía o, si se relajara zona, alegria-3 no entra por 1 hab. [COMPROBADO de seed] 2 matches exactos.

Errores / estados

CasoComportamiento
0 matchesUI-10: “Ninguna finca encaja con este criterio.” + Afloja un must / cambia zona
Red / 5xxUI-11 + Reintentar (repite POST)
Primera cargaSkeleton de 3 columnas caliza (doc 16)
Sin criterioRedirect /
VENTAEmpty honesto (seed vacío)

[DECISIÓN] GET /api/listings con query es el listado crudo (Could en UI). El Must de producto es POST compare.


F5 — Ficha de finca (BUSCADOR)

/fincas/:slug → GET /api/listings/:slug
              → cover, dirección, zona, precio, m², planta, gastos, extras, cuerpo
              → “ZAGUÁN no llama. Si hay visita, la agencia llama.”
              → enlace Comparar → /comparar
              → 404 slug DRAFT o inexistente → mensaje + volver al wizard
PasoAcciónSistema
1Lee dirección y precioListing
2Lee planta, gastos, extrasListing
3Lee cuerpoListing.body
4Decide otra finca o volverRouter

Éxito: finca leíble; disclaimer visible; no hay reserva ni teléfono de captación.
Error: 404 si slug inexistente o status=DRAFT.

AC ficha

#Criterio
1Foto real del cover (no color sólido) — salvo DRAFT sin foto
2Dirección en Instrument Serif; zona en Sora hierro
3Precio, m², planta, gastos, extras visibles sobre el fold
4Disclaimer “ZAGUÁN no llama” visible sin scroll en desktop
5Sin CTA de visita, lead o “te llamamos”

[DECISIÓN] Un DRAFT nunca hace 200 en esta ruta. No se “previsualiza” en público ni con query ?preview=. Preview = L+1 con token.


F6 — Login JWT (AGENTE)

/login → POST /api/auth/login { email, password }
      → 200 { accessToken, user } → localStorage → /mesa  (o ?next sanitizado)
      → 401 → mensaje error en form
CampoValidación clienteAPI
emailrequired, email@IsEmail()
passwordrequired, min 6@MinLength(6)

Éxito: token guardado; user role=AGENT, nombre Lucía Ferrer.
Credencial demo: lucia@zaguan.barrio / password123.

CasoComportamiento
Credenciales inválidas401 + mensaje
Red caídaError de red en UI
Token caducado en mesa401 en GET → re-login

F7 — Mesa de la agencia (AGENTE)

/mesa (token en cliente)
  → GET /api/mesa/listings
  → Render filas + badges DRAFT / PUBLISHED + CTA Nueva
CasoComportamiento
Lista con itemsFilas: dirección, zona, renta, status, updatedAt, acción Publicar si DRAFT
Lista vacíaEmpty: “Aún no hay fincas. Da de alta la primera.”
Fallo red / 401Error + reintentar / re-login

[DECISIÓN] Sin stats endpoint en v1. Recuentos DRAFT/PUBLISHED, si se muestran, se derivan del array.

Seed: 5 PUBLISHED + merced-2 DRAFT.


F8 — Alta de finca (AGENTE)

/mesa/nueva
  → validación cliente
  → POST /api/listings {
        title, address, zona, tipo,
        sqm, rooms, rent, expenses, floorLabel,
        elevator, terrace, pets, renovation,
        cover, body
     }
  → 201 Listing { slug, status: DRAFT, ... }
  → o bien POST + PATCH { status: "PUBLISHED" } si elige Publicar
  → /mesa/ok/:slug
CampoValidación cliente (mín.)API
title / addressrequired, min 4@MinLength(4)
zonarequired, enumUSERA | VALLECAS | ENTREVIAS | ORCASUR
tiporequired, enumALQUILER | VENTA
sqmrequired, > 0@Min(1)
roomsrequired, 1–6@Min(1) @Max(6)
rentrequired, > 0@Min(1)
expensesrequired, ≥ 0@Min(0)
floorLabelrequired, min 2@MinLength(2)
elevator, terrace, pets, renovationbooleandefault false
coverrequired si Publicar; opcional en DRAFTallowlist de assets
bodyrequired, min 40@MinLength(40)
statusno en POST; siempre DRAFT al crearservidor fuerza DRAFT

Éxito: registro status=DRAFT (o PUBLISHED si eligió Publicar en el mismo viaje) + pantalla success.
No hay cuenta de buscador en v1.

Errores

CasoComportamiento
Validación DTO400 + mensaje de campo
Red caídaError de red en form; no navegar a /mesa/ok/…
Doble submitBotón disabled mientras pending
body < 40400
Sin token401

[DECISIÓN] Crear siempre como DRAFT. El botón Publicar del form hace POST + PATCH. Ver doc 20.

[COMPROBADO] Entidad Listing según encargo.


F9 — Publicar desde la mesa (AGENTE)

/mesa
  → PATCH /api/listings/:id { status: "PUBLISHED" }
  → 200 Listing actualizado
  → opcional: /mesa/ok/:slug si se quiere el board de success

Transiciones (máquina simple L1)

DesdeHaciaIntención
DRAFTPUBLISHEDLucía cierra la finca; entra al comparador
PUBLISHEDDRAFTCorrección / retirada temporal
RESERVED / VISITEDNo en v1

Éxito: badge actualizado; la finca aparece en GET /api/listings y puede salir en compare.
Errores: 404 id; 401 sin token; 400 status inválido.

Regla de integridad (ops): Publicar no crea una visita, una llamada ni un lead. Publicado ≠ visitado. ZAGUÁN no llama.


F10 — Success de publicación (AGENTE)

/mesa/ok/:slug → copy de acuse + título/slug
               → “Publicado no es visitado.”
               → “El barrio ya puede compararla. Nadie ha subido todavía.”
               → CTA ver finca pública / volver a la mesa
PasoAcciónSistema
1Lee acuseUI
2Comprueba la finca en incógnito (Could)Fuera / otra pestaña
3Sigue con el siguiente DRAFT/mesa

AC: el disclaimer es visible sin scroll en desktop.
Si se entra a /mesa/ok/algo-que-no-existe: mensaje genérico + volver a mesa; no se inventa un registro. [DECISIÓN]


F11 — Logout (AGENTE)

Click "Salir" (mesa) → limpia localStorage (token + user) → /login

Matriz de errores global

CódigoCuándoUI
400DTO inválidoMensaje campo / genérico
401Sin/mal token en mesaRe-login
404slug DRAFT, id o slug no existeMensaje + volver al wizard
5xx / networkAPI caídaError + retry

Flujos fuera de alcance v1

  • Registro / recuperación de password de AGENTE
  • Cuenta de buscador / favoritos / alertas
  • Reserva o confirmación de visita
  • “Te llamamos” / captura de teléfono
  • Notificación email “hay un piso nuevo”
  • Filtros multi-criterio en mesa
  • Detalle /mesa/:id
  • Mapa / street view
  • Pasarela / señal / contrato

05-data-model.md

05 — Modelo de datos — ZAGUÁN

1. Visión general

Dominio L1 de inmobiliaria de barrio + mesa de publicación:

EntidadTabla SQLPropósito
UserusersIdentidad de agencia (rol AGENT)
SessionsessionsSesión JWT / token persistido (Should)
ListinglistingsFinca en alquiler (o venta)
ComparisoncomparisonsCriterio + hasta 3 slugs (persistencia opcional)

Base: PostgreSQL (Neon project curly-recipe-23023272) · IDs: cuid() o entero serial.
[DECISIÓN] ensureSchema o Prisma son válidos si el contrato de tablas se cumple; el brief de stack no exige Prisma.

2. Enums (aplicación; persistidos como TEXT + check)

Role

ValorDescripción
AGENTOperadora de la mesa; único rol autenticado v1

PublishStatus

ValorDescripción
DRAFTInvisible en cara pública
PUBLISHEDVisible en listings / compare / ficha

Zona

USERA | VALLECAS | ENTREVIAS | ORCASUR

Tipo

ALQUILER | VENTA

3. Diagrama ER (texto)

users
  id, email, password_hash, name, role(AGENT)
  created_at, updated_at
  1 ──< sessions

sessions
  id, user_id → users
  token_hash, expires_at
  created_at

listings
  id, slug (unique)
  title, address, floor_label
  zona (USERA | VALLECAS | ENTREVIAS | ORCASUR)
  tipo (ALQUILER | VENTA)
  sqm, rooms, rent, expenses
  elevator, terrace, pets, renovation
  cover, body
  status (DRAFT | PUBLISHED)
  created_at, updated_at

comparisons                  -- opcional
  id
  zona, tipo, max_rent, rooms
  elevator, terrace, pets, renovation
  listing_slugs (TEXT[] o JSON)
  created_at

No hay FK entre listings y users en v1: la agente opera sobre el conjunto global.
No hay FK buscador: comparar es anónimo.

4. Tablas

users

CampoTipoConstraints
idTEXTPK
emailTEXTUNIQUE NOT NULL
password_hashTEXTNOT NULL (bcrypt)
nameTEXTNOT NULL
roleTEXTNOT NULL DEFAULT 'AGENT'
created_atTIMESTAMPTZDEFAULT now()
updated_atTIMESTAMPTZDEFAULT now()

sessions

CampoTipoConstraints
idTEXTPK
user_idTEXTNOT NULL FK → users(id) ON DELETE CASCADE
token_hashTEXTNOT NULL
expires_atTIMESTAMPTZNOT NULL
created_atTIMESTAMPTZDEFAULT now()

[DECISIÓN] El JWT viaja en Authorization: Bearer. sessions permite invalidar (logout) sin esperar expiración. Si el slice usa JWT stateless, documentar el hueco; D-P1-05 se cumple igual con guard.

listings

CampoTipoConstraints
idTEXTPK
slugTEXTUNIQUE NOT NULL, kebab-case
titleTEXTNOT NULL
addressTEXTNOT NULL
floor_labelTEXTNOT NULL (3º D, bajo, ático)
zonaTEXTNOT NULL
tipoTEXTNOT NULL DEFAULT 'ALQUILER'
sqmINTEGERNOT NULL CHECK > 0
roomsINTEGERNOT NULL CHECK ≥ 1
rentINTEGERNOT NULL CHECK > 0 · euros / mes
expensesINTEGERNOT NULL CHECK ≥ 0 · comunidad € / mes
elevatorBOOLEANNOT NULL DEFAULT false
terraceBOOLEANNOT NULL DEFAULT false
petsBOOLEANNOT NULL DEFAULT false
renovationBOOLEANNOT NULL DEFAULT false
coverTEXTfilename (amparo.jpg) · NULL en DRAFT sin foto
bodyTEXTNOT NULL
statusTEXTNOT NULL DEFAULT 'DRAFT'
created_at / updated_atTIMESTAMPTZ

extras no se persiste como columna suelta: se deriva para el comparador (patio vive en body / un campo libre opcional extras TEXT). [DECISIÓN v1] columna opcional extras TEXT para “patio”, “terraza”, “mascotas OK”, “reforma”. Seed la rellena.

extras seedOrigen
patioamparo-14
terrazaalbufera-212, alegria-3
mascotas OKyeserias-8
reformasicilia-9
patio de lucesmerced-2

comparisons (opcional)

CampoTipoConstraints
idTEXTPK
zonaTEXTNOT NULL
tipoTEXTNOT NULL
max_rentINTEGERNOT NULL
roomsINTEGERNOT NULL
elevator / terrace / pets / renovationBOOLEANNULL = OFF (no filtrar)
listing_slugsTEXT[] / JSONhasta 3
created_atTIMESTAMPTZ

[DECISIÓN] Persistencia opcional. El Must del contrato es el response { criteria, listings }. Si no hay tabla, el POST compara en caliente y no guarda.

5. Reglas de integridad y negocio

ReglaImplementación
Lectura pública de fincasGET /api/listings sin guard; solo PUBLISHED
Lectura pública de fichaGET /api/listings/:slug 200 solo si PUBLISHED; DRAFT → 404
ComparePOST /api/compare público; solo PUBLISHED; hasta 3
List / create / patch listingsJWT AGENT → 401 sin token
Slug únicokebab del título o dirección + sufijo -2 si colisión [DECISIÓN]
Status default listingDRAFT al crear
PasswordNunca en claro; solo password_hash
Orden listings GETrent asc (se compara precio, no “destacados”)
Orden mesaupdated_at desc
Orden comparescore (zona → rooms → cercanía al tope)
Publicar Listingno crea visita ni llamada
Público nunca lista DRAFTGET listings filtra; no hay ?includeDrafts
maxRentrent <= maxRent
Must ONigualdad true; OFF / omitido = no filtra

6. Contratos API (resumen)

GET /api/listings (público)

Query opcional: zona, maxRent, rooms, elevator, terrace, pets, renovation.
Response 200: ListingCard[] solo PUBLISHED.

[
  {
    "id": "lst_amparo",
    "slug": "amparo-14",
    "title": "Calle Amparo 14, 3º D",
    "address": "Calle Amparo 14, 3º D",
    "floorLabel": "3º D",
    "zona": "USERA",
    "tipo": "ALQUILER",
    "sqm": 68,
    "rooms": 2,
    "rent": 890,
    "expenses": 42,
    "elevator": false,
    "terrace": false,
    "pets": false,
    "renovation": false,
    "extras": "patio",
    "cover": "amparo.jpg",
    "status": "PUBLISHED"
  }
]

JSON camelCase hacia el web. [DECISIÓN]

POST /api/compare (público)

Body

CampoTipoReq
zonaZona
tipoTipo
maxRentnumber
roomsnumber
elevatorbooleanno
terracebooleanno
petsbooleanno
renovationbooleanno

Response 200

{
  "criteria": {
    "zona": "USERA",
    "tipo": "ALQUILER",
    "maxRent": 950,
    "rooms": 2,
    "elevator": false,
    "terrace": false,
    "pets": false,
    "renovation": false
  },
  "listings": [ "/* hasta 3 ListingCard */" ]
}

Si 0 matches: { criteria, listings: [] } + empty UI-10.
Si se persiste Comparison: el id no es Must en response.

GET /api/listings/:slug (público)

Listing completa (body) o 404 (inexistente o DRAFT).

POST /api/auth/login

Body: { email, password }
Response 200: { accessToken, user: { id, email, name, role } }

GET /api/mesa/listings (JWT)

Array Listing (incluir status), updatedAt desc. Sin token → 401. Incluye DRAFT.

POST /api/listings (JWT)

Body

CampoTipoReq
titlestring
addressstring
floorLabelstring
zonaZona
tipoTipono (default ALQUILER)
sqmnumber
roomsnumber
rentnumber
expensesnumber
elevator / terrace / pets / renovationbooleanno
extrasstringno
coverstringno
bodystring

Response 201: Listing + slug + status: "DRAFT".

PATCH /api/listings/:id (JWT)

Body: { status?: PublishStatus, ...campos editables }
Mínimo Must: { status }.
Response: Listing actualizada. Sin token → 401.

[COMPROBADO] PATCH sobre :id con { status }.

7. Seed de referencia (2026-08-18)

AGENTE

NombreEmailPassword
Lucía Ferrerlucia@zaguan.barriopassword123

[COMPROBADO] Credencial del encargo. Nombre de pila es constructo de diseño.

Listings PUBLISHED

slugDirecciónZonaHabRentaPlantaMusts / extrasCover
amparo-14Calle Amparo 14, 3º DUsera682890 €3ª SIN ascensorpatioamparo.jpg
albufera-212Av. de la Albufera 212, 5ºVallecas743980 €5ª CON ascensorterrazaalbufera.jpg
yeserias-8Paseo de Yeserías 8, bajoUsera622820 €bajomascotas OKyeserias.jpg
sicilia-9Calle Sicilia 9, 2ºEntrevías8131050 €2ª CON ascensorreformasicilia.jpg
alegria-3Calle de la Alegría 3, áticoOrcasur551760 €ático SIN ascensorterrazaalegria.jpg

Listing DRAFT

slugDirecciónZonaHabRentaNotas
merced-2Calle de la Merced 2, 1º izqUsera702870 €Sin foto pública · patio de luces · reforma parcial · no sale en GET público

Gastos (expenses) son catálogo demo, no cuotas reales de comunidad. [SUPUESTO de catálogo]

slugexpenses €/mes
amparo-1442
albufera-21268
yeserias-835
sicilia-955
alegria-330
merced-240

Textos de body son contenido de producto demo, no anuncios de una agencia real. [SUPUESTO de catálogo]

slugBody demo (extracto)
amparo-14Tercero a la calle, dos habitaciones al patio de manzana. Sin ascensor. La cocina toma la luz. Usera, a un salto de Marcelo Usera.
albufera-212Quinto con ascensor y terraza a la avenida. Tres habitaciones. Vallecas, cerca del puente.
yeserias-8Bajo con salida a un patio. Se admiten mascotas. Dos habitaciones. Yeserías, al borde de Usera.
sicilia-9Segundo reformado, ascensor, tres habitaciones. Entrevías. El más caro del seed.
alegria-3Ático de un dormitorio con terraza. Sin ascensor. Orcasur.
merced-2Borrador: primero interior, patio de luces. Lucía aún no ha subido fotos. Invisible en el comparador.

Las seis fincas son seed de producto. No se presentan como cartera real de una agencia de Marcelo Usera. [SUPUESTO de catálogo]

8. Evolución posible (no implementada)

CambioNivel
Persistencia obligatoria de Comparison + replayL+1
agentUserId + segundo agenteL+1 / L3
Preview token de DRAFTL+1
Soft delete + audit log de statusL3
Visitas / calendarioFuera (H2)
Lead / teléfono de buscadorFuera (S2)
Mapa / geocodeFuera (terna)

06-tech-stack.md

06 — Stack tecnológico — ZAGUÁN

1. Visión

CapaTecnologíaNotas
FrontendAngular standalone + signalsPuerto 4200
EstilosTailwind CSSTokens rusted (hormigón/grafito/óxido/hierro/caliza/junta)
BackendNestJS TypeScriptPuerto 3018, prefijo /api
PersistenciaPostgreSQL (ensureSchema o Prisma)Tablas users, sessions?, listings, comparisons?
DBNeon PostgreSQLproject curly-recipe-23023272
AuthJWTRol AGENT
DiseñoPaper01M09V13JEANPFFD6DS1RCN29G
Repo/Users/cristian/orca/zaguan-app/GitHub Criscode2022/zaguan-app

Stack fijo del cron (D-P0-03). No React/Next/Firebase.
[COMPROBADO] Puerto 3018, Neon, Paper y repo constan en el encargo.

2. Estructura

zaguan-app/
├── apps/api/     # Nest (paquete independiente)
├── apps/web/     # Angular + Tailwind (paquete independiente)
└── package.json  # scripts api / start · apps independientes

D-P1-03: npm --prefix o pnpm --filter. Prohibido npm workspaces que rompen Angular (AP-06).

Scripts raíz esperados:

ScriptAcción
apiArranca Nest en :3018
start / webArranca Angular en :4200

3. API pública vs JWT

MétodoRutaAuth
GET/api/listingsPúblico (solo PUBLISHED; query opcional)
POST/api/comparePúblico
GET/api/listings/:slugPúblico (404 si DRAFT)
POST/api/auth/loginPúblico
GET/api/mesa/listingsJWT AGENT
POST/api/listingsJWT AGENT
PATCH/api/listings/:idJWT AGENT

CORS: origen http://localhost:4200 en local.

[COMPROBADO] Superficie API del encargo. No se añaden Must extra (/visits, /leads, /favorites).

4. Justificación

ElecciónRazón
Angular + NestAlineación con la serie daily y el handoff del estudio
Neon serverlessVolumen bajo (S1: 8–20 fincas; demo 5+1); no cola Redis
JWT AGENTD-P1-05: panel interno cerrado desde el día 1
Filtro PUBLISHED en servidorSource of truth = SQL; el comparador no “esconde” DRAFT en cliente
Puerto 3018Evitar colisión con MARGEN :3017 / CESTA :3016 / RONDA :3015

5. Variables

.env local (nunca git):

VariableServicioDescripción
DATABASE_URLAPINeon curly-recipe-23023272
JWT_SECRETAPIFirma tokens
PORTAPIopcional, 3018
JWT_EXPIRESAPIopcional (p. ej. 12h)

.env.example solo placeholders.
Web: URL de API en ApiService (default http://localhost:3018/api).

6. Dependencias de producto (mínimas)

API

  • @nestjs/common / core / platform-express
  • @nestjs/jwt (o verificación manual)
  • bcrypt (o bcryptjs)
  • cliente Postgres (pg) o Prisma
  • class-validator + class-transformer

Web

  • Angular standalone
  • Tailwind 3
  • fuentes: Instrument Serif + Sora (Google Fonts o self-host)

7. Arranque (contrato)

cd /Users/cristian/orca/zaguan-app
# instalar apps independientes (npm --prefix o pnpm --filter)
# apps/api/.env → DATABASE_URL + JWT_SECRET + PORT=3018
# migrate / ensureSchema + seed
# api → http://localhost:3018
# web → http://localhost:4200

8. Lo que este stack no es

NoPor qué
npm workspacesAP-06
Firebase AuthStack fijo JWT + Neon
Next.js / ReactD-P0-03
Pasarela de pago / señalS4
Elasticsearch5 fincas no lo necesitan
CRM headlessS3: mesa de 1 agente, no tipos

9. Nota sobre el repo existente

[COMPROBADO] Existe /Users/cristian/orca/zaguan-app y el remoto previsto Criscode2022/zaguan-app.
El scaffold contiene leftover de MARGEN (issues, pieces, events, fotos de semanario en public/assets/). Esta suite define el contrato ZAGUÁN; el código debe alinearse a las rutas y API de los docs 03–05, no al leftover. [DECISIÓN]

07-creative-direction.md

07 — Dirección creativa — ZAGUÁN

1. Concepto

ZAGUÁN es un umbral de finca en pantalla: se entra con un criterio y se salen tres pisos, no una landing de “inmobiliaria digital”. El ground es hormigón fresco. Las columnas se apoyan como portales en una acera. El óxido es la reja, no un botón de marketing.

Mood elegido: rusted.
Primer instinto descartado: mineral / agencia caliza (piedra clara, serif de centro, “lujo sereno”). Ese mood ya huele a estudio de Chamberí. ZAGUÁN es Marcelo Usera, reja y junta.

[COMPROBADO] Mood, ground y paleta salen de docs/00-day-brief.md.

2. Territorio visual

EjeNo
SuperficieHormigón #E3E1DC, junta, card calizaPapel MARGEN, musgo CESTA, cromo RONDA, agua DERIVA, chalky PIZARRA
TipoInstrument Serif display + Sora UICormorant, Public Sans, Newsreader, Outfit, Spectral, Manrope, Zilla, Mulish, Lora, Karla
FotoFinca de barrio, mediodía, reja, tendederoStock de loft blanco, skyline, “pareja feliz con llaves”
GridWizard centrado + comparador 3 colsHero 2-col + 3 cards de features (AP-12)
AcentoÓxido #B54A2A en paso activo y PUBLISHEDCadmio editorial, naranja ONG, teal seguro
ChromePasos 1—2—3Masthead de periódico, rail, dock, sidebar CRM

3. Paleta

TokenHexRol
Hormigón / G-CONCRETE#E3E1DCGround de página. Gris mineral frío, no beige.
Grafito#1F1D1BTexto, wordmark, cuerpo. Casi negro, no púrpura.
Óxido#B54A2APaso activo, acento, badge PUBLISHED, CTA
Hierro#6A6560Meta, labels, nav idle, planta, gastos
Caliza#F3F1ECSolo cards (columna, ficha, fila). Nunca el fondo de página.
Junta#C9C4BBRules, bordes, skeletons

Jerarquía de color: el hormigón carga; el óxido puntúa; la caliza encuadra la foto. Si el óxido pinta fondos enteros, deja de ser reja y se vuelve “brand rust”. [DECISIÓN]

4. Tipografía

UsoFamiliaTamaños de referencia
WordmarkInstrument Serif 60028–32 desktop · 24 mobile
Titular de pasoInstrument Serif 60040–56
Dirección de fincaInstrument Serif 60028–36 desktop · 24 mobile
PrecioSora 60024–28
Cuerpo de fichaSora 40016–17 / 26–28 lh
UI, pasos, form, comparadorSora 400/500/60014 / 16
Meta (planta, gastos, zona)Sora 50012–13, tracking 0.04em, hierro

Prohibido: Instrument Serif por debajo de 20px (se rompe). Sora en el wordmark (se vuelve startup).

Fuentes vetadas de la serie: Cormorant / Public Sans / Spectral / Manrope / Zilla / Mulish / Newsreader / Outfit / Lora / Karla.

5. Fotografía

Las imágenes del case son el sistema, no un moodboard:

AssetLectura visualUso
hero.jpgZaguán / portal, reja, hormigónCover UX · atmósfera, no hero de marketing
listings/amparo.jpgFinca de Usera, patio, terceroamparo-14
listings/albufera.jpgBloque a la avenida, terraza, quintoalbufera-212
listings/yeserias.jpgBajo a la calle, umbralyeserias-8
listings/sicilia.jpgPortal reformado, Entrevíassicilia-9
listings/alegria.jpgÁtico, terraza, Orcasuralegria-3

Crop: columnas y ficha en 4:3 o 3:2 (fachada). No recorte 1:1 que corta el portal. [DECISIÓN] 4:3 de finca, distinto del 16:10 editorial de MARGEN.

Tratamiento: saturación real, sin LUT “real estate teal”, sin cielo HDR. El óxido no se superpone como filtro: vive en UI.

El DRAFT merced-2 no tiene foto pública. En mesa: placeholder junta + texto “sin foto”.

6. Layout

Chrome de pasos (S-WIZ)

ZAGUÁN                         1 Zona    2 Presupuesto    3 Musts
────────────────────────────────────────────  (rule junta 1px; óxido bajo el activo)

Wordmark a la izquierda. Pasos a la derecha o centrados. Sin hamburger en desktop. Sin “Agenda / Número / Blog”.

Home (H-WIZARD)

  1. Titular Instrument Serif: ¿Por dónde entras?
  2. Chips de zona (4) + tipo (2).
  3. CTA Siguiente. Pie “Soy agente”.

No hay portada fotográfica a 60vh. hero.jpg puede vivir en UX-00 Cover, no como hero de producto. [DECISIÓN]

Comparador (F-CMP)

3 columnas caliza, misma retícula: foto 4:3 → dirección → precio → fila m² / planta / gastos → extras. Disclaimer bajo el criterio, sobre las columnas.

Ficha

Cover a ancho de columna (~720px), no full-bleed de marketing. Dirección → precio → datos → cuerpo. Blancos de umbral, no de landing.

Mesa

Lista densa sobre hormigón. No imita el comparador. Es trastienda: filas, badges, CTA óxido. Lucía no necesita “inspiración de marca” para publicar.

7. Motion

SuperficieSpec
Hover columnaJunta grafito; no elevación 8px ni scale 1.04
Cambio de pasoFade 160ms o slide 8px; off si reduced-motion
SkeletonPulso 1.2s en junta; off si reduced-motion
PublicarBadge, no confetti ni “llaves animadas”

Prohibido: parallax en portal, scroll-jacking, page transitions de 400ms, sticker de “NUEVO” parpadeante.

8. Anti-clichés (serie ALS-2)

ClichéPor qué no
Agencia caliza mineralPrimer instinto del brief; vetado
Papel + cadmio kiosco (MARGEN)Editorial, no finca
Botanical / musgo (CESTA)Despensa
Chrome / neón (RONDA)Gaming
Chalky municipal (PIZARRA)Inscripción
Pareja stock con llaves gigantesMentira social del “ya es tuyo”
Lead blur + “te llamamos”Rompe S2 / H2
Iconos outline de casita 2014Chrome tipográfico basta

9. Tono de marca (visual + verbal, puente a doc 09)

Cercano de portal y de reja. Tuteo. Segunda persona cuando habla la UI. Tercera cuando habla la ficha.

El producto se compara y se publica. No vende una hipoteca, no pide visita, no pide el móvil.

10. Criterios de aceptación creativos

  1. Ground de página = #E3E1DC. Cards = #F3F1EC. Nunca al revés.
  2. Óxido en el paso activo y en el CTA, no un header naranja lleno.
  3. Comparador de 3 columnas visible en el primer viewport desktop (tras el criterio).
  4. Ninguna fuente de la lista vetada.
  5. Home reconocible como paso 1, no como hero 2-col de startup.
  6. Success de mesa no celebra una “visita confirmada”.

08-design-system.md

08 — Design system — ZAGUÁN

1. Tokens

--color-concrete: #E3E1DC;   /* G-CONCRETE ground */
--color-graphite: #1F1D1B;
--color-rust:     #B54A2A;
--color-iron:     #6A6560;
--color-limewash: #F3F1EC;   /* cards only — caliza */
--color-joint:    #C9C4BB;
--color-draft:    #6A6560;   /* badge DRAFT */
--color-live:     #B54A2A;   /* badge PUBLISHED */

--font-display: "Instrument Serif", "Iowan Old Style", serif;
--font-ui:      "Sora", system-ui, sans-serif;

--radius-card: 2px;          /* junta de finca, no pill */
--radius-cta:  2px;
--rule-wiz:    1px solid var(--color-joint);
--space:       4 / 8 / 12 / 16 / 24 / 32 / 48 / 64;

Tailwind (referencia):

KeyValor
colors.concrete#E3E1DC
colors.graphite#1F1D1B
colors.rust#B54A2A
colors.iron#6A6560
colors.limewash#F3F1EC
colors.joint#C9C4BB
fontFamily.displayInstrument Serif
fontFamily.sansSora

[COMPROBADO] Hex y tipo = brief.

2. Escala tipográfica

TokenFamiliaSize / lhUso
display-xlInstrument 60056 / 60Titular paso 1 desktop
display-lInstrument 60040 / 44Titular paso mobile · dirección ficha desktop
display-mInstrument 60028 / 32Dirección columna · wordmark
priceSora 60024 / 28Renta €/mes
bodySora 40016 / 26Cuerpo de ficha
uiSora 40016 / 24Form, comparador, lista mesa
metaSora 50013 / 18Zona, planta, gastos
stepSora 60012 / 16 · tracking 0.06em · uppercase1 Zona · 2 Presupuesto · 3 Musts

3. Componentes

Chrome de pasos (S-WIZ)

SpecValor
Alto desktop64–72px
Alto 39056px + “1 de 3”
FondoHormigón
Rule1px junta full-bleed
Paso activoSora 12 · óxido + filete 2px óxido
Paso idleHierro
Paso bloqueadoHierro 50% · no es enlace

Slots: 1 Zona / · 2 Presupuesto /criterio/presupuesto · 3 Musts /criterio/musts.
Wordmark ZAGUÁN → / (reinicia).
Mesa no vive aquí.

Chip de zona / tipo / hab

┌──────────┐
│  Usera   │   idle: caliza + junta
└──────────┘   selected: óxido texto o rule 2px óxido
  • <button type="button"> con aria-pressed.
  • Target ≥44px.
  • No pills 999px: radius 2.

Columna de comparador

┌─────────────────────────────┐
│ cover 4:3                   │
├─────────────────────────────┤
│ Dirección Instrument        │
│ 890 €/mes                   │
│ 68 m² · 3º D · 42 € gast.   │
│ patio                       │
└─────────────────────────────┘
  • Fondo caliza, sin sombra.
  • Toda la columna es un <a> a /fincas/:slug.
  • Hover: rule 1px grafito. Sin lift.
  • Hueco vacío (menos de 3): caliza + junta dashed + “Sin tercera finca”.

Ficha de finca

BloqueSpec
Columnamax-width 720px, centrada
Cover4:3, full de columna
Direccióndisplay-l / display-m
Precioprice + “/ mes” meta
Datosgrid 2×2: m², planta, gastos, extras
Cuerpobody 16/26 · max 70ch
Disclaimer“ZAGUÁN no llama. Si hay visita, la agencia llama.”
Volverlink “← Las tres fincas”

Fila de mesa

ColumnaContenido
Dirección1 línea, truncate
ZonaUSERA / …
Renta890 €
StatusBadge Borrador / Publicado
updatedAtmeta, Europe/Madrid
AcciónPublicar (si DRAFT) · ver

Badge DRAFT: texto hierro, rule junta.
Badge PUBLISHED: texto óxido o caliza sobre óxido ≥14 bold.

Botones

VarianteUsoSpec
PrimarySiguiente, Comparar, Publicar, EntrarFondo óxido, texto caliza, h-44, radius 2
SecondaryAtrás, Guardar borradorRule grafito, texto grafito, fondo hormigón
GhostSoy agente, Reintentar, SalirTexto hierro
DangerNo hay borrado Must

Pending: disabled + aria-busy. No spinner de marca.

Form

Labels visibles (no placeholder-only). Inputs h-44, radius 2, rule junta, foco outline 2px óxido offset 2. Textarea cuerpo min 6 filas. Select de zona, tipo, cover (allowlist).

Tope de renta: input numérico + (Should) slider. El valor se lee en Sora 24.

Toggles de musts: switch o checkbox grande, label a la derecha. OFF = “me da igual”.

Estados

EstadoReceta
Empty compareTitular Instrument 32 + 1 línea Sora + CTA “Afloja un must” → paso 3
Empty mesaTitular + CTA Nueva finca
ErrorBanner junta óxido + texto + Reintentar
LoadingSkeleton 3 columnas caliza; wizard: no skeleton (es form)
404 finca“Esa finca no está publicada.” + link al wizard

4. Grid y breakpoints

ViewportComportamiento
≥1200Wizard max 720; comparador 3 col iguales; mesa tabla
960–1199Comparador 3 col compactas; mesa tabla
600–959Comparador scroll-x snap; mesa lista
~390 (mobile wizard)Un paso por pantalla; chips wrap; comparador scroll-x; mesa apilada

Padding horizontal: 24 desktop, 16 mobile.
Main en 390: padding-bottom 24 (no hay rail inferior).

5. Iconografía

Mínima. El zaguán no es un set de iconos outline de casita.

SitioRecurso
ChromeTexto. Sin icono de “home”.
MustsNada o un trazo 1.5 de 16px (reja / terraza)
EmptyNada o una rule óxido de 32px
StatusTexto de badge, no semáforo

Si hace falta un icono (salir, error): trazo 1.5, 20px, grafito. No pack Remix/Lucide a color.

6. Elevación y borde

SuperficieTratamiento
PáginaHormigón plano
Card / columnaCaliza plana + rule 1px junta
MesaFilas separadas por rule junta
ModalNo hay modal Must

Cero sombras shadow-lg. El portal no flota.

7. Mapa componente → ruta

ComponenteRutas
WizardChromeComponent/, /criterio/presupuesto, /criterio/musts
ZonaChips/
RentField / RoomChips/criterio/presupuesto
MustToggles/criterio/musts
CompareColumn/comparar
FincaArticle/fincas/:slug
DeskRow/mesa
ListingForm/mesa/nueva
EmptyState / ErrorBanner / SkeletonCompare/comparar, /mesa

8. Criterios de aceptación DS

  1. Tokens Tailwind = hex del brief, sin “casi blanco” #FAFAFA como ground.
  2. Cards en caliza; página en hormigón.
  3. Primary CTA nunca en hierro sobre junta (contraste).
  4. Instrument Serif no aparece en labels de form ni en badges.
  5. Recorte de finca aspect-[4/3], no 16:10 editorial ni 1:1.
  6. Targets de chips, pasos y CTA ≥44px.

09-content-guide.md

09 — Guía de contenido — ZAGUÁN

1. Voz

Cercana de portal y de reja. Tuteo en UI. Segunda persona cuando la interfaz habla. Tercera cuando habla la ficha.

Sin jerga de portales (“inmueble premium”, “oportunidad única”, “no te lo pierdas”). Sin sede electrónica. Sin “te llamamos en 5 minutos”.

El producto se compara y se publica. No vende una hipoteca, no pide visita, no pide el móvil.

2. Palabras permitidas / prohibidas

UsarNo usar
Finca / pisoInmueble premium, propiedad, “home”
Criterio / topeOferta, puja, “tu presupuesto vinculante”
Comparar / columnasReservar, pedir visita, “apuntarme”
Publicar / publicadoVisitado, “ya ha venido gente”, “en opción”
BorradorPending review, staging, “oculto al algoritmo”
Mesa / agenciaInbox, CRM, backoffice, captación
Buscador / parejaUsuario, lead, prospecto
AgenteCommunity manager, closer, hunter
ZAGUÁN no llamaTe llamamos, reserva tu slot

3. Microcopy duro (literales irrenunciables)

El brief fija cuatro oposiciones. Aparecen donde el usuario puede confundirse.

OposiciónDóndeCopy
Comparar ≠ reservarComparador sobre las columnas · ficha“Comparar no es reservar. ZAGUÁN no pide ni confirma una visita.”
Criterio ≠ ofertaPaso 2 sobre el tope · resumen del comparador“Esto es un criterio, no una oferta.”
Publicado ≠ visitadoSuccess de mesa · badge de mesa“Publicado no es visitado. El barrio ya puede compararla; nadie ha subido todavía.”
ZAGUÁN no llamaFicha · pie del comparador“ZAGUÁN no llama. Si hay visita, la agencia llama.”

[DECISIÓN] Los cuatro textos son obligatorios en UI. No se diluyen a un tooltip.

4. Microcopy por superficie

SuperficieCopy
EsloganTres fincas. Un criterio.
Línea de distritoUsera · Vallecas · Entrevías · Orcasur
Paso 1 titular¿Por dónde entras?
Paso 1 ayudaElige zona y tipo. Luego el tope.
Paso 2 titular¿Hasta dónde llegas al mes?
Paso 2 disclaimerEsto es un criterio, no una oferta.
Paso 3 titular¿Qué no negocias?
Paso 3 ayudaApagado = te da igual. Encendido = tiene que estar.
Chip CTASiguiente · Comparar estas tres
AtrásAtrás
Card CTA implícitoToda la columna es el enlace; no “Ver más”
Ficha volver← Las tres fincas
Comparador titularTres fincas con tu criterio
Comparador disclaimerComparar no es reservar. ZAGUÁN no pide ni confirma una visita.
Empty comparadorNinguna finca encaja con este criterio. Afloja un must o sube el tope.
Empty mesaAún no hay fincas. Da de alta la primera.
ErrorNo hemos podido cargar las fincas.
Error retryReintentar
Loading (doc 16)Cruzando el zaguán…
Login titularMesa
Login submitEntrar
Login errorEmail o contraseña no valen.
Mesa titularMesa
Mesa CTANueva finca
PATCH PUBLISHEDPublicar
PATCH DRAFTVolver a borrador
Alta títuloNueva finca
Alta submit secundarioGuardar borrador
Alta submit primarioPublicar
Success titularPublicada.
Success cuerpoEl barrio ya puede compararla. Publicado no es visitado. Nadie ha subido todavía.
404 DRAFTEsa finca no está publicada.
Pie wizardSoy agente
SalirSalir
Hueco 3.ª colSin tercera finca con este criterio.

5. Fichas seed (tono)

Las fincas demo se escriben como nota de portal de barrio, no como folleto de obra nueva ni como copy de lujo.

FincaTono
Amparo 14Observación: tercero, patio, sin ascensor, luz de cocina. Sin “luminoso y acogedor”.
Albufera 212Quinto, terraza a la avenida, ascensor. Sin “espectacular”.
Yeserías 8Bajo, mascotas, patio. Sin atacar a una comunidad real.
Sicilia 9Reformado, Entrevías, tres hab. El precio se dice; no se justifica.
Alegría 3Ático, una hab, terraza, sin ascensor. Orcasur.
DRAFT Merced 2Oficio: sin foto aún. Invisible en público.

No inventar declaraciones de vecinos reales. Si hay una voz, es de la ficha (neutra) o de persona de diseño etiquetada.

6. Nombres propios del seed

NombreUso
Marcos VidalBUSCADOR; no deja PII en producto
Lucía FerrerAGENTE; lucia@zaguan.barrio
Inés RoldánPROPIETARIA del 3º de Amparo; no es cuenta
Marcelo UseraLocal de atrezzo de Lucía; no se afirma como negocio real

No inventar CIF, número de colegiada, ni precios oficiales de comunidad. [SUPUESTO de catálogo demo]
Rentas seed: 890 · 980 · 820 · 1050 · 760 · 870. No se afirman como mercado 2026.

7. Tono por estado (mesa)

StatusFrase mesaFrase que Lucía podría decir (fuera de app)
DRAFTEl barrio no lo ve“Aún no. Está en la mesa.”
PUBLISHEDEn el comparador“Ya está. No, nadie ha subido todavía.”

La app no envía esos mensajes en v1. El copy solo prepara el habla.

8. Capitalización y números

  • Wordmark: ZAGUÁN en versales Instrument Serif (con tilde).
  • Zonas: Usera, Vallecas, Entrevías, Orcasur (con mayúscula).
  • Renta: 890 €/mes (espacio ante €, 24h no aplica).
  • Superficie: 68 m² (espacio, superíndice).
  • Planta: 3º D · bajo · ático (minúscula en común).
  • Gastos: 42 € comunidad.
  • No mezclar “€890/mo” estilo US.

9. Idioma

es-ES. Fácil lectura. EN no en v1.
No mezclar “listing” en UI. En API sí: listings, maxRent.

10. Accesibilidad de copy

  • No transmitir estado solo con color (“el óxido significa publicado”).
  • Badge lleva texto: Borrador, Publicado.
  • Alt de fotos: descriptivo de la finca, no “imagen1”.
    • hero: “Zaguán de finca de barrio, reja y hormigón.”
    • amparo: “Fachada y patio de Calle Amparo 14, Usera.”
    • albufera: “Bloque en la avenida de la Albufera, terraza en quinto.”
    • yeserias: “Bajo en el paseo de Yeserías, umbral a la calle.”
    • sicilia: “Portal de Calle Sicilia 9, Entrevías.”
    • alegria: “Ático con terraza en Calle de la Alegría, Orcasur.”

11. Piezas que no se escriben

NoMotivo
“Te llamamos en 5 minutos”S2 / H2 / H3
“Reserva tu visita” / “Pedir cita”H2 · Comparar ≠ reservar
“Ya ha venido gente” como hecho de publicaciónPublicado ≠ visitado
“Has ofertado 950 €”Criterio ≠ oferta
Ranking de los más vistosLikes, otro producto
Lorem / “lorem ipsum piso”Prohibido en el case
Declaraciones inventadas de una agencia realVerdad de catálogo

10-accessibility.md

10 — Accesibilidad — ZAGUÁN

Objetivo: WCAG 2.2 AA. No se declara conformidad legal certificada.
Este documento cubre la UI (wizard, comparador, ficha, mesa). No audita la accesibilidad física de las fincas seed (ascensor, umbral, baño). [SUPUESTO]

1. Decisiones

TemaDecisión
Tipo UISora ≥16px body
DisplayInstrument Serif no se usa por debajo de 20px
ContrasteGrafito #1F1D1B sobre #E3E1DC / #F3F1EC
CTA óxidoTexto caliza sobre #B54A2A
Badge PUBLISHEDTexto caliza ≥14px bold o óxido sobre caliza
FocoOutline 2px óxido, offset 2px, visible (no outline-none global)
WizardChips son <button> con aria-pressed; pasos son <nav>
Comparador desktop3 <article> o 3 <a> en grid; no tabla de 8 columnas
Comparador móvilScroll-x snap; cada columna ≥280px; no apilar datos ilegibles
Imágenesalt = dirección de la finca
FormLabels visibles, no placeholder-only
ErroresTexto, no solo color
MobileTargets ≥44px (chips, Siguiente, toggles)
MovimientoFade 160ms; prefers-reduced-motion: reduce → 0
Live regionsaria-live="polite" en empty/error del comparador

2. Contraste (comprobación de diseño)

ParUsoNota
#1F1D1B / #E3E1DCBody sobre hormigónPasa AA y AAA cuerpo
#1F1D1B / #F3F1ECBody sobre calizaPasa AA y AAA
#F3F1EC / #B54A2ACTA / PUBLISHEDPasa AA para texto ≥14px bold
#6A6560 / #E3E1DCMeta hierroVerificar ≥13px; no usar en 11px
#6A6560 como texto 12px sobre juntaProhibido para body
Paso idle hierro 50%Solo si no es el único indicador+ “2 de 3” textual

3. Teclado

FlujoOrden
Paso 1Wordmark → chips zona → tipo → Siguiente → Soy agente
Paso 2Wordmark → pasos → tope → hab → Atrás / Siguiente
Paso 3Toggles → Atrás / Comparar
ComparadorResumen criterio → col 1 → col 2 → col 3
FichaVolver → contenido
LoginEmail → password → submit
MesaNueva → filas (Publicar / ver) → Salir
  • Enter en chip = selecciona (aria-pressed).
  • Enter en columna = navegar a ficha.
  • Escape no cierra el wizard (no es modal).
  • Focus trap: no hay modal Must.

4. Semántica

PiezaMarkup
Chrome pasos<nav aria-label="Criterio"> · activo aria-current="step"
Chips zonarole="group" + aria-label="Zona"
Tope<label for="maxRent"> + input type="number"
Musts<fieldset> + <legend> “Qué no negocias”
Comparador<section aria-label="Tres fincas">
Columnaun <a> por finca; hueco vacío no es enlace
DisclaimerNo es alert; es texto normal + borde
Precio890 € al mes anunciado con unidad, no solo “890”
Badgestexto visible; no solo icono
Skeletonsaria-busy="true" en el contenedor; aria-hidden en placeholders

5. Lector de pantalla — copy

SituaciónAnuncio
Chip“Usera, pulsado” / “Vallecas, no pulsado”
Columna“Calle Amparo 14, 3º D, 890 euros al mes, 68 metros, tercero sin ascensor, 42 euros de comunidad, patio”
Empty“Ninguna finca encaja con este criterio.”
ErrorAnuncio en live region; foco al banner
Success“Publicada. Publicado no es visitado.”

6. Producto vs finca física

Lo que ZAGUÁN puede hacerLo que no afirma
Decir si hay ascensor, planta, m²Que el 3º de Amparo sea accesible de hecho
Wizard usable con teclado y zoom 200%Que el local de Marcelo Usera tenga rampa
Extra “mascotas OK”Condiciones de la comunidad real

[DECISIÓN] v1 no incluye auditoría a11y de sede. El campo elevator es dato de catálogo, no un certificado.

7. Riesgos

RiesgoMitigación
3 columnas en 390UI mobile: scroll-x snap; no comprimir a 100px
Instrument Serif en meta 12pxProhibido; meta = Sora 13
Óxido como único estado de paso+ aria-current="step" + “2 de 3”
Foto sin altChecklist seed: 5 alts + hero
Toggle must leído como “sin X” cuando está OFFCopy: “apagado = te da igual”
Slider de renta sin inputInput numérico es la fuente; slider es Should

8. QA a11y (mínimo)

#PruebaPasa si
1Teclado paso 1 → 2 → 3 → comparar → fichaSin trampa; foco visible
2Teclado login → mesa → PATCHAcciones alcanzables
3Zoom 200% móvil 390Chips usables; no solapa inputs
4Lighthouse a11y ≥ 90 en /Sin contrast fails de tokens
5VoiceOver/NVDA: columna anunciadaPrecio y planta comprensibles
6prefers-reduced-motionSin stagger ni slide
7Contraste badges DRAFT / PUBLISHEDTexto bold ≥14

9. Criterios de aceptación

  1. Ningún control del wizard o CTA mide menos de 44×44 CSS px.
  2. El disclaimer no depende del color óxido para ser entendido.
  3. Las 5 fotos seed + hero tienen alt no vacío y no genérico.
  4. El foco no se pierde al reintentar un error de red.
  5. En 390 el comparador no obliga a leer 3 columnas a la vez: hay snap.
  6. Un DRAFT no se anuncia en el comparador.

11-privacy-security.md

11 — Privacidad y seguridad — ZAGUÁN

1. Datos que existen

DatoDóndeBase / nota
Email y hash de la agenteusersContrato interno de mesa
Fincas (dirección, renta, fotos)listings (público si PUBLISHED)Catálogo de producto; seed demo
Criterio de compareBody de POST; comparisons opcionalAnónimo; no se liga a persona
Sesión JWTAuthorization + sessions?Solo AGENTE

No se pide cuenta al buscador. No hay teléfono, email ni nombre de Marcos en producto. No hay tracking de menores. Las fotos seed no identifican vecinos reales.

2. Minimización

Qué no se recoge en v1Por qué
Teléfono / email del buscadorS2 / H3 · no lead
DNI / renta acreditadaCriterio ≠ oferta; no hay scoring
Geolocalización del dispositivoZona es un chip, no GPS
Historial de visitas a fincas físicasPublicado ≠ visitado
Fotos subidas por el buscadorNo hay UGC

Comparison persistida (si se activa) guarda criterio + slugs, no IP como identidad. [DECISIÓN] Si se persiste, retención corta (p. ej. 30 días) en L+1; v1 puede ser efímera.

3. Seguridad L1

ControlSpec
AuthJWT Bearer · role === AGENT
Passwordbcrypt cost 12; nunca en response
DRAFTNunca en GET /api/listings ni :slug (404)
EscrituraPOST / PATCH listings y GET mesa → 401 sin token
Rate limit/auth/login (anti stuffing; Should si no llega al slice)
CORSOrigen http://localhost:4200 en local
SecretosSolo .env (no git)
Cover allowlistFilenames seed; no URL arbitraria en POST
next post-loginPaths relativos allowlist (/mesa, /mesa/nueva)

No se declara cumplimiento RGPD definitivo. Sí: minimización y derecho a borrar la cuenta de agente (manual L1).

4. Superficies de riesgo

RiesgoMitigación v1
Enumeración de slugs DRAFT404 idéntico a “no existe”; no 403
JWT en localStorageAceptable en demo; no datos de buscador al lado
Criterio en sessionStorageNo es PII; se limpia al wordmark → /
Leftover pieces / cuentas MARGEN en DBI01 de build: schema propio, no mezclar
Fotos de interiores con personasSeed: fachadas / umbrales; no caras identificables

5. Roles y autorización (recordatorio)

RecursoAnónimoAGENT
GET listings / compare / :slug PUBLISHED
GET :slug DRAFT404404 (público); mesa sí
GET mesa / POST / PATCH401
LoginSí (emite token si vale)

Inés y Marcos no tienen fila en users.

6. Criterios de aceptación

  1. Response de login no incluye password ni password_hash.
  2. GET público de merced-2 es 404.
  3. GET /api/mesa/listings sin Bearer es 401, no 200 [].
  4. No hay formulario público que pida teléfono o email del buscador.
  5. .env no se commitea.

12-analytics.md

12 — Analítica — ZAGUÁN

North star: % de sesiones que llegan a /comparar con ≥1 columna y abren ≥1 ficha (compare_view + listing_open).

Instrumentación modelo. No hay proveedor conectado en v1 salvo que el build añada un logger local. [DECISIÓN] No se inventan cifras de conversión.

1. Eventos

EventoCuándoProps mín.
wizard_stepCompleta paso 1 / 2 / 3step, zona?, maxRent?
compare_viewRender de /compararmatches (0–3)
listing_openApertura de fichaslug
compare_empty0 matchescriteria
agent_loginJWT ok
listing_publishDRAFT → PUBLISHEDslug
error_bannerUI-11surface

2. Qué no se mide

No eventoMotivo
signup_click / lead_submitNo existe CTA
visit_bookedComparar ≠ reservar
offer_sentCriterio ≠ oferta
like / favoriteS4
Identidad de MarcosH3 · anónimo

Hipótesis H2: compare_view sin visit_click (el CTA no existe). Si en un test de 5 usuarios alguien busca el botón, se anota en research futuro, no como KPI inventado.

3. Funnel modelo (BUSCADOR)

paso 1  →  paso 2  →  paso 3  →  compare_view  →  listing_open

Activación = compare_view con matches ≥ 1.
Completud = listing_open en la misma sesión.

AGENTE: agent_loginlisting_publish.

4. Integridad (producto, no vanidad)

SeñalCómo se observa
DRAFT no filtraGET /listings sin merced-2 (QA, no analytics)
Expectativa de visitaComentarios cualitativos; cero evento de reserva
Salud4xx/5xx de listings/compare

5. Privacidad de analítica

Sin PII. Criterio se puede agregar (zona=USERA, maxRent en buckets de 50 €). No se guarda IP como usuario. Ver doc 11.

6. Criterios de aceptación (si se instrumenta)

  1. Los 7 eventos de §1 tienen nombre estable.
  2. No se dispara ningún evento de lead / visita / oferta.
  3. Un bloqueador de cookies no rompe el wizard (analítica no es Must de runtime).

13-qa-test-plan.md

13 — Plan de pruebas — ZAGUÁN

1. Smoke obligatorio (D-P1-06)

#AcciónEsperado
1GET /api/listings200 · length = 5 · slugs PUBLISHED presentes · ningún DRAFT merced-2
2GET /api/listings/amparo-14200 · address/rent/sqm/cover amparo.jpg
3POST /api/auth/login lucia@zaguan.barrio / password123200 · accessToken · user.role=AGENT
4GET /api/mesa/listings con Bearer200 · incluye DRAFT merced-2
5ng build o serve webWeb arranca o build OK

[COMPROBADO] Smoke = listings + ficha + login JWT + mesa.

2. Casos funcionales

IDCasoEsperado
S1Slugs seed publicadosGET listings incluye amparo-14, albufera-212, yeserias-8, sicilia-9, alegria-3
S2DRAFT no sale en públicomerced-2 ausente del array público
S3Slug DRAFTGET /api/listings/merced-2 404
S4Slug inexistente404
S5Compare MarcosPOST { zona: USERA, tipo: ALQUILER, maxRent: 950, rooms: 2 } · incluye amparo-14 y yeserias-8 · rent ≤ 950 · ≤ 3 items
S6Compare UI/comparar muestra columnas (precio, m², planta, gastos, extras) y disclaimer; cero CTA Reservar
S7Ficha ACdirección, precio, planta, gastos, “ZAGUÁN no llama”
S8Wizard es la home/ es paso 1 (zona+tipo); no hero 2-col
S9Mesa sin JWT401 / redirect login
S10POST listing201 · status=DRAFT · slug kebab
S11PATCH PUBLISHED200 · la finca aparece en GET listings
S12PATCH DRAFT (retirar)200 · desaparece de público · slug público 404
S13Login malo401 · mensaje en form
S14Empty compare (VENTA o tope 400)UI-10
S15API caídaUI-11 + Reintentar conserva la vista
S16LoadingSkeleton 3 cols antes del primer paint de compare
S17Mobile 390Wizard 3 pasos + compare snap; CTA ≥44px; no hero 2-col
S18Fotos seed5 covers resuelven a assets/listings/*
S19Password no viaja de vueltalogin response sin hash
S20Body POST listingacepta address, zona, rent, sqm, rooms, musts, body
S21Success/mesa/ok/:slug copy “Publicado no es visitado”
S22Público no lista mesaGET mesa sin token 401 (no 200 [])
S23Query GET listings?zona=USERA&maxRent=950 no devuelve sicilia-9 (1050)
S24Paso 2 sin zonaredirect /
S25Microcopy ×4comparar≠reservar · criterio≠oferta · publicado≠visitado · no llama

3. Datos de prueba

UsoValor
AGENTElucia@zaguan.barrio / password123
BUSCADORMarcos Vidal · sin cuenta
Finca felizamparo-14
Finca DRAFTmerced-2
Compare felizUSERA · ALQUILER · 950 · 2 hab · musts OFF
Compare vacíoVENTA, o maxRent 400

4. Regresión de diversidad / craft

CheckFalla si
Home es wizardHay hero 2-col + 3 cards de marca (AP-12) o lookbook (MARGEN) o tabla (CESTA)
Shell es S-WIZHay masthead de periódico, rail, dock flotante o command search como nav primaria
Copy honestoSuccess dice “ya hay visitas” o compare dice “reserva” o paso 2 dice “tu oferta”
PaletaSe cuelan cadmio MARGEN / musgo CESTA / cromo RONDA / chalky PIZARRA
TipoCormorant / Public Sans / Newsreader / Outfit / Spectral / Manrope / Zilla / Mulish / Lora / Karla
RutasAparecen /visita, /agenda, /piezas, /necesidades, /inbox
GroundPágina en caliza #F3F1EC en vez de hormigón #E3E1DC

5. Hipótesis (no se “prueban” en la demo)

H1, H2, H3 requieren uso real. El QA verifica que existen las superficies que permitirían medirlas (3 pasos, 3 columnas, disclaimer de reserva, ausencia de signup buscador, DRAFT 404).

6. Criterios de salida QA v1

  • Smoke §1 en verde.
  • S2, S3, S5, S6, S8, S11, S17, S21, S22, S25 en verde.
  • Cero blockers de copy deshonesto (reservar / ofertar / te llamamos / visitado).
  • Sin .env en el repo.

7. Fuera de este plan

Playwright e2e completo, carga, fuzzing, auditoría de una finca o agencia reales.

14-dev-handoff.md

14 — Handoff desarrollo — ZAGUÁN

1. Arranque

cd /Users/cristian/orca/zaguan-app

# apps independientes (npm --prefix o pnpm --filter)
# apps/api/.env
#   DATABASE_URL=  # Neon curly-recipe-23023272
#   JWT_SECRET=
#   PORT=3018

# migrate / ensureSchema + seed
# api → http://localhost:3018
# web → http://localhost:4200

Demo: lucia@zaguan.barrio / password123.

2. Paridad Paper

Debe verse en AngularArtboard
Tokens rusted + Instrument Serif / SoraUI-00
Wizard paso 1 zonaUI-01
Wizard paso 2 presupuestoUI-02
Wizard paso 3 mustsUI-03
Comparador 3 colsUI-04
Ficha + “no llama”UI-05
Login AGENTEUI-06
Mesa DRAFT / PUBLISHEDUI-07
Alta de fincaUI-08
Success “publicado ≠ visitado”UI-09
Empty / errorUI-10, UI-11

Loading (skeleton 3 cols) y mobile wizard (~390) se implementan sobre las mismas rutas; spec en docs 16–17.

No clonar MARGEN (lookbook + masthead), CESTA (tabla + rail), RONDA (feed + dock), CANTERA (parte + dock) ni PIZARRA (command search + inscripción).

Paper se construye en paralelo; los nombres de artboard son el inventario planificado (doc 00).

3. Contratos

TemaContrato
Login{ accessToken, user } camelCase (no access_token)
ListadoGET /api/listings → PUBLISHED; query zona, maxRent, rooms, elevator, terrace, pets, renovation
ComparePOST /api/compare{ criteria, listings } (≤3)
FichaGET /api/listings/:slug · 404 si DRAFT
MesaGET /api/mesa/listings Bearer
AltaPOST /api/listings campos de doc 05
StatusPATCH /api/listings/:id { status }
Status enumDRAFT | PUBLISHED
Zona enumUSERA | VALLECAS | ENTREVIAS | ORCASUR
Tipo enumALQUILER | VENTA
RoleAGENT (no EDITOR)

Campos JSON en camelCase hacia el web aunque SQL sea snake_case. [DECISIÓN]

4. Mapa de rutas web

PathGuard clientePágina
/noWizard paso 1
/criterio/presupuestosoft (zona)Paso 2
/criterio/mustssoft (tope)Paso 3
/compararsoft (criterio)Comparador
/fincas/:slugnoFicha
/loginnoLogin
/mesasoft (redirect)Mesa
/mesa/nuevasoftAlta
/mesa/ok/:slugsoftSuccess

Soft guard público: si falta el paso anterior, /.
Soft guard mesa: si no hay token, /login?next=. La API es la autoridad.

[COMPROBADO] Solo estas rutas. No implementar /visita, /agenda, /piezas, /mesa/:id.

5. Assets

Copiar desde el case:

2026-08-18-zaguan/assets/hero.jpg
2026-08-18-zaguan/assets/listings/amparo.jpg
2026-08-18-zaguan/assets/listings/albufera.jpg
2026-08-18-zaguan/assets/listings/yeserias.jpg
2026-08-18-zaguan/assets/listings/sicilia.jpg
2026-08-18-zaguan/assets/listings/alegria.jpg
  → apps/web/public/assets/listings/   (hero a public/assets/)

cover del seed = filename (amparo.jpg, …).

Quitar leftover public/assets/{merienda,colegio,piscina,biblioteca}.jpg (semanario MARGEN) si sigue en el repo.

6. DoD implementación L1

CheckOK
API sirve :3018
Seed 1 AGENTE + 5 PUBLISHED + 1 DRAFT
Smoke GET listings + GET slug + login + GET mesa
POST compare devuelve ≤3 PUBLISHED
JWT protege mesa / POST / PATCH
DRAFT 404 en público
Web wizard 3 pasos + compare + ficha + mesa + alta + success
Microcopy ×4
Tokens Tailwind = paleta brief
Home es paso 1 (no leftover lookbook / tabla / feed)
ng build o equivalente OK
README del repo app con credencial

7. No hacer

ProhibidoMotivo
npm workspacesAP-06
Commit de .envSecreto
Prometer visita o llamadaMicrocopy duro / AP-09
Estado SEATED / ENROLLED / CONTACTED / RECEIVEDVocabulario de otros días
Role EDITOREso es MARGEN
Search bar o mapa como homeRompe H-WIZARD
Hero 2-col + 3 cardsAP-12
Lookbook + mastheadMARGEN
Tabla de necesidades como homeCESTA
Feed + dockRONDA
image_gen como captura de productoD-P0-09
Body POST con kicker / issueId / needSlugLeftover

8. Contactos de diseño (artefactos)

ArtefactoPath
Briefux-projects/2026-08-18-zaguan/docs/00-day-brief.md
Paperhttps://app.paper.design/file/01M09V13JEANPFFD6DS1RCN29G
Datosdocs/05-data-model.md
Flujosdocs/04-user-flows.md
IXdocs/16-interaction-specs.md
Builddocs/20-implementation.md

15-roadmap.md

15 — Roadmap — ZAGUÁN

Este documento no es un cajón de trabajo L1 a medias.
El vertical slice L1 del brief está especificado como done-scope (docs 01, 18, 19).
Aquí solo viven ideas de L+1 / L3 y pulido posterior.

1. Hecho en L1 (esta ejecución documental)

EntregaEvidencia
Definición producto + ternadocs 00-brief, 01
Personas Marcos / Lucía / Inés, JTBD, journeydocs 02
IA wizard-first + chrome S-WIZdocs 03
Flujos F-CMP + mesa AGENTEdocs 04
Modelo users / listings / comparisons + seeddocs 05
Stack Angular/Nest/Neon/JWT :3018docs 06, 20
Dirección rusted + DSdocs 07, 08
Copy Comparar ≠ reservar · Criterio ≠ oferta · Publicado ≠ visitado · ZAGUÁN no llamadocs 09
Paper ref 12 UX + 12 UIdocs 00-paper-reference
Suite 00–20 + README + executivecase folder

2. Implementación L1 (misma complejidad, código)

Contrato en docs 14 y 20. No es deuda silenciosa del case documental: es el build del repo zaguan-app. El alcance L1 ya está cerrado en spec; el código alinea leftover (issues / pieces / events) al contrato.

Eso no se reabre aquí como “próximo trimestre de producto”.

3. L+1 (misma familia, subida de pulido)

ÍtemNotas
Persistencia estable de Comparison + replayHome sigue siendo el paso 1
Preview de DRAFT con tokenNo 200 público
Relajar musts con mismatch badgesSigue sin inventar fincas
Rate limit loginAnti stuffing
Playwright S1–S25CI
Despublicar con confirmPATCH DRAFT
API URL por environmentDeploy
prefers-reduced-motion verificadoa11y
Foto para merced-2 cuando se publiqueAllowlist

Estas son subidas de pulido, no parches del L1 documental.

[DECISIÓN] No dejar backlog del vertical slice como “mañana”. Lo de arriba es L+1.

4. L3 (requiere brief nuevo de diversidad)

  • Multi-agencia / varios locales
  • Cuenta BUSCADOR y favoritos
  • Calendario de visitas / F-BOOK
  • Lead “te llamamos”
  • Pasarela / señal / contrato
  • Mapa como home
  • Search-first (rompería terna)
  • CRM de captación (40 estados)

5. Explícitamente fuera (no backlog disfrazado)

ÍtemPor qué
Reserva / slot / confirmación de visitaH2 · ≠ F-BOOK
Lookbook editorial / agenda≠ MARGEN
Tabla de necesidades / compromiso≠ CESTA
Parte de obra / wizard de oficio≠ CANTERA
Feed + dock≠ RONDA
App nativaFuera de stack daily
Hero 2-col + 3 cardsAP-12

6. Orden de ataque si hay continuidad de código

  1. Contratos 05–06 + seed 05 + smoke 13 (alinear leftover)
  2. Web paridad UI-00…11
  3. Preview token + mismatch badges (L+1)
  4. Rate limit si hay tráfico
  5. Evaluar brief L3 (no parche silencioso)

16-interaction-specs.md

16 — Especificación de interacción — ZAGUÁN

1. Convenciones

TokenValor
Duración corta120–160ms
Easingease-out
Reduced motion0ms
Target≥44×44
Pendingcontrol disabled + aria-busy

Loading y mobile wizard (variante 390 de UI-01…03 y skeleton de UI-04) viven en este doc; no tienen artboard extra en el inventario de 12. [DECISIÓN]


2. Chrome de pasos (S-WIZ)

GestoResultado
Tap wordmark/ ; reinicia criterio (confirm no Must)
Tap 1 Zona/ ; aria-current="step"
Tap 2 si hay zona/criterio/presupuesto
Tap 2 sin zonano navega; el paso se ve bloqueado
Tap 3 si hay tope/criterio/musts
Tap 3 sin topeno navega
Scroll del pasoChrome puede ser sticky (Should); no se esconde
TecladoTab: wordmark → pasos alcanzables → contenido

Estado activo: label óxido + filete 2px óxido bajo el ítem.
Estado default: hierro.
Estado bloqueado: hierro 50%, no es enlace.

Desktop: pasos a la derecha o centrados.
390: wordmark + “1 de 3 · Zona”. Los tres labels caben si se compactan a 1 / 2 / 3.


3. Paso 1 · zona + tipo (UI-01 · mobile variante)

GestoResultado
Tap chip zonaaria-pressed único (radio); persiste zona
Tap tipoALQUILER / VENTA; default ALQUILER
Tap Siguientesi hay zona+tipo → paso 2; si no, inline
Tap Soy agente/login
Primera cargaForm visible; no skeleton de marca

AC: chips y titular están en el viewport inicial a 390 y a 1280 (sin hero de 60vh).

390: chips en wrap 2×2; CTA full-width h-44 al fondo del paso, no tapado por teclado (el paso 1 no abre teclado).


4. Paso 2 · presupuesto (UI-02)

GestoResultado
Input topedígitos; suffix “€/mes”; disclaimer visible
Chip 1 / 2 / 3+radio rooms
Slider (Should)sincroniza el input; no sustituye al input
Siguientevalida maxRent ≥ 500 y rooms
Atrás/ conservando zona

El valor del tope nunca se etiqueta “oferta” ni “puja”.


5. Paso 3 · musts (UI-03)

GestoResultado
ToggleON = filtro duro; OFF = no filtra. aria-checked
CompararPOST /api/compare/comparar (pending en el botón)
Atrás/criterio/presupuesto
Error de red en POSTbanner UI-11 en el propio paso; no navegar

Doble tap Comparar: ignorado mientras pending.


6. Comparador (UI-04) + loading

GestoResultado
Primera cargaLoading: 3 skeletons caliza 4:3 + 4 barras junta; aria-busy
200 con 1–3columnas; huecos dashed si < 3
200 con 0UI-10 empty + CTA “Afloja un must” → paso 3
Error 5xx / redUI-11 + Reintentar (repite POST)
Tap columna/fincas/:slug (columna completa clicable)
Tap hueco vacíonada
Hover desktopRule 1px grafito; no elevación
Pull-to-refreshNo en v1

AC: precio, m², planta, gastos, extras visibles en cada columna sin hover. Disclaimer “Comparar no es reservar” sobre el fold desktop.

390: scroll-x scroll-snap-type: x mandatory; cada columna ~82vw; dots o “1/3” Sora 12. Criterio resumen sticky arriba.

Orden visual = score del API. No “destacados” por likes.


7. Ficha de finca (UI-05)

GestoResultado
ScrollLectura normal; sin scroll-jacking
Tap ← Las tres fincas/comparar (criterio intacto)
FotoNo lightbox v1
Selección de textoPermitida (anotar dirección es backstage)

Sin doble tap like. Sin “llamar ahora”. Sin WhatsApp click-to-chat.


8. Login (UI-06)

GestoResultado
SubmitPOST login; pending en botón
200Guarda token + user; navega next sanitizado o /mesa
401Texto bajo el form; password no se limpia
EnterSubmit

next permitido: paths que empiezan por / y no // ni http. Allowlist: /mesa, /mesa/nueva.


9. Mesa (UI-07)

GestoResultado
Sin tokenRedirect login
Tap Nueva finca/mesa/nueva
Tap Publicar en fila DRAFTPATCH PUBLISHED; badge cambia
Tap título[DECISIÓN] no hay /mesa/:id; el título no navega; “ver” (link a ficha si PUBLISHED) sí
EmptyTitular + CTA Nueva
Error 401 mid-sessionRedirect login
Error redBanner + Reintentar

No swipe-to-archive. No kanban.

Optimistic UI en Publicar: badge pasa a Publicado; si PATCH falla, rollback + toast error.

Publicar no anima unas llaves ni un “+1 visita”.


10. Alta de finca (UI-08)

CampoInteracción
address / titletext
zonaselect 4 valores
tiposelect ALQUILER / VENTA
rent, sqm, rooms, expensesnumber
floorLabeltext (3º D)
musts4 checkboxes
coverselect filenames seed (preview 4:3); opcional en DRAFT
bodytextarea 6+ filas
GestoResultado
Guardar borradorPOST DRAFT; redirect /mesa o /mesa/ok/:slug con copy de borrador
PublicarPOST + PATCH PUBLISHED → /mesa/ok/:slug
Submit inválidoFoco al primer campo error; no POST
Error redBotón Reintentar; no limpia campos
Doble tapIgnorado mientras pending

Submit deshabilitado si address < 4, body < 40, rent vacío o zona vacía.

[DECISIÓN] Dos botones visibles. Primario = Publicar. Secundario = Guardar borrador. El primario no se llama “Poner en visita”.


11. Success (UI-09)

GestoResultado
CTA primarioVer la finca pública /fincas/:slug
CTA secundarioVolver a la mesa /mesa
CopyVisible “Publicado no es visitado”

Sin confetti. Sin “¡ya hay visitas!”.


12. Empty (UI-10) · Error (UI-11) · Loading · Mobile

EstadoInteracción
Empty compareTitular + 1 línea + CTA a paso 3 (y secundario a paso 2)
Empty mesaTitular + CTA Nueva
ErrorBanner + Reintentar; foco al banner; no pierde criterio ni form
Loading compare3 skeletons; no spinner centrado de marca
Loading mesa5 filas junta
Mobile 390 wizardUn paso = una pantalla; CTA h-44; chrome “N de 3”
Mobile 390 comparesnap-x; no 3 cols de 100px
Mobile 390 fichacover 4:3 full; disclaimer visible

13. Motion

SuperficieSpec
Cambio de pasoFade 160ms o translateX 8px (off si reduced-motion)
Columnas compareFade 160ms, stagger ≤30ms (off si reduced-motion)
Cambio de badgeCrossfade 120ms
ChromeSin hide-on-scroll v1
SkeletonsEstáticos o pulso 1.2s opacity 0.6–1; off si reduced-motion

Prohibido: scroll-jacking, page transitions de 400ms, parallax de portal, like animation, llaves girando.


14. Criterios de aceptación de interacción

  1. Un BUSCADOR en 390px llega de paso 1 a comparar en 3 taps de CTA (Siguiente ×2 + Comparar).
  2. Un AGENTE en 390px publica un DRAFT en ≤ 3 taps desde mesa (acción en fila).
  3. Ningún submit permite doble POST.
  4. El chrome no cubre el H1 del paso (spacer si sticky).
  5. Reduced-motion elimina stagger y slide.
  6. La home no es hero 2-col (AP-12).
  7. En 390 el recorte 4:3 de la primera columna es visible (snap).
  8. /comparar no presenta ningún control de reserva ni “te llamamos”.
  9. El loading de compare es skeleton de 3 columnas, no un logo.
  10. Paso 2 muestra “criterio, no una oferta” sin scroll en desktop.

17-prototype-map.md

17 — Mapa de prototipo — ZAGUÁN

Paper no es clicable vía MCP. El prototipo vivo es la app Angular.
Los artboards de Paper se construyen en paralelo; esta tabla es el contrato de paridad.

RutaPaperRol
/UI-01 (+ mobile 390, spec 16)BUSCADOR
/criterio/presupuestoUI-02BUSCADOR
/criterio/mustsUI-03BUSCADOR
/compararUI-04, UI-10, UI-11 (+ loading, spec 16)BUSCADOR
/fincas/:slugUI-05BUSCADOR / Inés
/loginUI-06AGENTE
/mesaUI-07AGENTE
/mesa/nuevaUI-08AGENTE
/mesa/ok/:slugUI-09AGENTE

Proceso: UX-00…UX-11. Tokens: UI-00.

Loading y mobile no tienen board propio en el inventario de 12: se prototipan como estados / variantes de UI-01 (390) y UI-04 (skeleton). Ver doc 16.

Recorrido demo (guión)

  1. Abrir / — paso 1 ¿Por dónde entras?; chips Usera / Vallecas / Entrevías / Orcasur.
  2. Elegir Usera + Alquiler → Siguiente.
  3. Paso 2: tope 950 €, 2 hab. Leer “Esto es un criterio, no una oferta.”
  4. Paso 3: musts OFF → Comparar estas tres.
  5. /comparar — columnas Amparo 14 (890) y Yeserías 8 (820); tercera hueco o vacía. Leer “Comparar no es reservar.”
  6. Tocar Calle Amparo 14, 3º D — ficha, patio, “ZAGUÁN no llama.”
  7. Comprobar que no hay botón Reservar ni Te llamamos.
  8. /loginlucia@zaguan.barrio / password123.
  9. /mesa — ver DRAFT Calle de la Merced 2.
  10. Nueva finca o Publicar el DRAFT → /mesa/ok/:slug — “Publicado no es visitado.”
  11. Volver a comparar en otra sesión: el DRAFT no está; lo publicado sí.

Estados en el mismo prototipo

EstadoCómo provocarlo
Empty comparetipo VENTA, o maxRent 400, o musts imposibles (ascensor+terraza+reforma+mascotas en Usera 2 hab 950)
Empty mesalista vacía autenticada
ErrorAPI caída / 5xx
Loadingthrottle de red en POST compare
404 DRAFT/fincas/merced-2 sin publicar
Mobile wizard390 × 844 en /, /criterio/presupuesto, /criterio/musts
Mobile compare390 × 844 en /comparar (snap-x)
Hueco 3.ª colcriterio Marcos (2 matches)

Fuera del prototipo clicable

  • Paper artboards son estáticos.
  • Llamada de Lucía a Inés / juego de llaves (backstage).
  • Visita en el zaguán físico.
  • Reserva de visita (no existe).
  • Lead / teléfono.
  • Mapa.
  • Portal multi-agencia.

18-completeness-audit.md

18 — Auditoría de completitud — ZAGUÁN (2026-08-18)

1. Alcance auditado

Vertical slice L1: wizard de criterios + comparador de 3 fincas + ficha + mesa AGENTE JWT, con docs, referencia Paper, contratos Angular + Nest + Neon.

Este encargo cubre la suite documental. Paper hi-fi y el repo zaguan-app pueden existir en paralelo; la auditoría marca lo que esta suite cierra. El day brief no se ha sobrescrito.

2. Checklist CRON / ALS-2

RequisitoEstadoEvidencia
Diversidad sector/tipo/nivelOKInmobiliaria de barrio L1; no galería; no tabla; no feed; no parte de obra
Terna S/H/F 0 códigos = N−1 · ≥2 vs N−2OKS-WIZ · H-WIZARD · F-CMP vs MARGEN S-TOP · H-GALLERY · F-STORY
Day brief + anti-patronesOKdocs/00-day-brief.md (no sobrescrito)
Paper ≥12 UX + ≥10 UIOKUX-count 12 · UI-count 12 · file 01M09V13JEANPFFD6DS1RCN29G
Docs 00–20OKsuite en docs/ + executive + README
JWT AGENTEOKRole AGENT · D-P1-05
API + seed + NeonOKcontratos curly-recipe-23023272, port 3018
Web tokensOKInstrument Serif + Sora · paleta rusted
Wizard-first (no AP-12)OKUI-01 / docs 03, 07, 08
Hipótesis no fake fieldOKetiquetas en doc 02
Copy ×4 irrenunciableOKdocs 01, 09, 16
Ground G-CONCRETEOK#E3E1DC · no G-GALLERY / G-MOSS / G-CHROME / G-WATER / G-MUNICIPAL

3. Cobertura funcional

Feature briefSpecUI PaperAPIDocs
Wizard 3 pasosUI-01, 02, 03criterio cliente + POST compare03, 04, 16
Comparador 3 cols (precio, m², planta, gastos, extras)UI-04POST compare04, 05
Ficha de fincaUI-05GET :slug05, 09
Login AGENTEUI-06POST login04, 06
Mesa DRAFT → PUBLISHEDUI-07GET mesa + PATCH04, 05
Alta + successUI-08, UI-09POST listings04, 09
EmptyUI-10200 []16
ErrorUI-115xx/red16
Loadingspec 16 (sobre UI-04)pending16, 17
Mobile wizardspec 16 (variante UI-01)10, 16, 17
Design systemUI-0008
Seed 5 PUBLISHED + 1 DRAFTseed05

4. Cobertura Paper (literales)

IDNombreEn 00-paper-reference
UX-00Cover
UX-01Stakeholders
UX-02Personas
UX-03JTBD
UX-04Stories
UX-05Journey
UX-06Blueprint
UX-07Site map
UX-08Flujos
UX-09Datos+permisos
UX-10Métricas
UX-11Research
UI-00Tokens
UI-01Wizard 1 zona
UI-02Wizard 2 presupuesto
UI-03Wizard 3 musts
UI-04Comparador 3 cols
UI-05Ficha
UI-06Login
UI-07Mesa
UI-08Alta
UI-09Success
UI-10Empty
UI-11Error

[COMPROBADO] UX-count: 12 y UI-count: 12 literales para el gate check-paper-reference.mjs. Paper hi-fi en construcción paralela; inventario planificado escrito.

5. Rúbrica de calidad (auto SCORE orientativo · docs)

EjeScore 1–5Comentario
Diversidad5Wizard+comparador vs galería MARGEN, tabla CESTA y parte CANTERA; L1 no L2
Craft visual (spec)4–5Rusted + Instrument/Sora + fotos de finca documentadas
Densidad UX docs5Suite L1: tablas, AC, etiquetas S/H/D
Completitud código L1App fuera o en paralelo; leftover scaffold anotado
Authz5Contrato: guard en mesa/POST/PATCH; DRAFT 404
Verdad investigación5Sin entrevistas ni stats de campo falsas

6. Huecos aceptados (no regresiones de cierre documental)

HuecoClasificación
Visita / lead / mapa / search-homeFuera L1
e2e automatizadoL+1
Preview token de DRAFTL+1
Comparison persistida siempreOpcional; contrato de response cierra el Must
Leftover issues / pieces / events en repoBuild; contrato de esta suite gana
Paper/app no necesariamente tocados aquíEncargo = documentación

7. Veredicto

COMPLETO para entrega documental del caso 2026-08-18.
La suite define producto L1 usable (tablas, AC, riesgos, contratos) sin presentar investigación de campo como hecho.
Cualquier ampliación visita / lead / mapa / search-first / lookbook requiere nuevo brief de diversidad, no parche silencioso.

19-backlog-completo.md

19 — Backlog completo — ZAGUÁN

Inventario de ítems. Los del alcance L1 documental del día están Done.
El resto es opcional / siguiente nivel, no deuda oculta del cierre.

1. Done — L1 case documental (2026-08-18)

IDÍtemCapa
D01Definición producto ZAGUÁN + esloganDocs
D02Day brief diversidad inmobiliaria L1Docs (preexistente, no pisado)
D03Terna R-IN · S-WIZ · H-WIZARD · F-CMPDocs
D04Personas Marcos Vidal / Lucía Ferrer / Inés RoldánDocs + Paper ref
D05JTBD + stories MustDocs
D06IA wizard-first + chrome S-WIZDocs
D07Flujos F1–F11 + ACDocs
D08Modelo users / listings / comparisonsDocs
D09Contratos API :3018Docs
D10Auth JWT AGENTDocs
D11Estados DRAFT / PUBLISHED + 404 de borradorDocs
D12Comparador 3 cols (precio, m², planta, gastos, extras)Docs
D13Microcopy ×4 (reservar / oferta / visitado / no llama)Docs
D14North star compare + fichaDocs
D15Tokens rusted + Instrument Serif / SoraDocs
D16Paper UX-00…11 + UI-00…11Docs
D17Suite docs 00–20 + README + executiveDocs
D18Seed 5 PUBLISHED + 1 DRAFTDocs
D19Assets listados (hero + 5 fincas)README
D20QA smoke + S1–S25Docs
D21Neon curly-recipe-23023272Docs
D22Wizard 3 pasos especificadoDocs
D23Ground G-CONCRETE #E3E1DCDocs
D24Rutas reales solo las 9 del encargoDocs

Los must-have del brief (wizard, comparador, ficha, login+mesa, alta+success, empty/loading/error/mobile, ≥12 UX + ≥10 UI) están dentro de D01–D24, no como leftover.

2. Backlog L1 implementación (repo zaguan-app)

Alineación de código al contrato ya cerrado. No es alcance documental pendiente.

IDÍtemPrioridadNotas
I01Alinear apps/api + apps/web al contrato ZAGUÁNP0quitar leftover issues/pieces/events
I02Schema listings / users / comparisons?P0
I03Seed Lucía + 5 PUBLISHED + 1 DRAFTP0
I04GET listings / listings/:slug / POST compareP0DRAFT → 404
I05POST listings + PATCH :id { status }P0
I06POST login JWTP0lucia@zaguan.barrio
I07GET mesa/listingsP0
I08Wizard 3 pasos AngularP0S-WIZ
I09Comparar / ficha / login / mesa / alta / okP0
I10Tokens Tailwind + fotosP0
I11Smoke curl + buildP0D-P1-06
I12Puerto API 3018P0

3. Backlog L+1 (polish)

IDÍtemPrioridadNotas
B01canActivate guards AngularP1UX auth + wizard
B02Persistencia Comparison + replayP2home sigue paso 1
B03Preview token DRAFTP1no 200 público
B04Rate limit loginP1anti stuffing
B05Playwright smokeP1CI
B06Skeleton UI-04 en códigoP2
B07Mismatch badges si se relajan mustsP3
B08Confirm al revertir a DRAFTP2
B09API URL por environmentP1deploy
B10Página 404 amigableP3
B11Sessions invalidablesP2
B12Foto merced-2 al publicarP2

4. Backlog L3 (requiere brief nuevo)

IDÍtemDependencia
C01Multi-agencia / varios localesvenues + membership
C02Cuenta BUSCADOR + favoritosauthz
C03Calendario de visitasH2 se rompe · brief nuevo
C04Lead “te llamamos”S2 se rompe
C05Pasarela / señalS4 se rompe
C06Mapa como homerompe terna
C07Search-first homerompe terna
C08CRM 40 estadosS3 se rompe

5. Backlog explícitamente fuera

IDÍtem
E01Reserva / slot / confirmación de visita
E02Lookbook editorial / agenda semanal
E03Tabla de necesidades / compromiso de caja
E04Parte de obra / wizard de oficio
E05Feed + dock
E06App nativa
E07Hero 2-col + 3 cards de marca

6. Explicitamente no-backlog

IdeaRazón
Hero 2-col + 3 cardsAP-12 / rompe H-WIZARD
Masthead de periódicoS-TOP de MARGEN
Sidebar CRMS-SIDE
Dock flotanteS-DOCK / S-BOTTOM
Rail de iconosS-RAIL de CESTA
Command search municipalPIZARRA
Form “pide visita”H2
“Te llamamos”S2 / H3

7. Orden de ataque recomendado (continuidad de código)

  1. I01–I12 (vertical slice runnable, sin leftover)
  2. B01 + B05 + B09
  3. B03 + B08
  4. B02 + B07 si el empty es frecuente
  5. Evaluar brief L3 — no parche silencioso

8. Trazabilidad

OrigenÍtems
Day brief must-haveD01–D24, I01–I12
Doc 15 L+1B01–B12
Doc 15 L3 / fueraC01–C08, E01–E07

20-implementation.md

20 — Implementación — ZAGUÁN

1. Resumen ejecutivo técnico

CampoValor
App path/Users/cristian/orca/zaguan-app
APINestJS · puerto 3018 · prefijo /api
WebAngular standalone · puerto 4200
Packageapps independientes (npm --prefix o pnpm --filter)
DBNeon PostgreSQL · project curly-recipe-23023272
Tablasusers, listings (+ sessions Should, comparisons opcional)
AuthJWT Bearer · role AGENT
DominioUser, Listing, Comparison (opcional)
Fecha2026-08-18
GitHubhttps://github.com/Criscode2022/zaguan-app

Este documento es la especificación de build alineada al case. No sustituye al código: si el repo diverge, gana el contrato de docs 05 + este archivo tras actualizar ambos.

[COMPROBADO] Path, puerto, Neon, Paper y GitHub salen del encargo.
[COMPROBADO] El repo existe; contiene leftover de MARGEN (issues, pieces, events, assets de semanario). El contrato de esta suite es ZAGUÁN.

2. Cómo arrancar

cd /Users/cristian/orca/zaguan-app
# instalar dependencias por app (no npm workspaces)

# apps/api/.env
#   DATABASE_URL=   # Neon curly-recipe-23023272
#   JWT_SECRET=
#   PORT=3018

# schema + seed
# api → http://localhost:3018
# web → http://localhost:4200

Credenciales

RolEmailPassword
AGENTElucia@zaguan.barriopassword123

3. Schema

Al boot o vía migrate:

  1. CREATE TABLE / migrate de users, listings (doc 05). Opcional sessions, comparisons.
  2. Índices: listings(slug), listings(status, zona, rent), listings(updated_at DESC), users(email).
  3. Seed idempotente:
    • upsert AGENTE lucia@zaguan.barrio (bcrypt de password123)
    • upsert listings por slug

No borrar datos de agente en cada boot si ya existen filas distintas al seed.

Tablas (recordatorio)

TablaContenido
usersAGENTE Lucía
listings5 PUBLISHED + 1 DRAFT
comparisonsOpcional: criterio + slugs
sessionsShould: sid + token_hash + expires_at

4. Módulos API a implementar

Auth

  • POST /api/auth/login
  • Valida email/password; compara bcrypt; emite JWT con sub, email, role.
  • Guard JWT + role === AGENT protege mesa / escritura.

Listings / compare

MétodoRutaAuthNotas
GET/api/listingsNoPUBLISHED; query zona, maxRent, rooms, elevator, terrace, pets, renovation
POST/api/compareNobody criterio → { criteria, listings[≤3] }
GET/api/listings/:slugNo404 si no existe o DRAFT
GET/api/mesa/listingsJWTtodas; updatedAt desc
POST/api/listingsJWTcrea DRAFT; slug kebab
PATCH/api/listings/:idJWT{ status } y/o campos

Slug

kebab(address o title)  +  opcional "-2" si unique falla

Reintentar sufijo (máx. 5). Seed usa slugs fijos (amparo-14, …).

[DECISIÓN] Timezone Europe/Madrid para updatedAt de UI. JSON transporta ISO UTC. Rentas y gastos son enteros en euros.

Filtro público

GET /listings = WHERE status = 'PUBLISHED' [AND query]
GET /listings/:slug = 404 unless status = 'PUBLISHED'
POST /compare = top 3 PUBLISHED por score (doc 04)

Publicar no toca visitas ni genera llamada.

Score de compare (referencia de implementación)

hard: tipo AND rent <= maxRent AND (must ON ⇒ field = true)
sort: zona match DESC, rooms exact DESC, (maxRent - rent) ASC
limit 3

5. Frontend a implementar

PáginaRutaResponsabilidad
ZonaPage/paso 1, chips, persist criterio, chrome S-WIZ
PresupuestoPage/criterio/presupuestotope + hab + disclaimer oferta
MustsPage/criterio/muststoggles + POST compare
CompararPage/comparar3 cols, empty/loading/error
FincaPage/fincas/:slugGET slug, ficha, 404 DRAFT
LoginPage/loginform → login → mesa
DeskPage/mesalist + PATCH en fila + empty/error + logout
NewListingPage/mesa/nuevaform create + Guardar / Publicar
OkPage/mesa/ok/:slugconfirmación + “publicado ≠ visitado”

Shell pública: WizardChromeComponent. Mesa: wordmark + Salir, sin pasos.
ApiService centraliza base URL http://localhost:3018/api, token storage (zaguan_token, zaguan_user), criterio (zaguan_criteria), métodos tipados (Listing, CompareResponse, User).

6. Decisiones de implementación

DecisiónRazón
Puerto API 3018Evitar colisión con MARGEN 3017
Tablas listings / usersDominio inmobiliario, no leftover pieces / issues
Role AGENTEncargo; no EDITOR
Soft auth en páginas de mesaSimple; API es autoridad
Soft guard de wizardSin zona no hay paso 2
Sin FK User–ListingAgencia de 1
Slug kebabURL de lectura humana
Templates standaloneVelocidad daily
Crear siempre DRAFTBorrador invisible
Home no usa hero 2-colAP-12 / H-WIZARD
PUBLISHED no VISITED / RESERVEDDominio de catálogo, no CRM
JSON camelCaseConvenio web serie daily
PATCH :id con { status }Contrato del encargo
Success /mesa/ok/:slugBoard UI-09
Cover allowlistEvitar URL arbitraria
Comparison opcionalMust = response, no tabla

7. Variables de entorno

VariableServicioDescripción
DATABASE_URLAPINeon curly-recipe-23023272
JWT_SECRETAPIFirma tokens
PORTAPIopcional, 3018
JWT_EXPIRESAPIopcional, default 12h

Web: URL de API en ApiService (default localhost:3018).

8. Smoke de implementación (mínimo)

  1. Schema OK (tablas existen).
  2. GET /api/listings incluye 5 slugs PUBLISHED y cero DRAFT.
  3. GET /api/listings/merced-2 404.
  4. POST /api/compare USERA / ALQUILER / 950 / 2 devuelve amparo-14 y yeserias-8.
  5. POST /api/auth/login 200 + accessToken.
  6. GET /api/mesa/listings Bearer incluye el DRAFT.
  7. GET /api/mesa/listings sin token 401.
  8. PATCH /api/listings/:id { status: "PUBLISHED" } 200 y la finca aparece en listings.
  9. Web: wizard dominante, 3 columnas, disclaimers ×4, fotos 4:3.

9. Estructura de ficheros clave (objetivo)

zaguan-app/
├── package.json
├── apps/api/
│   ├── src/
│   │   ├── main.ts
│   │   ├── app.module.ts
│   │   ├── auth/
│   │   ├── listings/
│   │   └── compare/
│   └── .env.example
├── apps/web/
│   ├── tailwind.config.js
│   └── src/app/
│       ├── app.routes.ts
│       ├── core/api.service.ts
│       ├── shell/wizard-chrome.component.ts
│       └── pages/
│           ├── zona/
│           ├── presupuesto/
│           ├── musts/
│           ├── comparar/
│           ├── finca/
│           ├── login/
│           ├── desk/
│           ├── new-listing/
│           └── ok/
└── README.md

Nombres de fichero orientativos; el contrato es de rutas y tablas, no de filenames exactos.

10. Seed — checklist de implementación

#Check
1User Lucía Ferrer / lucia@zaguan.barrio / password bcrypt de password123
25 listings PUBLISHED con slugs y covers de doc 05
31 listing DRAFT merced-2 sin cover público
4Zonas USERA / VALLECAS / ENTREVIAS / ORCASUR
5Tipo ALQUILER en las 6
6Musts y extras alineados (patio, terraza, mascotas, reforma, ascensor)
7Idempotencia: upsert por email / slug

11. Alineación case ↔ app

DocEvidencia esperada en código
05 data modelcolumnas y enums
03–04 IA/flowsroutes + controllers
08 DStailwind colors + fontFamily Instrument/Sora
09 contentstrings en templates (microcopy ×4)
00 paperURLs en README case
11 securityGuard en mesa/POST/PATCH; DRAFT 404

12. Notas de cierre técnico

  • El case documental L1 está especificado (docs + Paper ref + contratos).
  • Must-have de producto del brief están escritos con AC.
  • Mejoras (preview token, comparison persistida, e2e, rate limit) viven en backlog L+1, no como deuda silenciosa.
  • Si el repo aún muestra issues / pieces / events / fotos de merienda, no está alineado: I01 es el primer ítem de build.