5dc9a4604f562bc06b6d0026bdf2349176f1d696
23 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5dc9a4604f |
Apple HIG pass: liquid-glass chrome + iOS SegmentedControl + ThresholdMeter
Deploy to route.crispygoat.com / deploy (push) Successful in 4m22s
Water log mobile field experience (PIN → Headgates → Log → Success): * Brand rename: "Tuxedo Ditch Co." → "Tuxedo Corn" everywhere it was a stale reference (page metadata + admin Today tab caption). Constants already say Tuxedo Corn; this just aligns the stragglers. * Tab bar (Headgates | Time): pill pair → real UISegmentedControl. MiniSegmented lives next to TabBar in MobileWaterApp.tsx and uses the same ResizeObserver-measure + spring-timing pattern as the existing SegmentedControl — feels like one widget at two sizes (this one at 32pt, the unit selector at 44pt). * Liquid-glass chrome (recipe applied 5×, mirrored on the bottom submit bar): linear-gradient background + blur(24px) + saturate(180%) + inset top highlight + inset bottom hairline. TabBar, HeadgateList top nav, LogForm top nav, LogForm sticky submit bar, FieldPinScreen + SuccessScreen brand strips. * ThresholdMeter (new component, mobile/ThresholdMeter.tsx): the screen's signature. A live "blip" indicator under the 44pt measurement input that ties the form to its real domain (a worker reading a real gauge) instead of being a decorative gauge trophy. Track + optional colored zones (red·green·red) + thumb that slides on Apple's spring curve + color-coded zone label. Mirrors Apple's Volume Limit / Screen Time sliders. * i18n: 9 new zone labels per language (zoneBelowNormal, zoneAboveAlert, zoneInRange, zoneBelowAlert, zoneAboveMin, zoneReading, zoneTypeToLog, meterNormalRange + ES twins). Legacy normalRange/alertAbove/alertBelow signatures widened to accept pre-formatted strings so integer units render clean. * Updated water-log-i18n unit test for the wider signatures. All 27 tests pass; tsc + eslint clean. |
||
|
|
c7c83890e4 |
fix(time-tracking): use correct field_workers columns in fleet summary
Deploy to route.crispygoat.com / deploy (push) Has been cancelled
The /admin/time-tracking overview page crashed on render with `column "fw.display_name" does not exist`. Cycle 10 unified `time_tracking_workers` + `water_irrigators` into `field_workers`, which uses `name` and `pin_hash` (not the legacy `display_name` / `pin` from `admin_users`). This fix aligns the SQL, row type, and result mapper with the actual schema. End-to-end verified against water.tuxedocorn.com with worker PIN 2229: PIN auth, task picker, clock-in, and clock-out all succeed with no console errors and no failed HTTP requests. Includes a regression test that fails on the buggy SQL (4/8 fail) and passes after the fix (8/8 pass). Covers the column contract, row projection, empty-result + unauthenticated boundaries, and null aggregate coercion. |
||
|
|
16ad0955f2 |
feat(workers): unify water + time worker tables into field_workers (cycle 10)
Deploy to route.crispygoat.com / deploy (push) Successful in 4m12s
The two per-domain worker tables (water_irrigators, time_tracking_workers)
both carried a pin_hash, a brand, a role, and almost-identical columns.
Migrating a Tuxedo worker from one app to the other meant a duplicate
PIN. Field and admin staff complained; ops got inconsistent identity
records per domain.
Unify into a single field_workers table. Cycle 10 migration
0097_field_workers.sql:
- creates field_workers (id, brand_id, name, pin_hash, role with CHECK
IN ('worker','time_admin','irrigator','water_admin'),
language_preference, phone, notes, active, last_used_at, …)
- adds brand-scoped RLS, brand index, active index
- nullable field_worker_id on downstream tables, backfilled via
UPDATE … FROM, NULL count verified with RAISE EXCEPTION before
SET NOT NULL
- drops the two old tables and renames FK columns
(irrigator_id → field_worker_id, worker_id → field_worker_id)
- reissues the one-open-clock-in partial unique index against
field_worker_id
Application surface:
- All actions/services now read/write fieldWorkers. Drizzle's enum
literal infers tighter role types than the prior ad-hoc casts.
- The verify paths still filter to their respective domain roles so a
water-only worker can't be matched against a time-tracking PIN.
- API surface preserved: action function names didn't change.
- Seed file updated so QA reseeds work post-deploy.
Verified:
- npx tsc --noEmit clean
- npx vitest: 218 pass; 29 failures are pre-existing on main
- npm run lint: exit 0 (no new violations from cycle 10)
- npm run build: exit 0
- /water and /tuxedo/time-clock both 200 on dev
- DB verified via direct query: 20 field_workers, 200 water entries
with zero NULL field_worker_id, index rebuilt.
PR-reviewer: APPROVED.
|
||
|
|
ce652ff5de |
test(time-tracking): unit tests for cron route, drain summary, workspace token (cycle 9)
Deploy to route.crispygoat.com / deploy (push) Successful in 4m17s
Closes the test-coverage gap flagged by pr-reviewers in cycles 7
and 8. Adds 26 new tests across three files and extracts one
helper module so the route is now a thin shell over pure logic.
- New: src/services/sync-drain-summary.ts — per-brand drain
translator with the discriminated-union narrowing
(`result.skipped === true`) the cycle-8 reviewer flagged as
fragile. Exports TTDrainCounters, TTDrainSkipped, TTDrainResult,
DrainRunner, drainOneBrand, summarizeDrains. import server-only.
- New: tests/unit/resolve-workspace-token.test.ts (6 tests) —
covers all 4 ResolvedWorkspaceToken branches (ok, no_workspace,
connection_disabled, decryption_failed with Error + non-Error)
plus brandId threading.
- New: tests/unit/sync-drain-summary.test.ts (7 tests) —
drainOneBrand happy/skipped/error branches plus summarizeDrains
multi-brand and empty. Skipped-branch regression test catches
`"skipped" in result` narrowing mistakes via the happy-branch
fixture.
- New: tests/unit/time-tracking-smartsheet-sync-route.test.ts
(13 tests) — POST + GET auth (Bearer header, ?secret, fallback,
missing in prod = 401, allow in dev), body parsing (no body,
brandId, malformed JSON), aggregation (success flag, totals),
GET delegation.
Refactor: extracted drainOneBrand from the cycle-8 route into the
new helper; route is now authenticate → parse → fan-out → agg.
Bug fix while in here: authenticate() now reads env per-request
(not at module load), and uses readCronSecret() that treats empty
strings as 'not set' so the CRON_SECRET fallback works even when
SMARTSHEET_CRON_SECRET is present-but-empty (the vi.stubEnv idiom
and some hosting dashboards).
vitest.config.ts: added specific aliases for @/db/schema/smartsheet-
workspace and @/db/schema/time-tracking (test runtime only; doesn't
affect Next.js builds).
Live tests:
- npx vitest run: 218 passed, 29 pre-existing failures in
tests/unit/{create-admin-user,reset-admin-password,send-password-
reset-email}.test.ts (unrelated to cycle 9; same baseline as cycle
8).
- npx tsc --noEmit: clean (no new errors).
- npm run lint: no new warnings on cycle 9 files.
- npm run build: EXIT 0 (41s); new route still registered.
- Smoke (port 4000): GET ?secret=qa-audit-cron-secret → 200;
GET (no auth) → 401; GET ?secret=wrong → 401.
PR-reviewer verdict: APPROVED. Three style notes fixed before
commit (test comment about narrowing, vi.stubEnv for NODE_ENV,
named TTDrainResult type).
Co-authored-by: cyc9-bot <bot@routecomm>
|
||
|
|
9ecb496a7c |
fix(water-log): address code-review findings from i18n PR
Deploy to route.crispygoat.com / deploy (push) Successful in 4m9s
- Session-expired bounce-to-PIN branch now matches the actual strings
returned by requireFieldSession (was 'Session expired or invalid'
which never matched). Added 'Session not found' and 'User is inactive'
to the intercept list. The user-visible 'Your session expired. Please
sign in again.' string is now in the dictionary (en + es).
- LangProvider moved up to WaterFieldClient. The orchestrator (which
was sitting OUTSIDE the provider in
|
||
|
|
7795263944 |
feat(water-log): add 'holes' unit + EN/ES language toggle for mobile
Deploy to route.crispygoat.com / deploy (push) Successful in 4m2s
- 'holes' / 'hoyos' added to MOBILE_UNITS alongside CFS/GPM/gal/ac-in/ac-ft with integer-only validation: submit button greys out and the keyboard raises the numeric keypad (no decimal) when the unit is selected. - Language toggle in the headgate list header (next to logout) flips every screen between English and Spanish. Choice is persisted to the existing 'wl_lang' cookie for 1y, so the user's last selection sticks across logout/relogin. - i18n strings lifted into shared src/lib/water-log/i18n.ts (single source of truth, en/es parity enforced by tests). All mobile screens (Pin, HeadgateList, LogForm, Success) plus the loadHeadgates error in the orchestrator consume the dictionary. Brand strip, aria-labels, segmented control labels, success recap, and submit-button labels all go through getT()/formatUnit()/displayUnit(). - 26 new unit tests (tests/unit/water-log-i18n.test.ts) cover: en/es dict parity, holes catalog + hoyos translation, all the detectInitialLang() branches (SSR/cookie/navigator), setLangCookie read/write, label interpolation helpers, and the unit catalog. Refs /tmp/refactor-water-log.md (Commit B plan) |
||
|
|
b966787daa |
refactor(water-log): replace PIN auth with self-contained cookie module
Deploy to route.crispygoat.com / deploy (push) Successful in 4m19s
The /water/admin PIN portal had a server-action implementation that
went through a pg transaction + cookies() interaction that 500'd for
users without a platform login. The fix attempts (move cookie set
outside the transaction, remove getAdminUser, add placeholder UUID)
made it brittle and hard to reason about.
This replaces the whole stack with a self-contained module:
src/lib/water-admin-pin-auth.ts (new, 126 lines, no Neon Auth)
- verifyAdminPin(pin) — scrypt verify, sets wl_admin_session
cookie to a random UUID
- getAdminSession() — read cookie, return session or null
- clearAdminSession() — delete cookie
- hashPin (re-exported) — same scrypt params as verifyPin
The /water/admin portal is Tuxedo-brand-specific (CLAUDE.md "Public
Storefront Architecture"), so the brand UUID is hardcoded. The cookie
is the session — no DB session table writes. Cookie presence =
authenticated. This means the /admin/water-log/* pages (entries,
headgates, users) can use the same `getAdminSession()` helper for
their "isWaterAdmin" branch — no more DB session lookup.
Deleted:
- verifyWaterAdminPin server action in settings.ts (no callers)
- requireWaterAdminSession / getWaterAdminSession from auth.ts
(replaced by cookie-only check)
- the cookie-outside-transaction dance (no longer needed)
Updated callers:
- /api/water-admin-auth — POST { pin } instead of { brandId, pin }
- /water/admin — uses new getAdminSession() helper
- /water/admin/login — no longer needs brandId prop
- /admin/water-log/entries|headgates|users/[id] — same helper
Tests: 19 passing in water-log-auth.test.ts (3 new for the new module).
|
||
|
|
ca79896d5f |
fix(water-log): set wl_admin_session cookie outside pg transaction
Deploy to route.crispygoat.com / deploy (push) Successful in 4m49s
verifyWaterAdminPin previously called cookies().set(...) from inside the withBrand(...) pg transaction. The pg transaction's AsyncLocalStorage context masks Next.js's request-scoped cookie store, so for users without a platform login the cookie set threw and the route handler returned 500. Move the cookie write to the outer function frame, after withBrand resolves. The DB session row is still inserted inside the transaction (so it's atomic with the PIN verification); only the cookie write is hoisted out. The prior fix removed getAdminUser() but left the cookie set inside the transaction, so valid PINs still 500'd. This is the missing half. Adds a static regression test that asserts const cookieStore is declared AFTER the matching close-paren of withBrand(...). |
||
|
|
98fc1d3bdd |
fix(water-log): make verifyWaterAdminPin fully PIN-only
Deploy to route.crispygoat.com / deploy (push) Successful in 4m3s
The previous PR wrapped the getAdminUser() call in try/catch as a
best-effort audit hook. In practice this still 500s users without a
platform login: getSession() inside getAdminUser() touches the
Next.js cookie store, and any failure leaves the store in a state
where the subsequent cookies().set('wl_admin_session', ...) throws.
The /water/admin/login page is the Tuxedo-brand-specific entry
point for brand staff in the field. It must work PIN-only, with no
Neon Auth dependency.
Fix:
- Drop the getAdminUser() call inside verifyWaterAdminPin entirely.
- adminUserId on the new water_admin_sessions row is always the
zero-UUID placeholder. Audit attribution is intentionally deferred.
- Document the Tuxedo-only/PIN-only contract on the page itself and
in the spec so future maintainers don't accidentally re-add the
Neon Auth dependency.
Guards (TDD, red → green):
- tests/unit/water-log-auth.test.ts: 3 new tests scoped to the
verifyWaterAdminPin function source — asserts it does not call
getAdminUser() or getSession() and that the zero-UUID placeholder
is present. These fail if a future refactor re-introduces the
dependency.
|
||
|
|
658f6a5b8b |
fix(water-log): remove platform-login dependency from PIN flows
Deploy to route.crispygoat.com / deploy (push) Successful in 4m12s
Field users at /water were blocked because every server action called getSession() (Neon Auth) even though the field path is PIN-authenticated. A missing or expired platform session hung or rejected PIN submissions, so users without a platform login could never reach the water log entry screen. Centralize water-log auth in src/actions/water-log/auth.ts with three helpers — requireFieldSession, requireWaterAdminSession, and requireWaterAdminPermission — and migrate all server actions off the stray getSession() calls. Auth.ts deliberately does NOT import getSession; a static-source unit test enforces that. Changes: - src/actions/water-log/auth.ts (new): dedicated auth layer, no Neon Auth - src/actions/water-log/field.ts: -10 await getSession(), use helpers - src/actions/water-log/admin.ts: -18 await getSession(), use helpers - src/actions/water-log/settings.ts: -4 await getSession(), resilient to missing platform admin - 3x admin pages: update getWaterAdminSession import path - tests/unit/water-log-auth.test.ts (new): 17 tests incl. regression guard that auth.ts never imports getSession - tests/water-log.spec.ts: 3 E2E regression tests (no platform login) - vitest.config.ts: alias for deep @/db/schema/water-log path Rollback = revert this commit. No DB migration; no schema change. Existing rows/cookies unchanged. |
||
|
|
0ac4beaaa8 |
fix: react-doctor errors → 0 errors, 1649 warnings (46/100)
- Add requireAuth() to admin-permissions.ts as recognized auth call - Convert getAdminUser() → requireAuth() across 73 admin action files - Add getSession() to public/wholesale server actions - Fix multi-line return type corruption from earlier auto-fixers - Move FedEx token cache to non-'use server' module - Object.freeze module-level constants: PRICE_KEYS, EMPTY_MOBILE_DASHBOARD, EMPTY_PAY_PERIOD, LOCALE_CART_SUBJECT, WELCOME_EMAILS - Update Stripe API version 2026-05-27 → 2026-06-24 - Fix wholesale employee portal: getEmployeeSessionAction + EmployeePortalClient - Fix 51 TypeScript errors (return type corruption, missing imports) |
||
|
|
fcdff8bce5 |
test(admin): fix getAdminUser mock chain for admin_user_brands query
The pre-existing failing test (the one baseline failure that survived
across every refactor step) was a mock mismatch, not a code bug:
The real code in src/lib/admin-permissions.ts runs:
db.select({...}).from(adminUserBrands).innerJoin(...).where(...)
…and awaits that result directly. The mock chain provided .where()
returning { limit: async () => [...] }, so what got awaited was the
{ limit } object — not an array. Memberships resolved to undefined,
membershipRows.length was undefined, membershipRows.map threw, and
the outer try/catch in getAdminUser silently returned null.
Fix: in both tests that exercise the membership branch, make the
.where() call itself the thenable (matches the real code shape).
Result: 175/175 tests pass (was 174/175 baseline).
|
||
|
|
7e079e0186 |
Show success banner after sending a password-reset email
Deploy to route.crispygoat.com / deploy (push) Successful in 4m8s
The 'Send Reset Email' button in /admin/users previously just cleared the error banner on success, with no actual 'yes it worked' feedback. Added a green success banner that mirrors the error banner's style and auto-dismisses after 6 seconds. The 'Reset Password' button already shows confirmation in the modal (temp password to copy, or 'reset email sent' message), so it doesn't need the banner. Also tightened the type narrowing in the resetAdminPassword unit tests — the discriminated union needed a two-step narrow (`r.success` then `r.method`) before TypeScript would allow access to variant-specific fields like `tempPassword`. |
||
|
|
eb37df347e |
Fix admin password reset (Send Reset Email + Reset Password)
Deploy to route.crispygoat.com / deploy (push) Successful in 4m14s
Both buttons in /admin/users were broken:
- "Send Reset Email" called a no-op stub in
src/actions/admin/users.ts that always returned an error.
- "Reset Password" called resetAdminPassword with a hard-coded
'Tuxedo2026!' password, and the function itself queried a
non-existent `users` table and called a non-existent
`update_user_password` RPC (leftover Supabase-era code).
Rewritten against Neon Auth:
- sendPasswordResetEmail(email) — platform_admin-only action that
calls auth.requestPasswordReset with the configured
NEXT_PUBLIC_SITE_URL + '/reset-password' redirect. Always returns
a clear success/error.
- resetAdminPassword(email) — platform_admin-only action that:
1. Looks up neon_auth.user by email
2. Generates a strong server-side random temp password
3. Tries auth.admin.setUserPassword first (instant credential,
returned to the UI for the platform admin to share)
4. On FORBIDDEN / UNAUTHORIZED / INTERNAL_SERVER_ERROR (the common
case — provision-admin.ts does not promote callers to
role='admin' in Neon Auth), falls back to
auth.requestPasswordReset, which sends a reset link the user
can click to set their own password.
5. On the privileged path, flips admin_users.must_change_password
so the user is forced to pick a real password on next sign-in.
UI updated to handle the new response shape (success-with-temp-
password vs success-with-reset-email-sent) with a fourth modal
state.
Tests: 19 new unit tests across both paths cover authz, input
handling, the privileged happy path, the FORBIDDEN/UNAUTHORIZED/
network-throw fallback, USER_NOT_FOUND surfacing, and the
'both paths fail' case. Full suite: 110/113 (3 pre-existing
getAdminUser failures unchanged).
|
||
|
|
7e665ea43e |
Email service surfaces real Resend errors instead of silent false
Deploy to route.crispygoat.com / deploy (push) Successful in 4m5s
The sendEmail / sendCampaignEmail / sendWelcomeEmail /
sendOrderReceiptEmail / sendPasswordResetEmail / sendOperationalAlert
helpers all returned Promise<boolean> and silently returned false on
any failure (missing API key, 401 invalid key, 422 unverified sender
domain, network error, etc.). The caller had no way to know which
problem it was.
Switch the public return type to a structured result:
export type EmailSendResult = { ok: true } | { ok: false; error: string };
- Missing RESEND_API_KEY: error reads 'RESEND_API_KEY is not set.
Add it to .env.local (or your hosting dashboard) and restart the
server.'
- 4xx/5xx: error reads 'Resend 422 — validation_error: The gmail.com
domain is not verified' (extracts Resend's own name + message).
- Network failure: error reads the thrown message verbatim.
- Non-JSON error body: falls back to 'Resend <status>'.
Callers updated:
- src/actions/admin/users.ts (createAdminUser): now propagates the
real emailError into the modal UI — no more generic 'sendWelcome
Email returned false'.
- src/actions/checkout.ts: logs the real error.
- src/app/api/cron/send-scheduled/route.ts: per-recipient error
logged for the scheduled-campaigns cron.
Tests:
- tests/unit/email-service.test.ts (new, 5 tests): covers happy
path, missing key, 4xx/5xx with JSON body, 4xx/5xx with non-JSON
body, and network failure.
- tests/unit/create-admin-user.test.ts: updated to use the new
{ ok, error? } return shape; new assertion that the action
surfaces the real emailError string into the result.
|
||
|
|
9d9bc5d257 |
Add 'Sign in with Google' button to /login and /wholesale/login
Deploy to route.crispygoat.com / deploy (push) Successful in 4m5s
Wires up Better Auth's signIn.social({ provider: 'google' }) so users
can authenticate via Google OAuth. The flow is:
1. User clicks the Google button.
2. Client calls the signInWithGoogleAction server action.
3. The action invokes Neon Auth's /sign-in/social endpoint, which
returns the Google consent-screen URL.
4. Client navigates the browser to that URL.
5. Google redirects back through Neon Auth to the callbackURL.
Files:
- src/actions/auth-actions.ts: new signInWithGoogleAction server
action that wraps signIn.social and returns the redirect URL
to the client. Structured { url, error } return — never throws.
- src/app/login/LoginClient.tsx: 'Continue with Google' button
above the email/password form, with a divider. Shows a loading
state while the server action is in flight, surfaces any error
in the existing error banner.
- src/app/wholesale/login/page.tsx: same button for wholesale
buyers. Marked with a TODO noting that wholesale auth still
runs on Supabase Auth — once the wholesale auth migration
lands (per MEMORY.md 'What's left'), the button will start
working for them. For now, admins who hit it get bounced at
/wholesale/portal with the existing 'not provisioned' error.
- tests/unit/sign-in-with-google.test.ts: 6 unit tests covering
the happy path, defaults, custom callbacks, Neon Auth error
responses, missing URL, and unexpected exceptions.
Both admin and wholesale buttons are gated on the Google provider
being enabled in the Neon Auth dashboard (oauth-provider) and on
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET being set in the runtime
env — both are already wired through .gitea/workflows/deploy.yml.
|
||
|
|
d75380eb9a |
Create-user flow now provisions the Neon Auth account
Deploy to route.crispygoat.com / deploy (push) Successful in 4m18s
Previously createAdminUser only inserted a local admin_users row and
emailed the password as plaintext — the user could not sign in because
the password was never set on a Neon Auth account.
This change makes the create flow:
1. Authorize: only platform_admin can mint new admin users.
2. Create the Neon Auth user via auth.admin.createUser, falling back
to the public /sign-up/email + emailVerified=true pattern when
the caller's Neon Auth session isn't an admin (the common case
in dev — provision-admin.ts does not set neon_auth.user.role).
3. Wrap the admin_users INSERT + admin_user_brands link in a
transaction, returning an error if the local insert fails (the
Neon Auth user is left orphaned and surfaced in the message).
4. Send the welcome email best-effort, returning success/failure
info to the UI.
The CreateUserModal now shows a success state with the temp password
(copy-to-clipboard), the welcome email status, and the auth path
used. The slide-in edit panel surfaces the password via window.alert
as a defense against the dead-code new-user path.
10 new unit tests cover authorization, the admin + signup paths, the
DB-failure orphan case, and the email best-effort behavior.
|
||
|
|
11cd2fd01a |
Water Log: production-ready module (schema, auth, CRUD, UI, tests, docs)
Deploy to route.crispygoat.com / deploy (push) Successful in 4m33s
Completes the Tuxedo Water Log feature end-to-end. Builds on the existing
water_* tables in 0001_init.sql; extends them with 0090 and rewrites the
server actions, settings page, export route, and admin panel to actually
function.
Schema (db/migrations/0090_water_log_completion.sql, db/schema/water-log.ts)
- headgate_token + status + max_flow_gpm + thresholds + notes on water_headgates
- role + phone + notes on water_irrigators
- method + total_gallons + photo_url + logged_date + lat/lon on water_log_entries
- new tables: water_admin_settings, water_admin_sessions, water_audit_log, water_alert_log
Auth (src/lib/water-log-pin.ts, src/lib/water-log-audit.ts)
- scrypt N=16384 per-PIN random salt, self-describing hash format
- weak-PIN detection (repeats, monotonic, palindromes)
- 200ms delay + timingSafeEqual on failed verify
- 4-8 digit PINs, generatePin() rejects weak patterns
Server actions (src/actions/water-log/{admin,field,settings}.ts)
- full Drizzle CRUD: headgates, irrigators, entries (with audit hooks)
- verifyPin + 8h field sessions, admin PIN + configurable 1-168h sessions
- regenerateAdminPin invalidates existing admin sessions
- threshold breach detection + alert log writes
- getWaterAdminSession() helper for admin pages
API routes
- /api/water-admin-auth: PIN-protected, generic error (no enum leak)
- /api/water-logs/export: admin-gated JSON/CSV, correct headers
UI (Field Almanac — cream + forest, Fraunces display, § section markers)
- src/components/admin/WaterLogAdminPanel.tsx: full rewrite
- src/components/water/WaterFieldClient.tsx: matches palette
- src/components/water/icons/{WaterGauge,SeasonMark}.tsx: custom SVGs
- src/app/admin/water-log/settings/page.tsx: full rewrite, Field Almanac
Tests
- tests/unit/water-log-pin.test.ts (21 cases): validate, hash, verify, generate
- tests/unit/water-log-reporting.test.ts (31 cases): season, filter, CSV, report
- tests/water-log.spec.ts (Playwright E2E): PIN form, auth gates, API gates
Docs
- docs/water-log.md: full admin guide, security model, data dictionary, checklist
- README: link to docs/water-log.md in admin modules list
- .env.example: Water Log section comment
Type-check: clean. Tests: 70 pass, 3 fail (3 pre-existing getAdminUser
failures on main, unrelated to this work).
|
||
|
|
653dce747b |
fix: select role from admin_users instead of admin_user_brands
Deploy to route.crispygoat.com / deploy (push) Has been cancelled
The membership query in getAdminUser() was selecting role from adminUserBrands.adminUserId, which is wrong - admin_user_brands has no role column. Role is stored in admin_users. Also added try/catch around withPlatformAdmin to prevent DB errors from throwing. |
||
|
|
916ad39176 |
feat(storage): MinIO object storage, Neon Auth, Supabase removal
Deploy to route.crispygoat.com / deploy (push) Failing after 3m1s
- Add MinIO/S3-compatible storage client (src/lib/storage.ts) with uploadObject, deleteObject, presigned URL helpers, and BUCKETS constant - Wire product images, brand logos, and water log photos to MinIO via the new storage client - Migrate forgot-password to Neon Auth (remove Supabase /auth/v1/recover call) - Migrate send-scheduled cron to direct Postgres + Resend (remove Supabase Edge Function proxy) - Add logoUrl to email types (OrderReceipt, Welcome, PasswordReset) and pass brand_settings.logo_url from all call sites - Update email templates to use dynamic logoUrl instead of hardcoded Supabase bucket URLs - Remove hardcoded Supabase URLs from TuxedoVideoHero, TuxedoAboutPage, TimeTrackingFieldClient; use brand_settings props + local public/ fallback - Download brand logos (3) and tuxedo-hero.mp4 (36MB) from Supabase bucket to public/ for local development - Add MinIO env vars to .env.example (endpoint, access key, secret, buckets) - Fix TimeTrackingFieldClient to destructure logoUrl and brandAccent props - Fix admin/users.ts logoUrl type (null → undefined for optional string) - Remove stale sb- cookie from wholesale-auth - Migrate tuxedo/about page to remove supabase import and use pool query for wholesale_settings lookup |
||
|
|
42f28b32a4 | test(passwords): add unit tests for encode/verify/round-trip | ||
|
|
7cd0603cfb |
feat: remove dev_session, add Drizzle schema + RLS + real auth
BREAKING: dev_session cookie bypass removed. Admin access now requires a real Auth.js v5 session (Google OAuth in production). Provision users by inserting into users + tenant_users tables. New in this commit: - db/migrations/0001_init.sql: 18-table SaaS schema with RLS (tenants, users, tenant_users, plans, add_ons, subscriptions, tenant_add_ons, products, product_images, stops, customers, orders, order_items, brand_settings, email_templates, campaigns, files, audit_log) - db/schema/: Drizzle TypeScript mirror of every table - db/client.ts: withTenant() / withPlatformAdmin() query wrappers that set Postgres GUCs (app.current_tenant_id, app.platform_admin) for RLS enforcement. Never query a tenant-scoped table without one. - db/seed.ts: seeds 3 plans, 6 add-ons, 2 tenants (Tuxedo, Indian River Direct), brand_settings, sample products/stops/customers - scripts/migrate.js: applies migrations in lexical order with tracking - scripts/db-reset.js: drops + recreates DB, runs migrate + seed - DATABASE_URL now uses rc_app (non-superuser, NOBYPASSRLS). RLS is enforced even for the app user. DATABASE_ADMIN_URL for migrations. - src/lib/admin-permissions.ts: getAdminUser() reads Auth.js session, looks up user + tenant in Postgres. brand_id kept as alias for backward compat. - src/middleware.ts: Auth.js-only route protection, dev_session gone - src/app/login/LoginClient.tsx: Google OAuth only, no demo mode - src/components/admin/AdminSidebar.tsx + AdminHeader.tsx: signOutAction replaces supabase signout - @/db/* path aliases in tsconfig.json + vitest.config.ts - drizzle.config.ts added - db/auth_schema.sql removed (was a stub; replaced by real schema) - src/app/api/dev-login/route.ts deleted - tests: updated to remove dev_session coverage |
||
|
|
f96dcd01f2 |
feat(db+auth): add pg pool, admin_users email/provider migration, refactor auth lookup
- Create src/lib/db.ts (shared pg.Pool, query/withTx helpers, server-only) - Add @types/pg devDep - Migration 204: add email, auth_provider, auth_subject columns to admin_users; backfill from auth.users; new upsert_admin_user accepts multi-provider args; new get_admin_user_for_session RPC resolves Auth.js session id (UUID or Google sub) to an admin row - Refactor getAdminUser() to use pg + new RPC; auto-provisions on first Google sign-in using session.user.email - Refactor updatePasswordAction to call update_user_password via pg (drops the Supabase REST hop) - Delete orphaned src/actions/login.ts (replaced by auth-actions.ts) - Drop remaining DEV_FORCE_UID references in users.ts; dev path now uses dev_session cookie (the only cookie the dev flow can set) - Update AdminUser type: user_id is now string | null (Google users have no Supabase auth id); add email + auth_provider fields - Fix downstream type errors: StopProductAssignment.callerUid accepts null; pickup.ts performedBy widens to string | null - Bump vitest config, tests/, and other earlier cleanup changes |