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,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.
|
||||
|
||||
Reference in New Issue
Block a user