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
@@ -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