feat: public landing page with magic login, brand palette and demo build flag
Adds a public funnel landing at `/` and moves the login to `/login`, rebuilt around the panel's own colors instead of an invented palette. Landing (`src/components/landing/`, one section per file, no props): - Seven funnel sections composed by `LandingPage`. The hero's eight swatches are literally `PIE_COLORS` from `DashboardPage`, the same hex values the seed hands to avatars and services, so "your colors become your numbers" is literal. - `BrandMark` becomes the single source for the logo, replicating `public/favicon.svg`. Blue is the action surface, orange only ever marks. - `NotebookVisual` is the one deliberate exception to the palette: it is what the product replaces. - Copy drops all system vocabulary; motion comes from `src/lib/motion.ts` with a single easing, and reduced-motion resolves `initial` to the final state so a never-firing `whileInView` cannot leave a section invisible forever. Routing and bundle: - `homePathFor` is the single definition of each role's destination. - Landing, login, dashboard and calendar load lazily. Eager, the login dragged framer-motion (~40 KB gz) into every panel load and the landing downloaded recharts + FullCalendar (~187 KB gz) without charting anything. `clsx` is pinned to the `react` chunk because Rollup otherwise assigns it to `charts`, making the entry import 111 KB gz for a 200-byte utility. Responsiveness (iPhone/iPad), verified with `npm run audit:responsive`: - No touch form field below 16px, `dvh` height utilities, safe-area insets, and 40px touch targets keyed off `pointer: coarse` rather than `sm:`. - New `.ld-gutter`: `.safe-x` lives outside `@layer` and beats Tailwind's `px-*`, so it left the login's side padding at 0 on anything but an iPhone in landscape. No overflow check could see it — there was no overflow, just zero margin. The audit now guards it with a `gutter` check. - `shell-height` no longer fires on pages that legitimately scroll; the static `raw-viewport-unit` scan covers those instead. Production build: - The Dockerfile now sets `VITE_DEMO_UI=1` as a build arg. `DEMO` is a build-time constant, so without it Vite eliminated the magic login and the "Ver como…" switcher: the deployed landing promised "no registration" and led to an empty form. Verified by building both ways and diffing the bundle. - Service worker cache bumped to v2 so the orphaned pre-landing chunks get purged from returning visitors' caches. Verified: typecheck clean; unit 39/39, e2e 33/33, admin 17/17, booking 12/12, landing 13/13 (WebKit), PWA passed against the real production build. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0b466f33f3
commit
4c19244df9
@@ -43,3 +43,81 @@
|
||||
3. Replace local overlapsRange with imported overlaps.
|
||||
4. Hoist per-slot employee-name SELECT out of slots loop.
|
||||
5. MonthCalendar nav buttons → h-11 w-11 (44px touch target).
|
||||
|
||||
== DEMO EMAIL DOMAIN TASK ==
|
||||
- Task 1: complete — source constants, README credentials, active fixtures, and executable plan login example updated. Commits 6355d18..50dea33; review clean.
|
||||
- Task 2: complete — local data/agendapro.db migrated 2 users; second update changed 0; appointments 161 and clients 15 preserved; review clean.
|
||||
- Task 3: complete with limitation — typecheck/build/unit/admin passed; auth paths passed; e2e/booking slot assertions remain blocked by pre-existing null working-hours data, not the email change; review approved.
|
||||
- Task 4: complete — committed source/docs through 3e056a0, pushed to GitHub and Gitea, Coolify deployment wbnbq9b0kneba67sznu7p1qf finished, runtime container healthy.
|
||||
- Task 5: complete — production health/page passed, new admin/owner logins returned 200, old-domain logins returned 401; active DB was fresh and seeded with new emails, so targeted SQL was a no-op.
|
||||
- Final review: complete — final-fix review approved; regex acceptance fix review approved.
|
||||
|
||||
== AGENDAMAX PWA INSTALLABILITY TASK ==
|
||||
- Task 1: complete — manifest, iOS metadata, reproducible Playwright-generated 192/512 PNG icons, and package scripts. Commits 9d2ab39..de947f9; review clean.
|
||||
- Task 2: complete — versioned shell/static service worker and production-only registration. Commits de947f9..65233f2; review clean.
|
||||
- Task 3: complete — install prompt hook/component, iOS Safari guide, safe-area styling, and Login/AppShell/AdminShell integration; review fixes added prompt guard, appinstalled precedence, close-button label, and compact mobile layout. Commits 65233f2..9ded129; review clean.
|
||||
- Task 4: complete — production-server PWA endpoint/browser tests, online/offline API bypass checks, three-shell coverage, and install documentation; review clean. Commits 9ded129..43f5d13.
|
||||
- Final fix wave: complete — credential-aware cache policy, AgendaMax-scoped cleanup, session dismissal persistence, configurable PWA test credentials, dialog semantics, and dismissal regression coverage. Commit 0b466f3; final whole-feature review approved.
|
||||
|
||||
== LANDING PÚBLICA + ACCESO DE UN CLIC (2026-07-28) ==
|
||||
Spec: docs/superpowers/specs/2026-07-28-landing-funnel-bi-design.md
|
||||
Plan: docs/superpowers/plans/2026-07-28-landing-funnel-bi.md
|
||||
|
||||
Ruteo: `/` sirve la landing (pública, React.lazy); el login pasa a `/login`. `homePathFor(user)`
|
||||
centraliza el destino por rol. `/b/:slug` intacto, fuera del AuthProvider.
|
||||
|
||||
Dos iteraciones de diseño a pedido del usuario:
|
||||
1. Primera versión oscura/sobria (criterio Apple). Entregada y verificada.
|
||||
2. Rediseño a claro, vívido y animado, con copy humano para dueñas de salón y público de 50+
|
||||
(guía de dolores: Base de Conocimiento - IA Negocios y Dev/03-Preguntas-Guia.md, Q15/Q72/Q87:
|
||||
hablar de lo que gana el cliente, no de lo que hace el sistema). Se eliminó todo vocabulario de
|
||||
sistema del copy visible: ni «business intelligence», ni «datos», ni «métricas», ni «panel».
|
||||
3. Paleta rebasada sobre los colores REALES del panel a pedido del usuario: las 8 muestras del
|
||||
abanico del hero son PIE_COLORS de DashboardPage.tsx; la marca sale de public/favicon.svg vía
|
||||
src/components/BrandMark.tsx (única fuente); CTA = brand-500→brand-700 como .btn-primary.
|
||||
|
||||
Defectos encontrados y corregidos durante la verificación (no estaban en el plan):
|
||||
- El chunk de entrada arrastraba framer-motion (39 KB gz) a TODA carga del panel, porque LoginPage
|
||||
era eager. Se hizo lazy junto con la landing.
|
||||
- La landing descargaba recharts (111 KB gz) y FullCalendar (76 KB gz) sin usarlos, porque
|
||||
DashboardPage y CalendarPage eran eager. Ambas a lazy, con Suspense en el <Outlet> de AppShell.
|
||||
- `clsx` lo comparten lib/format.ts y recharts; Rollup lo asignaba al chunk `charts`, así que el
|
||||
entry importaba 111 KB para una utilidad de 200 bytes. Fijado al chunk `react`.
|
||||
- LOGIN CON EL TEXTO PEGADO AL BORDE en móvil: `.safe-x` está fuera de @layer y gana a `px-5`,
|
||||
dejando el padding en 0 con inset 0 (todo lo que no sea iPhone landscape). Nueva clase `.ld-gutter`.
|
||||
Ningún check existente lo detectaba (no hay desborde: hay cero margen) → añadido check `gutter`.
|
||||
- `shell-height` del audit daba falso positivo en toda página que scrollea: comparaba #root contra
|
||||
el viewport vía el fallback a body.firstElementChild. Acotado a [data-app-shell]; a cambio se
|
||||
añadió el check estático `raw-viewport-unit` sobre las fuentes públicas (verificado inyectando
|
||||
`min-h-screen` en ProofSection: dispara).
|
||||
- Rótulo «Como una de tus muchachas» en /login con Diego Castillo en la lista debajo. Reescrito sin
|
||||
asumir género («Como alguien de tu equipo»); igual en ObjectionsSection.
|
||||
- Anclas bajo el nav fijo (scroll-mt-24); degradado del cierre invisible tras `-z-10`; etiquetas del
|
||||
abanico ilegibles (eliminadas); avisos flotantes solapando el abanico (reubicados a las 3 zonas
|
||||
libres) y detalle truncado (acortado).
|
||||
|
||||
Audits ajustados por el cambio de ruteo (`/` → `/login`): visual-audit.mjs, responsive-audit.mjs y
|
||||
los SIETE `goto(baseUrl)` de pwa-e2e.mjs que esperan InstallAppPrompt. La landing se añadió a las
|
||||
listas de páginas de los dos audits.
|
||||
|
||||
VERIFICACIÓN FINAL (todo en verde):
|
||||
- typecheck: 0 errores
|
||||
- test:landing (NUEVO, WebKit): 13/13 — ruteo, acceso de un clic, sin desborde a 390px y
|
||||
prefers-reduced-motion sin contenido invisible en las 7 secciones
|
||||
- audit:responsive: 0 hallazgos (9 dispositivos × 10 páginas)
|
||||
- audit:visual: 62 pantallas, 0 fallos, 0 desborde, 0 errores de consola
|
||||
- test:pwa sobre el build: passed
|
||||
- test:unit 39/39 · test:e2e 33/33 · test:admin 17/17 · test:booking 12/12
|
||||
- build: el chunk de entrada NO importa charts, framer ni calendar (verificado en dist/)
|
||||
- WebKit a 390/440/744/820/1024/1440 en `/` y `/login`: sin desborde, sin campos <16px, sin
|
||||
objetivos <40px, sin errores
|
||||
|
||||
Sin commits (política del repo). `npm run lint` sigue roto por falta de eslint.config.js (preexistente).
|
||||
|
||||
FOLLOW-UPS DIFERIDOS:
|
||||
1. `/login` en desktop deja un vacío bajo la tarjeta del formulario (columnas de altura muy distinta).
|
||||
No es un defecto medible; revisar si molesta en uso real.
|
||||
2. La landing no tiene contenido de precios ni captura de leads: en fase demo la conversión es entrar
|
||||
a la demo. Habrá que decidirlo antes de un lanzamiento real.
|
||||
3. Seguridad sin cambios: `/login` expone contraseñas de cuentas existentes bajo la bandera DEMO.
|
||||
Autorizado solo para esta fase; es lo único que separa esto de una fuga si se publica.
|
||||
|
||||
@@ -1,74 +1,37 @@
|
||||
# Task 1 Report - AgendaMax demo access references
|
||||
# Task 1 Implementation Report
|
||||
|
||||
## Status
|
||||
## Scope
|
||||
|
||||
Complete for the source and documentation layer. Local and production SQLite data were not touched.
|
||||
Implemented only the static AgendaMax PWA contract from Task 1. Existing worktree changes and graphify outputs were not modified.
|
||||
|
||||
## Changed files
|
||||
## Changed Files
|
||||
|
||||
Implementation commit `6355d18a07b5bef6fd3515b80a0f768b0ae1050b` (`docs: standardize AgendaMax demo access`) changed:
|
||||
- `scripts/generate-pwa-icons.mjs`: reproducible Playwright generator using the existing `public/favicon.svg` as a base64 data URL.
|
||||
- `public/icon-192.png`: generated 192x192 PNG install icon.
|
||||
- `public/icon-512.png`: generated 512x512 PNG install icon.
|
||||
- `public/manifest.webmanifest`: exact AgendaMax manifest metadata and icon declarations.
|
||||
- `index.html`: manifest link, Apple install metadata, and `viewport-fit=cover`; existing favicon, theme, and description preserved.
|
||||
- `package.json`: added `generate:pwa-icons` and `test:pwa` scripts.
|
||||
|
||||
- `README.md` - added an explicit `Contraseña` column with `demo1234` for all eight demo accounts; changed only Admin and Dueño to `@agendamax.demo`.
|
||||
- `server/index.ts` - changed the fresh default owner email.
|
||||
- `server/scripts/seed.ts` - changed fresh admin and owner emails; retained `demo1234`.
|
||||
- `src/pages/LoginPage.tsx` - changed only the development default owner email.
|
||||
- `admin-test.mjs`, `e2e-test.mjs`, `e2e-full.mjs`, `server/scripts/booking-e2e.mjs`, `visual-audit.mjs` - aligned active platform login fixtures and assertions.
|
||||
- `docs/superpowers/plans/2026-07-26-auto-assign-specialist-booking-redesign.md` - aligned its copy-pasteable booking login example.
|
||||
## Verification
|
||||
|
||||
The current untracked Task 1 brief, design, and migration plan were not modified or staged. The completed Task 5 report was classified as historical and was not modified. No employee email domain or password was changed.
|
||||
- `node scripts/generate-pwa-icons.mjs`: exit 0, no output.
|
||||
- PNG signature/dimensions Node check: passed; `public/icon-192.png` is PNG 192x192 and `public/icon-512.png` is PNG 512x512.
|
||||
- Reproducibility check: passed. SHA256 values remained stable across regeneration:
|
||||
- 192 icon: `A5073D1EA7662F7E90D4E8E1DF12FF563A6DCC161FFCF6C4591D7C5F2695F6FE`
|
||||
- 512 icon: `19914C9D4C3F626782C85920DAD9E16B069809B5D876A903BF88695E1F9F045F`
|
||||
- `npm.cmd run typecheck`: exit 0.
|
||||
- `npm.cmd run build`: exit 0; Vite transformed 2463 modules and emitted the production bundle.
|
||||
- Dist asset/manifest check: passed; `dist/manifest.webmanifest`, `dist/icon-192.png`, and `dist/icon-512.png` exist and the manifest contract is valid.
|
||||
- `git diff --check`: no whitespace errors. Git emitted only existing line-ending normalization warnings.
|
||||
|
||||
## TDD evidence
|
||||
## Self-Review
|
||||
|
||||
### RED
|
||||
|
||||
The five executable login fixtures were updated before the source constants. A read-only source assertion was then run against `server/index.ts`, `server/scripts/seed.ts`, and `src/pages/LoginPage.tsx`. It failed as expected with `AssertionError` because `owner@agendamax.demo` and `[email protected]` were not yet present while the old source constants remained.
|
||||
|
||||
The focused runtime visual check was also attempted before source changes:
|
||||
|
||||
```text
|
||||
npm run audit:visual
|
||||
page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:5173/
|
||||
```
|
||||
|
||||
This did not reach the application and did not touch the database.
|
||||
|
||||
### GREEN
|
||||
|
||||
After the minimal source and README edits, the corrected read-only assertion passed:
|
||||
|
||||
```text
|
||||
source and active login fixtures: PASS
|
||||
```
|
||||
|
||||
The final active-reference and README credential assertion also passed:
|
||||
|
||||
```text
|
||||
active references and README credentials: PASS
|
||||
```
|
||||
|
||||
## Tests and outputs
|
||||
|
||||
- `npm run typecheck` - passed, exit code 0; `tsc -b --noEmit` reported no errors.
|
||||
- `npm run build` - passed, exit code 0; Vite transformed 2463 modules and built in 5.63s.
|
||||
- `npm run test:unit` - passed: 39 tests, 39 pass, 0 fail.
|
||||
- `npm run audit:visual` - unavailable: no server was listening on `localhost:5173` (`ERR_CONNECTION_REFUSED`).
|
||||
- `npm run lint` - unavailable: ESLint 9.39.5 could not find `eslint.config.js`, `eslint.config.mjs`, or `eslint.config.cjs`.
|
||||
- `rg -n ...` - unavailable because `rg` is not installed/on PATH in this environment. The equivalent scoped repository search found no old domain in active application, README, fixture, or executable plan references.
|
||||
- `git diff --cached --check` - passed before the implementation commit.
|
||||
|
||||
The database-mutating API suites (`test:e2e`, `test:admin`, and `test:booking`) were not run because this task explicitly forbids touching the ignored local database. They remain for the post-migration verification task.
|
||||
|
||||
## Self-review
|
||||
|
||||
- The quick-login behavior is unchanged: `LoginPage` still loads `/api/auth/demo-users`, and the existing password and account-switching logic remain intact.
|
||||
- Only platform-owned Admin and Dueño domains changed to `@agendamax.demo`; all employee domains remain on their existing business domains.
|
||||
- All documented demo rows explicitly show `demo1234`, including every employee row.
|
||||
- No migration, abstraction, production command, local database command, `.env.local.ps1`, or `data/` file was added or changed.
|
||||
- The implementation commit contains exactly the ten intended Task 1 files and excludes the supplied untracked design/plan files.
|
||||
- Remaining `@agendapro.demo` matches are intentional migration/history references in the new migration plan/spec and the completed Task 5 report.
|
||||
- The browser-tab icon remains `/favicon.svg`.
|
||||
- The manifest uses `/` for both `start_url` and `scope`, standalone display, portrait-primary orientation, the requested colors, and PNG icon declarations.
|
||||
- The generator resolves paths from its own module location, closes pages and the browser, and produces deterministic 1x screenshots.
|
||||
- No unrelated tracked or untracked worktree files were staged.
|
||||
|
||||
## Concerns
|
||||
|
||||
- Live/API and visual login verification is pending a running dev server and the later SQLite migration; those prerequisites were intentionally not started in this task.
|
||||
- Lint remains unavailable until the repository ESLint 9 configuration is supplied or the project pins a compatible ESLint setup.
|
||||
- The exact `rg` inventory command cannot be reproduced until ripgrep is installed/on PATH.
|
||||
- `npm.cmd run test:pwa` was not run because `pwa-e2e.mjs` is intentionally a Task 4 deliverable and does not exist at this task boundary. The script is present as required for the later task.
|
||||
|
||||
@@ -1,61 +1,38 @@
|
||||
# Task 2 Report — scheduling.ts pure functions + unit tests
|
||||
# Task 2 Implementation Report
|
||||
|
||||
## Status
|
||||
✅ COMPLETE — all acceptance criteria met.
|
||||
## Scope
|
||||
|
||||
## Files changed
|
||||
- **Created** `server/lib/scheduling.ts` — 15 pure exports (types + functions), zero imports. ~95 lines.
|
||||
- **Created** `server/lib/scheduling.test.ts` — 15 `test()` blocks using `node:test` + `node:assert/strict`.
|
||||
- **Modified** `package.json` — added `"test:unit": "node --import tsx --test server/lib/scheduling.test.ts"` to `scripts`.
|
||||
Implemented only the production AgendaMax service worker and its production-only registration. Existing `.superpowers/sdd/*` changes and `graphify-out/*` outputs were preserved and were not staged.
|
||||
|
||||
## Changed Files
|
||||
|
||||
- `public/sw.js`: added versioned shell/static caching with `agendamax-shell-v1`, install/activate lifecycle handling, network-first navigation fallback, and cache-first handling limited to script, style, image, font, manifest, and worker requests.
|
||||
- `src/main.tsx`: registered `/sw.js` after the React root render only when `import.meta.env.PROD` and service workers are supported.
|
||||
|
||||
## Verification
|
||||
|
||||
### `npm run typecheck` → 0 errors
|
||||
```
|
||||
> [email protected] typecheck
|
||||
> tsc -b --noEmit
|
||||
(clean — no diagnostics)
|
||||
```
|
||||
`allowImportingTsExtensions: true` is set in `tsconfig.json`, so `import { ... } from "./scheduling.ts"` resolves correctly under tsc.
|
||||
- `npm.cmd run typecheck`: exit 0.
|
||||
|
||||
### `npm run test:unit` → all PASS
|
||||
```
|
||||
✔ parseWorkingHours: json válido → mapa 1..7
|
||||
✔ parseWorkingHours: null/invalid → null
|
||||
✔ isoDayOfWeek: lunes=1, domingo=7
|
||||
✔ getWorkingHoursForDate: empleado tiene prioridad sobre negocio
|
||||
✔ getWorkingHoursForDate: día cerrado → null
|
||||
✔ normalizeText/tokens: quita acentos y lowercase
|
||||
✔ specialtyMatch: coincidencia exacta de etiqueta → 1
|
||||
✔ specialtyMatch: etiqueta con acento/case → 1
|
||||
✔ specialtyMatch: sin etiquetas → 0.5
|
||||
✔ specialtyMatch: etiqueta no relacionada → 0.5
|
||||
✔ overlaps: bordes inclusivos de no-traslape
|
||||
✔ hasConflict: lista vacía → false
|
||||
✔ scoreCandidate: pesos 50/30/20
|
||||
✔ scoreCandidate: mayor efficiency → mayor score
|
||||
✔ scoreCandidate: menor load (más disponible) → mayor score
|
||||
ℹ tests 15
|
||||
ℹ pass 15
|
||||
ℹ fail 0
|
||||
ℹ duration_ms 201.8453
|
||||
```
|
||||
```text
|
||||
> [email protected] typecheck
|
||||
> tsc -b --noEmit
|
||||
```
|
||||
|
||||
**Summary line:** `# tests 15 | # pass 15 | # fail 0 | duration 201.8 ms`
|
||||
- `npm.cmd run build`: exit 0. Vite transformed 2463 modules and emitted the production bundle.
|
||||
- `Test-Path -LiteralPath "dist/sw.js"`: `True`; Vite copied the public worker to the production output.
|
||||
- `git diff --check`: no whitespace errors. Git emitted only LF-to-CRLF normalization warnings for existing/changed working-tree files.
|
||||
|
||||
## Deviations from plan
|
||||
- **None.** The plan said "≈14 tests" but the spec actually contains 15 `test()` blocks — all 15 are present and pass. Code is verbatim from the plan (only the pure functions; no DB imports added — those belong to Task 3).
|
||||
## Self-Review
|
||||
|
||||
## Self-review
|
||||
|
||||
- **All 15 tests present?** ✅ Counted 15 `test(` calls in the file; runner reports `tests 15 / pass 15`.
|
||||
- **`scoreCandidate` verifies 50/30/20 weights?** ✅ The "pesos 50/30/20" test asserts three exact points on the response surface:
|
||||
- `(1, 1, 0)` → 100 (50·1 + 30·1 + 20·1)
|
||||
- `(0, 0, 1)` → 0 (all terms zero)
|
||||
- `(0.5, 0, 1)` → 25 (only specialty term contributes: 50·0.5 = 25)
|
||||
These three points uniquely pin the 50/30/20 coefficients — any other weights would fail at least one assertion. The two follow-up tests ("mayor efficiency", "menor load") additionally confirm the monotonic direction of each knob.
|
||||
- **`overlaps` boundary case verified?** ✅ The "bordes inclusivos de no-traslape" test explicitly asserts `overlaps(100, 200, 200, 300) === false` — i.e. one window ending exactly when another starts is **not** an overlap. Two true-overlap cases (forward and reverse order) are also covered.
|
||||
- **No DB leakage?** ✅ `scheduling.ts` has zero `import` statements and does not reference `DatabaseSync`, `db`, or any module. Task 3 can append the DB section cleanly.
|
||||
- Cache name and shell URLs match the Task 2 contract exactly.
|
||||
- Activation removes older cache names and claims clients after activation.
|
||||
- Fetch handling rejects non-GET requests, cross-origin requests, and paths beginning with `/api/` before any response interception.
|
||||
- Navigation requests use network-first behavior and fall back to the cached root document.
|
||||
- Static caching is allowlisted by request destination; there is no catch-all cache path.
|
||||
- Registration is deferred until `load`, uses `{ updateViaCache: "none" }`, and registration failure is non-fatal.
|
||||
- Only `public/sw.js` and `src/main.tsx` are intended for the Task 2 commit.
|
||||
|
||||
## Concerns
|
||||
None. Ready for Task 3.
|
||||
|
||||
- Validation is static/build-level only; browser service-worker runtime behavior is intentionally left to the later PWA end-to-end task.
|
||||
- `git diff --check` reports normal line-ending normalization warnings from the existing PowerShell/Git configuration, not whitespace errors.
|
||||
|
||||
@@ -1,41 +1,145 @@
|
||||
# Task 3 Report — scheduling.ts DB functions
|
||||
# Task 3 Report: Add Install Detection and UI
|
||||
|
||||
## Files changed
|
||||
- `server/lib/scheduling.ts` — appended DB-backed functions from Task 3 plan (`CandidateInfo`, `safeArr`, `getCandidates`, `getExistingBusy`, `isAvailable`, `rankOne`, `pickBestSlotEmployee`, `AutoAssignResult`, `AutoAssignCtx`, `autoAssign`, `runInTransaction`) + `import type { DatabaseSync } from "node:sqlite"`.
|
||||
## Status
|
||||
|
||||
## Typecheck
|
||||
`npm run typecheck` → **0 errors** (after the deviation below).
|
||||
Implemented Task 3 only. The install hook detects standalone mode, Chromium's deferred install event, and iOS Safari instructions. The reusable prompt is rendered once in each required shell surface and uses the existing modal/button/icon patterns.
|
||||
|
||||
## test:unit
|
||||
`npm run test:unit` → **15/15 pass** (pure tests unaffected).
|
||||
## Changed files
|
||||
|
||||
## tx smoke-test output
|
||||
Temp file `_tx-smoke.mjs` (deleted after run). It imports `runInTransaction` and `db`, exercises happy path + throw path, and verifies a real `UPDATE` was rolled back.
|
||||
- `src/lib/useInstallPrompt.ts`: added the browser-only install state hook, local deferred-event type, event listener lifecycle, install action, and dismissal behavior.
|
||||
- `src/components/InstallAppPrompt.tsx`: added the reusable Chromium/iOS install action and iOS instructions modal.
|
||||
- `src/index.css`: added `.safe-area-bottom` using `env(safe-area-inset-bottom)`.
|
||||
- `src/pages/LoginPage.tsx`: added one compact install action below the login form.
|
||||
- `src/components/AppShell.tsx`: added one install action to the mobile header.
|
||||
- `src/components/AdminShell.tsx`: added one install action to the mobile header.
|
||||
|
||||
```
|
||||
PASS runInTransaction returns fn value (42)
|
||||
PASS no active tx after commit
|
||||
PASS runInTransaction rethrows on error
|
||||
PASS no active tx after rollback
|
||||
PASS rollback undid the UPDATE
|
||||
5/5 passed
|
||||
EXIT=0
|
||||
## Verification
|
||||
|
||||
### Typecheck
|
||||
|
||||
Command:
|
||||
|
||||
```text
|
||||
npm.cmd run typecheck
|
||||
```
|
||||
|
||||
`PRAGMA active_transaction` was used (newer node:sqlite exposes it; falls back to a no-row / 0 object) to confirm no dangling transaction after both commit and rollback. The real-state rollback check (UPDATE `_tx_probe SET v=999` inside a throwing fn) proves the ROLLBACK is functional, not just emitted.
|
||||
Output:
|
||||
|
||||
## Deviations
|
||||
1. **Plan typo fix in `autoAssign` (line 256).** The plan literally wrote:
|
||||
```ts
|
||||
if (!best || ranked.score > best.score || (ranked.score === best.score && c.info.id < best.info.id)) {
|
||||
```
|
||||
but inside `autoAssign` the loop variable `c` is a `CandidateInfo` (no `.info` field), so `c.info.id` is a type error (TS2339). Fixed to `c.id < best.info.id`. `best` is still a `RankedCandidate` so `best.info.id` is correct. The semantic intent (tie-break by lowest employee id) is preserved exactly. Note: the sibling function `pickBestSlotEmployee` uses `c.id` correctly in the plan — confirming the autoAssign line was a typo, not an intentional shape.
|
||||
```text
|
||||
> [email protected] typecheck
|
||||
> tsc -b --noEmit
|
||||
```
|
||||
|
||||
No other deviations. The `import type { DatabaseSync }` is placed mid-file (after `scoreCandidate`), which is valid because ES module `import` declarations are hoisted; `tsc -b --noEmit` and `tsx` both accept it.
|
||||
Exit code: `0`.
|
||||
|
||||
### Production build
|
||||
|
||||
Command:
|
||||
|
||||
```text
|
||||
npm.cmd run build
|
||||
```
|
||||
|
||||
Output summary:
|
||||
|
||||
```text
|
||||
Preflight OK: Node v26.4.0.
|
||||
vite v5.4.21 building for production...
|
||||
✓ 2465 modules transformed.
|
||||
✓ built in 4.08s
|
||||
```
|
||||
|
||||
Exit code: `0`.
|
||||
|
||||
### Diff validation
|
||||
|
||||
Command:
|
||||
|
||||
```text
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Result: no whitespace errors. Git emitted only existing LF/CRLF conversion notices for worktree files.
|
||||
|
||||
## Self-review
|
||||
- **Does `autoAssign` return `null` when no candidate is free?** YES. Two null paths:
|
||||
1. `candidates.length === 0` → immediate `return null` (line 245).
|
||||
2. After the loop, `if (!best) return null` (line 260) covers the case where every candidate was filtered out (closed that day, slot outside working window, or has a conflict).
|
||||
- **Does it respect per-employee working hours?** YES. For each candidate it calls `getWorkingHoursForDate(c.empWh, ctx.bizWh, new Date(ctx.startMs))` — employee override wins, business is the fallback, and a closed day returns `null` → candidate is `continue`d. Then the slot must satisfy `ctx.startMs >= wh.startMs && ctx.endMs <= wh.endMs`, otherwise the candidate is skipped.
|
||||
- **Does `runInTransaction` roll back on throw?** YES. Verified by smoke test: a thrown error inside `fn` causes `db.exec("ROLLBACK")` to run, the error is re-thrown, and a real `UPDATE` made inside the throwing fn is undone (the probe row retained its pre-tx value).
|
||||
|
||||
- The hook uses a local `BeforeInstallPromptEvent` interface and does not add an unsafe global declaration.
|
||||
- `beforeinstallprompt` and `appinstalled` listeners are removed on unmount.
|
||||
- Standalone detection suppresses the CTA, and unsupported browsers remain unchanged.
|
||||
- iOS instructions are limited to Safari and the existing `Modal` owns backdrop/Escape close behavior.
|
||||
- The safe-area utility is applied only inside the iOS modal content.
|
||||
- Each shell renders exactly one prompt instance outside `SidebarContent`, avoiding duplicate listeners from the desktop/mobile sidebar duplication.
|
||||
- The accessible action text remains `Instalar AgendaMax` in every supported prompt.
|
||||
- No Task 4 tests or documentation changes were added.
|
||||
|
||||
## Concerns
|
||||
|
||||
- No browser-level visual/manual check was run; verification was limited to the required typecheck, production build, and diff inspection.
|
||||
- `git diff --check` reports line-ending notices from the existing Windows worktree configuration, not whitespace failures.
|
||||
|
||||
## Preserved worktree changes
|
||||
|
||||
Existing `.superpowers/sdd/*` changes and untracked `graphify-out/*` artifacts were not reverted or staged.
|
||||
|
||||
## Review Fixes
|
||||
|
||||
- `src/lib/useInstallPrompt.ts`: added a ref-based in-flight guard so rapid clicks can invoke the native prompt only once. The deferred event is restored when `prompt()` or `userChoice` fails, the error is contained, and the state returns to `available` for retry. `appinstalled` now clears dismissal before setting `installed`.
|
||||
- `src/components/Modal.tsx`: added `aria-label="Cerrar"` to the shared icon-only close button.
|
||||
- `src/components/InstallAppPrompt.tsx`: added an explicit `compact` presentation. Header instances use a fixed icon button with a screen-reader label and tooltip, while Login keeps the full visible action label.
|
||||
- `src/components/AppShell.tsx` and `src/components/AdminShell.tsx`: enabled the compact prompt presentation in the narrow mobile headers.
|
||||
|
||||
## Review Fix Verification
|
||||
|
||||
### Typecheck
|
||||
|
||||
Command:
|
||||
|
||||
```text
|
||||
npm.cmd run typecheck
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```text
|
||||
> [email protected] typecheck
|
||||
> tsc -b --noEmit
|
||||
```
|
||||
|
||||
Exit code: `0`.
|
||||
|
||||
### Production build
|
||||
|
||||
Command:
|
||||
|
||||
```text
|
||||
npm.cmd run build
|
||||
```
|
||||
|
||||
Output summary:
|
||||
|
||||
```text
|
||||
Preflight OK: Node v26.4.0.
|
||||
vite v5.4.21 building for production...
|
||||
✓ 2465 modules transformed.
|
||||
✓ built in 3.95s
|
||||
```
|
||||
|
||||
Exit code: `0`.
|
||||
|
||||
### Diff inspection
|
||||
|
||||
`git diff --check` reported no whitespace errors. The only messages were existing Windows LF/CRLF conversion notices. The implementation diff contains only the hook, shared prompt, shared modal accessibility, and two mobile header call-site fixes described above.
|
||||
|
||||
## Review Fix Self-Review
|
||||
|
||||
- A second click while `installInFlight.current` is true returns immediately, including clicks occurring after React has scheduled the deferred-event state update.
|
||||
- Failed native prompt or choice promises no longer produce an unhandled rejection and restore the captured event for a retryable `available` state.
|
||||
- A later `appinstalled` event overrides any prior dismissal and returns the hook state as `installed`.
|
||||
- Both modal close mechanisms remain intact, and the icon-only close control now has an accessible name.
|
||||
- Header install controls are compact without hiding the action from assistive technology; the full accessible name remains `Instalar AgendaMax`.
|
||||
- No unrelated worktree files were modified or staged, and no Task 4 tests or documentation were added.
|
||||
|
||||
## Remaining Concerns
|
||||
|
||||
- No browser-level visual/manual check was run; verification is limited to typecheck, production build, and source diff inspection.
|
||||
- The existing Windows worktree continues to emit LF/CRLF conversion notices during Git operations.
|
||||
|
||||
@@ -1,89 +1,79 @@
|
||||
# Task 4 Report — backend `booking.ts`: slots con working_hours + `pickBestSlotEmployee`
|
||||
# Task 4 Report
|
||||
|
||||
**Branch:** `feat/auto-assign-specialist`
|
||||
**Date:** 2026-07-26
|
||||
**Plan:** `docs/superpowers/plans/2026-07-26-auto-assign-specialist-booking-redesign.md` (Task 4)
|
||||
## Files
|
||||
|
||||
## Files changed
|
||||
- `server/routes/booking.ts` — only file modified by this task. Diff: **+56 / −31** lines (`git diff --stat`).
|
||||
|
||||
No other files touched. `POST /:slug/book` left exactly as-is (Task 5 territory).
|
||||
|
||||
## What changed
|
||||
1. **Imports (top of file).** Added scheduling imports from `../lib/scheduling.ts`:
|
||||
`parseWorkingHours, getWorkingHoursForDate, getCandidates, getExistingBusy, pickBestSlotEmployee, type WorkingHoursMap`.
|
||||
2. **`publicBusiness(slug)`** now SELECTs two extra columns: `auto_assign_specialist, working_hours` (so the public business payload exposes them for the frontend).
|
||||
3. **`GET /:slug/slots`** computation block fully rewritten:
|
||||
- `bizWh = parseWorkingHours(biz.working_hours)` — real business working-hours map.
|
||||
- When `employeeId` is **absent** (`useRanking = !employeeId`): pre-fetches ranked `CandidateInfo[]` via `getCandidates(...)` and the per-employee busy map via `getExistingBusy(...)`, then for every grid step calls `pickBestSlotEmployee(...)` to attach the best-ranked free specialist.
|
||||
- When `employeeId` is **provided**: resolves the per-employee working-hours window (falling back to business hours) and uses the existing busy list with the local `overlapsRange` helper.
|
||||
- Day window comes from `getWorkingHoursForDate(...)` — **no more hardcoded `09:00`/`20:00`**.
|
||||
- If the resolved window is `null` (business closed that day) → `res.json({ slots: [], service: {...} })`, no throw.
|
||||
- 30-min grid, `t + dur*60000 <= dayEnd` end-clamp, `t < now+30min` skip, 40-slot cap, and the response shape `{ slots: [{time, iso, employee_id, employee_name}], service: {id, name, price, duration_min} }` are all preserved.
|
||||
4. Added a tiny module-local `overlapsRange(a,b,c,d)` helper (equivalent to `overlaps` in scheduling.ts; the plan suggested this to keep imports minimal).
|
||||
- Added `pwa-e2e.mjs` with production endpoint, manifest, SPA fallback, Chromium install prompt, iOS instructions, standalone/unsupported states, service-worker readiness/source checks, and real browser `/api/health` request verification.
|
||||
- Updated `README.md` with local production validation, Android/Chrome installation, iOS Safari instructions, standalone behavior, connectivity expectations, and Coolify HTTPS validation guidance.
|
||||
|
||||
## Verification
|
||||
|
||||
### `npm run typecheck`
|
||||
```
|
||||
> [email protected] typecheck
|
||||
> tsc -b --noEmit
|
||||
```
|
||||
**0 errors.** ✓
|
||||
- `node --check pwa-e2e.mjs`: passed.
|
||||
- `npm.cmd run typecheck`: passed.
|
||||
- `npm.cmd run build`: passed; Vite produced the production bundle in `dist/`.
|
||||
- `npm.cmd run test:pwa`: passed; output `PWA checks passed` against the built production server on port 3000.
|
||||
- `npm.cmd run audit:visual`: passed; 58 screens/visits, 0 failed loads, 0 horizontal overflow issues. The audit reported 8 console 404 messages while still exiting 0.
|
||||
- `git diff --check`: passed with no whitespace errors. Git emitted existing LF-to-CRLF working-copy warnings for tracked files.
|
||||
|
||||
### Smoke test (live server on :3000, seeded business `lumiere-estetica-spa`)
|
||||
Wrote a throwaway `.mjs` script (deleted after) that logs in as owner, reads `/api/settings` for the slug, then hits `/api/public/<slug>` and `/api/public/<slug>/slots`.
|
||||
|
||||
**Public business now exposes the new fields:**
|
||||
```
|
||||
[info] public.business.auto_assign_specialist = 0
|
||||
[info] public.business.working_hours = string
|
||||
[ok] publicBusiness() exposes auto_assign_specialist + working_hours
|
||||
```
|
||||
|
||||
**Slots — auto-assign path (no `employee_id`, uses `pickBestSlotEmployee`):**
|
||||
Service `id=6` "Corte + arreglo de barba" (Barbería, 45 min), date `2026-07-27` (Monday, dow=1).
|
||||
```
|
||||
[info] slots status=200 count=18
|
||||
[info] service in resp = { id: 6, name: 'Corte + arreglo de barba', price: 280, duration_min: 45 }
|
||||
[sample] first slot = { time: '09:00 a.m.', iso: '2026-07-27T15:00:00.000Z', employee_id: 2, employee_name: 'Mateo Herrera' }
|
||||
[sample] last slot = { time: '07:00 p.m.', iso: '2026-07-28T01:00:00.000Z', employee_id: 2, employee_name: 'Mateo Herrera' }
|
||||
[ok] all 18 slots have employee_id+employee_name and iso within 09:00-20:00 (inWindow=true)
|
||||
```
|
||||
- 18 slots, each carries `employee_id` + `employee_name` (resolved by `pickBestSlotEmployee`).
|
||||
- Window strictly within the seed's Lun-Vie 09:00–20:00.
|
||||
- Only Mateo Herrera offers service #6 in the seed, so the ranker correctly resolves every slot to him.
|
||||
|
||||
**Closed-day path (Saturday, default seed = closed):**
|
||||
```
|
||||
[info] Saturday 2026-08-01 slots count=0 (expected 0 if Sat closed)
|
||||
[ok] closed day → { slots: [] } with service object (no throw)
|
||||
```
|
||||
The endpoint returns `{ slots: [], service: {...} }` — no exception, matching constraint #5.
|
||||
|
||||
**Specific-employee path (`employee_id=2`, the only employee offering svc #6):**
|
||||
```
|
||||
status: 200 count: 18
|
||||
all match eid: true
|
||||
first: { time: '09:00 a.m.', iso: '2026-07-27T15:00:00.000Z', employee_id: 2, employee_name: 'Mateo Herrera' }
|
||||
service in resp: { id: 6, name: 'Corte + arreglo de barba', price: 280, duration_min: 45 }
|
||||
```
|
||||
Every slot's `employee_id === 2`; service object included.
|
||||
|
||||
## Deviations from the plan
|
||||
1. **Dropped `isoDateStr` from the Task-4 import list.** The plan's Task-4 import block includes `isoDateStr`, but the Task-4 code never references it, and `tsconfig.json` has `"noUnusedLocals": true` — keeping it would fail the `npm run typecheck` verification step. Task 5 ("Ampliar imports de scheduling") explicitly adds more imports and can re-introduce `isoDateStr` at that point when it's actually used by the `POST /book` rewrite. Functionality identical.
|
||||
2. **Renamed `const any = …` → `const anyEmp = …`** in the fallback query inside the candidates block. `any` is a reserved-ish TypeScript type name and using it as a value identifier is confusing and risky under strict mode. Purely cosmetic; behavior identical.
|
||||
The first PWA run exposed a test-harness race where the synthetic event could fire before React registered its listener. The harness was corrected to dispatch 100 ms after `load`; the subsequent production run passed.
|
||||
|
||||
## Self-review
|
||||
|
||||
- **Does the day window come from `working_hours` (not the old hardcoded 09/20)?** Yes. The old `new Date(${date}T09:00:00)` / `T20:00:00` literals are gone. The window now comes from `getWorkingHoursForDate(singleEmpWh | null, bizWh, dateObj)`, which reads `parseWorkingHours(biz.working_hours)`. Confirmed empirically: a Saturday (closed in seed) returns 0 slots, while a Monday returns 18 slots within 09:00–20:00.
|
||||
- **When `employeeId` is null, does it use `pickBestSlotEmployee`?** Yes. `useRanking = !employeeId` gates the branch; when true, the per-slot resolver is `chosen = pickBestSlotEmployee(candInfos, candBusyMap, bizWh, {name, category}, slotStart, slotEnd)`. The previous "first free employee" inner loop is removed.
|
||||
- **Response shape preserved?** Yes — `{ slots: [{time, iso, employee_id, employee_name}], service: {id, name, price, duration_min} }` verified in all three paths (ranking, specific-employee, closed-day).
|
||||
- **30-min grid, 40-slot cap, skip-past/<30min-from-now?** All preserved (loop body unchanged in spirit; observed 18 slots, well under 40).
|
||||
- **Closed day → `{slots: []}` + service object, no throw?** Confirmed.
|
||||
- **POST /book untouched?** Yes — diff shows the entire POST handler is byte-identical to the pre-task version.
|
||||
- Endpoint checks use the configured `PWA_BASE_URL`, defaulting to `http://localhost:3000`.
|
||||
- Manifest assertions cover standalone display, root start URL/scope, and both required icon sizes.
|
||||
- Browser contexts are isolated for Chromium prompt, iOS Safari instructions, standalone mode, and unsupported/no-event mode.
|
||||
- Service-worker readiness has a five-second bound, and the source guard checks cover `/api/` and non-GET requests.
|
||||
- The browser API check observes a real request and validates its JSON response rather than using a fixture.
|
||||
- No app authentication, tenant data, API route, database schema, or runtime behavior was changed.
|
||||
|
||||
## Concerns
|
||||
- The seed only has one employee (Mateo Herrera) offering service #6, so the ranker resolves every slot to the same person. To genuinely exercise the multi-candidate ranking in integration, Task 7's `booking-e2e.mjs` (or a manual second employee with overlapping service + different `efficiency_score` / `specialties`) would be needed. Out of scope for Task 4.
|
||||
- `biz.working_hours` is returned to the frontend as a raw JSON **string** (not parsed). Frontend will need to `JSON.parse` it — that's expected per `shared/types.ts` (`working_hours?: WorkingHoursMap | string | null`) and consistent with how Settings already returns it. No action needed here.
|
||||
- A leftover orphan `tsx watch` process from an initial failed `Start-Process` attempt was killed during cleanup; the user's main `npm run dev` (concurrently) was left untouched and is still serving on :3000.
|
||||
|
||||
- The visual audit continues to record eight pre-existing 404 console messages on some authenticated/admin visits. They were not introduced by Task 4 and were not changed because the task explicitly limits scope to PWA verification and production documentation.
|
||||
- The report itself and pre-existing `.superpowers/sdd/*` and `graphify-out/*` changes remain outside the Task 4 commit staging set.
|
||||
|
||||
## Review Fixes
|
||||
|
||||
- Added `fetchWithTimeout()` using `AbortController` for every direct endpoint/source fetch. The in-page `/api/health` request now uses its own bounded `AbortController` as well.
|
||||
- Added exact-one accessible-action coverage for the login page, authenticated business shell, and authenticated admin shell. Business/admin flows use the existing demo credentials and a mobile viewport so their compact install controls are visible and accessible.
|
||||
- Replaced fixed sleeps in standalone/unsupported checks with bounded DOM-state polling for zero matching install actions.
|
||||
- Replaced iOS body-text checks with visible exact-text locators for `Compartir` and `Añadir a pantalla de inicio`.
|
||||
- Relaxed service-worker source checks to tolerate whitespace and quote style while still requiring the pathname `/api/` bypass and non-GET guard.
|
||||
- Tracked all browser contexts and closes them explicitly before closing the browser in `finally`; default UI/navigation timeouts are bounded.
|
||||
|
||||
## Review-Fix Verification
|
||||
|
||||
- `node --check pwa-e2e.mjs`: passed.
|
||||
- `npm.cmd run typecheck`: passed.
|
||||
- `npm.cmd run build`: passed; Vite produced the production bundle in `dist/`.
|
||||
- `npm.cmd run test:pwa`: passed; output `PWA checks passed` against the built production server on port 3000.
|
||||
- `npm.cmd run audit:visual`: passed; 58 screens/visits, 0 failed loads, 0 horizontal overflow issues. The audit still reports 8 pre-existing console 404 messages.
|
||||
- `git diff --check`: passed with no whitespace errors; Git only emitted existing LF-to-CRLF working-copy warnings.
|
||||
|
||||
## Review-Fix Self-review
|
||||
|
||||
- Direct network operations now have bounded completion, including response-body reads for endpoint/source assertions and the browser-side API response.
|
||||
- Login, business, and admin each assert one and only one accessible `Instalar AgendaMax` action; the two shell checks exercise the authenticated UI path rather than assuming route markup.
|
||||
- Negative install-state checks wait on the DOM condition with a timeout and fail non-zero if the condition never becomes true.
|
||||
- All changes remain in the PWA test harness and this report; application behavior and unrelated tests are unchanged.
|
||||
|
||||
## Remaining Concerns
|
||||
|
||||
- The visual audit's 8 pre-existing 404 console messages remain outside Task 4 scope. They do not cause failed loads, overflow failures, or a non-zero audit exit.
|
||||
|
||||
## Service-worker Behavioral Fix
|
||||
|
||||
- Added a reload and `navigator.serviceWorker.controller` gate before behavioral API checks, ensuring requests run through an active, controlling worker.
|
||||
- While online, the browser now verifies `GET /api/health` returns `200` and a safe invalid `POST /api/auth/login` returns `401`.
|
||||
- With the Playwright context offline, both API requests must produce bounded network errors rather than cached responses. Connectivity is restored in a `finally` block.
|
||||
|
||||
## Service-worker Behavioral Verification
|
||||
|
||||
- `npm.cmd run typecheck`: passed.
|
||||
- `npm.cmd run build`: passed; Vite produced the production bundle in `dist/`.
|
||||
- `npm.cmd run test:pwa`: passed; output `PWA checks passed` against the built production server on port 3000.
|
||||
- `git diff --check`: passed with no whitespace errors; Git only emitted existing LF-to-CRLF working-copy warnings.
|
||||
|
||||
## Service-worker Behavioral Self-review
|
||||
|
||||
- The online assertions validate real statuses (`200` and `401`) before offline mode is enabled, so a cached fixture cannot satisfy the positive path.
|
||||
- Offline assertions require both requests to reject with a network-error result and do not accept a response status or body.
|
||||
- The offline state is always reverted in `finally`, and the existing bounded request, navigation, worker-readiness, UI, install-surface, and cleanup checks remain intact.
|
||||
|
||||
Reference in New Issue
Block a user