Component architecture
- Reorganise src/lib/components into pages/, sections/, ui/ subdirectories and
  update all imports across routes and tests.

Owner admin dashboard (+2k lines)
- Scheduled welcome-pack emails: durable queue with cancel/reschedule and a
  background sender loop (SCHEDULED_CHECK_INTERVAL_SECONDS, default 60s).
- Custom welcome-pack subject line, preview recipients, and client BCC.
- Add-client, edit client profile, and reset-onboarding flows.
- "View as client" onboarding preview (owner impersonation, dry-run submit).
- Tabbed owner welcome route (/owner/welcome/[[tab]]).

MYOB integration
- New mail_api/myob.py: create new clients as MYOB AccountRight customer
  contacts. Disabled until all MYOB_* env vars are set (no-op otherwise).

Onboarding
- New "Does your dog resource guard?" Yes/No question in the Behaviour step.
- Persist vetAddress, flea/tick, and pet-insurance fields server-side.

Misc
- vite config, new hooks.ts, responsive CSS tweaks, deploy/docker config,
  mail-api README and start-dev.ps1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 21:51:29 +12:00
co-authored by Claude Opus 4.8
parent 7d5b72e1d3
commit 0e88e7bdac
82 changed files with 4543 additions and 914 deletions
+11
View File
@@ -40,6 +40,17 @@ EMAIL_SEND_TIMEOUT_SECONDS=20
# Rotate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" # Rotate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
DEPLOY_SMOKE_SECRET=ed7261d3d7a5ac0a51e0cfb2bf4e2bd4009503605d2963d3ee766b7e885e76eb DEPLOY_SMOKE_SECRET=ed7261d3d7a5ac0a51e0cfb2bf4e2bd4009503605d2963d3ee766b7e885e76eb
# MYOB integration — optional. When all of MYOB_API_KEY / MYOB_API_SECRET /
# MYOB_REFRESH_TOKEN / MYOB_COMPANY_FILE_URI are set, the "Also create this
# client in MYOB" checkbox in the control panel's Add-client modal creates the
# client as a customer contact in MYOB AccountRight. Leave blank to disable.
# MYOB_API_KEY=
# MYOB_API_SECRET=
# MYOB_REFRESH_TOKEN=
# MYOB_COMPANY_FILE_URI=https://api.myob.com/accountright/<company-file-guid>
# MYOB_CF_USERNAME=Administrator
# MYOB_CF_PASSWORD=
# Security hardening — sensible defaults are in mail_api/config.py. # Security hardening — sensible defaults are in mail_api/config.py.
# Override only if the public domains change or you need to allow extra origins. # Override only if the public domains change or you need to allow extra origins.
# CORS_ALLOWED_ORIGINS=https://goodwalk.co.nz,https://www.goodwalk.co.nz,https://clients.goodwalk.co.nz,https://cp.goodwalk.co.nz # CORS_ALLOWED_ORIGINS=https://goodwalk.co.nz,https://www.goodwalk.co.nz,https://clients.goodwalk.co.nz,https://cp.goodwalk.co.nz
+1
View File
@@ -399,6 +399,7 @@ function Invoke-AllFormSmokeTests {
dogName = 'Smoke' dogName = 'Smoke'
dogBreed = 'Test' dogBreed = 'Test'
vetName = 'Dr Smoke' vetName = 'Dr Smoke'
vetAddress = '2 Vet Rd, Auckland'
vetPhone = '+64-00-000-0000' vetPhone = '+64-00-000-0000'
emergencyContactName = 'Smoke Contact' emergencyContactName = 'Smoke Contact'
emergencyContactPhone = '+64-00-000-0000' emergencyContactPhone = '+64-00-000-0000'
+7
View File
@@ -59,6 +59,13 @@ services:
TRUSTED_HOSTS: ${TRUSTED_HOSTS:-} TRUSTED_HOSTS: ${TRUSTED_HOSTS:-}
MAX_REQUEST_BODY_BYTES: ${MAX_REQUEST_BODY_BYTES:-} MAX_REQUEST_BODY_BYTES: ${MAX_REQUEST_BODY_BYTES:-}
DEPLOY_SMOKE_SECRET: ${DEPLOY_SMOKE_SECRET:-} DEPLOY_SMOKE_SECRET: ${DEPLOY_SMOKE_SECRET:-}
# MYOB integration (optional — see deploy.env.template).
MYOB_API_KEY: ${MYOB_API_KEY:-}
MYOB_API_SECRET: ${MYOB_API_SECRET:-}
MYOB_REFRESH_TOKEN: ${MYOB_REFRESH_TOKEN:-}
MYOB_COMPANY_FILE_URI: ${MYOB_COMPANY_FILE_URI:-}
MYOB_CF_USERNAME: ${MYOB_CF_USERNAME:-Administrator}
MYOB_CF_PASSWORD: ${MYOB_CF_PASSWORD:-}
PYTHONUNBUFFERED: '1' PYTHONUNBUFFERED: '1'
TZ: ${TZ:-Pacific/Auckland} TZ: ${TZ:-Pacific/Auckland}
expose: expose:
+7
View File
@@ -32,6 +32,13 @@ services:
REPLY_TO: ${REPLY_TO:-info@goodwalk.co.nz} REPLY_TO: ${REPLY_TO:-info@goodwalk.co.nz}
ENABLE_GENERAL_ENQUIRIES: ${ENABLE_GENERAL_ENQUIRIES:-false} ENABLE_GENERAL_ENQUIRIES: ${ENABLE_GENERAL_ENQUIRIES:-false}
DATA_DIR: ${MAIL_API_DATA_DIR:-/app/data} DATA_DIR: ${MAIL_API_DATA_DIR:-/app/data}
# MYOB integration (optional — see deploy.env.template).
MYOB_API_KEY: ${MYOB_API_KEY:-}
MYOB_API_SECRET: ${MYOB_API_SECRET:-}
MYOB_REFRESH_TOKEN: ${MYOB_REFRESH_TOKEN:-}
MYOB_COMPANY_FILE_URI: ${MYOB_COMPANY_FILE_URI:-}
MYOB_CF_USERNAME: ${MYOB_CF_USERNAME:-Administrator}
MYOB_CF_PASSWORD: ${MYOB_CF_PASSWORD:-}
TZ: ${TZ:-Pacific/Auckland} TZ: ${TZ:-Pacific/Auckland}
volumes: volumes:
- mail_api_data:${MAIL_API_DATA_DIR:-/app/data} - mail_api_data:${MAIL_API_DATA_DIR:-/app/data}
+22
View File
@@ -0,0 +1,22 @@
# Mail API local dev
Use the project virtualenv, not the global Python install. The local environment already resolves compatible `pydantic` and `pydantic-core` versions together.
## Start on Windows PowerShell
```powershell
cd C:\Users\mattc\gw-svelte\gw-svelte\mail-api
.\start-dev.ps1
```
## First-time setup
```powershell
cd C:\Users\mattc\gw-svelte\gw-svelte\mail-api
py -3.14 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
```
## If you see a `pydantic-core` mismatch
You are almost certainly running against a global interpreter or a stale environment. Reinstall into the project virtualenv and start the app with `.\start-dev.ps1`.
Binary file not shown.
Binary file not shown.
+72 -72
View File
@@ -207,12 +207,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"27" "27"
], ],
@@ -432,12 +432,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"45" "45"
], ],
@@ -657,12 +657,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"95" "95"
], ],
@@ -882,12 +882,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"103" "103"
], ],
@@ -1107,12 +1107,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"39" "39"
], ],
@@ -1332,12 +1332,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"41" "41"
], ],
@@ -1557,12 +1557,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"36" "36"
], ],
@@ -1782,12 +1782,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"83" "83"
], ],
@@ -2007,12 +2007,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"76", "76",
"49" "49"
@@ -2233,12 +2233,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"99" "99"
], ],
@@ -2458,12 +2458,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"84" "84"
], ],
@@ -2683,12 +2683,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"51" "51"
], ],
@@ -2908,12 +2908,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"87" "87"
], ],
@@ -3134,12 +3134,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"88" "88"
], ],
@@ -3359,12 +3359,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"34" "34"
], ],
@@ -3584,12 +3584,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"61", "61",
"47" "47"
@@ -3811,12 +3811,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"57" "57"
], ],
@@ -4036,12 +4036,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"32" "32"
], ],
@@ -4261,12 +4261,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"22" "22"
], ],
@@ -4486,12 +4486,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"64" "64"
], ],
@@ -4711,12 +4711,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"74" "74"
], ],
@@ -4936,12 +4936,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"56" "56"
], ],
@@ -5161,12 +5161,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"116", "116",
"93" "93"
@@ -5387,12 +5387,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"59" "59"
], ],
@@ -5612,12 +5612,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"30" "30"
], ],
@@ -5837,12 +5837,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"53" "53"
], ],
@@ -6062,12 +6062,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"107" "107"
], ],
@@ -6287,12 +6287,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"24" "24"
], ],
@@ -6512,12 +6512,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"43" "43"
], ],
@@ -6737,12 +6737,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"97" "97"
], ],
@@ -6962,12 +6962,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"90" "90"
], ],
@@ -7188,12 +7188,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"105" "105"
], ],
@@ -7413,12 +7413,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"101" "101"
], ],
@@ -7638,12 +7638,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"78" "78"
], ],
@@ -7664,12 +7664,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [], "onboardingEntryIds": [],
"contractEntryIds": [ "contractEntryIds": [
"29" "29"
@@ -7887,12 +7887,12 @@
"lifecycle": { "lifecycle": {
"status": "archived", "status": "archived",
"reason": "Imported from legacy Gravity Forms data", "reason": "Imported from legacy Gravity Forms data",
"changedAt": "2026-05-24T05:45:33.160Z", "changedAt": "2026-06-15T21:24:42.302Z",
"changedBy": "legacy-import" "changedBy": "legacy-import"
}, },
"legacy": { "legacy": {
"source": "gravity-forms-csv", "source": "gravity-forms-csv",
"importedAt": "2026-05-24T05:45:33.160Z", "importedAt": "2026-06-15T21:24:42.302Z",
"onboardingEntryIds": [ "onboardingEntryIds": [
"80" "80"
], ],
+654
View File
@@ -1437,3 +1437,657 @@ resend.exceptions.ResendError: API key is invalid
19/05/2026 23:32:40 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded 19/05/2026 23:32:40 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
19/05/2026 23:32:40 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address 19/05/2026 23:32:40 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
19/05/2026 23:37:58 New Zealand Standard Time INFO mail-api: [e8c74fa0] GET /auth/verify → 401 (2ms) 19/05/2026 23:37:58 New Zealand Standard Time INFO mail-api: [e8c74fa0] GET /auth/verify → 401 (2ms)
15/06/2026 20:01:52 New Zealand Standard Time INFO mail-api: Logging initialised → console=INFO, file=logs\mail-api.log (DEBUG, rotating)
15/06/2026 20:01:52 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
15/06/2026 20:01:52 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
15/06/2026 20:01:53 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
15/06/2026 20:01:53 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
15/06/2026 20:01:53 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
15/06/2026 20:05:19 New Zealand Standard Time INFO mail-api: [ae1b0f7e] POST /auth/request-code → 400 (0ms)
15/06/2026 20:06:13 New Zealand Standard Time INFO mail-api: [faf603f0] POST /auth/request-code → 400 (0ms)
15/06/2026 20:06:14 New Zealand Standard Time INFO mail-api: [dd8ad9b9] POST /auth/request-code → 400 (0ms)
15/06/2026 20:06:14 New Zealand Standard Time INFO mail-api: [3fb682e8] POST /auth/request-code → 400 (0ms)
15/06/2026 20:06:14 New Zealand Standard Time INFO mail-api: [734a08d2] POST /auth/request-code → 400 (0ms)
15/06/2026 20:06:14 New Zealand Standard Time INFO mail-api: [658d0d93] POST /auth/request-code → 400 (0ms)
15/06/2026 20:06:15 New Zealand Standard Time INFO mail-api: [a66b3c9f] POST /auth/request-code → 400 (0ms)
15/06/2026 20:06:15 New Zealand Standard Time INFO mail-api: [28307070] POST /auth/request-code → 400 (0ms)
15/06/2026 20:06:29 New Zealand Standard Time INFO mail-api: Logging initialised → console=INFO, file=logs\mail-api.log (DEBUG, rotating)
15/06/2026 20:06:29 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
15/06/2026 20:06:29 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
15/06/2026 20:06:29 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
15/06/2026 20:06:29 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
15/06/2026 20:06:29 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
15/06/2026 20:06:36 New Zealand Standard Time INFO mail-api: Logging initialised → console=INFO, file=logs\mail-api.log (DEBUG, rotating)
15/06/2026 20:06:37 New Zealand Standard Time CRITICAL mail-api: Startup aborted: missing env vars: ['RESEND_API_KEY', 'OWNER_EMAIL']
15/06/2026 20:06:55 New Zealand Standard Time INFO mail-api: [78c69f47] POST /auth/request-code → 400 (0ms)
15/06/2026 20:07:00 New Zealand Standard Time INFO mail-api: Logging initialised → console=INFO, file=logs\mail-api.log (DEBUG, rotating)
15/06/2026 20:07:00 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
15/06/2026 20:07:00 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
15/06/2026 20:07:00 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
15/06/2026 20:07:00 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
15/06/2026 20:07:00 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
15/06/2026 20:07:02 New Zealand Standard Time INFO mail-api: [ccfc7849] POST /auth/request-code → 400 (0ms)
15/06/2026 20:08:31 New Zealand Standard Time INFO mail-api: Logging initialised → console=INFO, file=logs\mail-api.log (DEBUG, rotating)
15/06/2026 20:08:31 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
15/06/2026 20:08:31 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
15/06/2026 20:08:31 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
15/06/2026 20:08:31 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
15/06/2026 20:08:31 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
15/06/2026 20:08:36 New Zealand Standard Time INFO mail-api: Logging initialised → console=INFO, file=logs\mail-api.log (DEBUG, rotating)
15/06/2026 20:08:36 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
15/06/2026 20:08:36 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
15/06/2026 20:08:36 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
15/06/2026 20:08:36 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
15/06/2026 20:08:36 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
15/06/2026 20:08:40 New Zealand Standard Time INFO mail-api: [b2f2b0fc] POST /auth/request-code → 400 (0ms)
15/06/2026 20:09:25 New Zealand Standard Time ERROR mail-api: [0f10f6ec] POST /auth/request-code crashed after 3ms
Traceback (most recent call last):
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\main.py", line 579, in _request_logging_middleware
response = await call_next(request)
^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\middleware\base.py", line 168, in call_next
raise app_exc from app_exc.__cause__ or app_exc.__context__
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\middleware\base.py", line 144, in coro
await self.app(scope, receive_or_disconnect, send_no_error)
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\middleware\cors.py", line 88, in __call__
await self.app(scope, receive, send)
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\middleware\trustedhost.py", line 51, in __call__
await self.app(scope, receive, send)
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\main.py", line 546, in __call__
await self.app(scope, _wrapped_receive, send)
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\middleware\exceptions.py", line 63, in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app
raise exc
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app
await app(scope, receive, sender)
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\fastapi\middleware\asyncexitstack.py", line 18, in __call__
await self.app(scope, receive, send)
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\routing.py", line 660, in __call__
await self.middleware_stack(scope, receive, send)
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\routing.py", line 680, in app
await route.handle(scope, receive, send)
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\routing.py", line 276, in handle
await self.app(scope, receive, send)
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\fastapi\routing.py", line 134, in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app
raise exc
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app
await app(scope, receive, sender)
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\fastapi\routing.py", line 120, in app
response = await f(request)
^^^^^^^^^^^^^^^^
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\fastapi\routing.py", line 674, in app
raw_response = await run_endpoint_function(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<3 lines>...
)
^
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\fastapi\routing.py", line 328, in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\main.py", line 2474, in auth_request_code
body = await request.json()
^^^^^^^^^^^^^^^^^^^^
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\requests.py", line 264, in json
self._json = json.loads(body)
~~~~~~~~~~^^^^^^
File "C:\Users\mattc\AppData\Local\Programs\Python\Python314\Lib\json\__init__.py", line 352, in loads
return _default_decoder.decode(s)
~~~~~~~~~~~~~~~~~~~~~~~^^^
File "C:\Users\mattc\AppData\Local\Programs\Python\Python314\Lib\json\decoder.py", line 345, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\mattc\AppData\Local\Programs\Python\Python314\Lib\json\decoder.py", line 361, in raw_decode
obj, end = self.scan_once(s, idx)
~~~~~~~~~~~~~~^^^^^^^^
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)
15/06/2026 20:09:38 New Zealand Standard Time ERROR mail-api: [251e2d22] POST /auth/request-code crashed after 1ms
Traceback (most recent call last):
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\main.py", line 579, in _request_logging_middleware
response = await call_next(request)
^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\middleware\base.py", line 168, in call_next
raise app_exc from app_exc.__cause__ or app_exc.__context__
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\middleware\base.py", line 144, in coro
await self.app(scope, receive_or_disconnect, send_no_error)
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\middleware\cors.py", line 88, in __call__
await self.app(scope, receive, send)
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\middleware\trustedhost.py", line 51, in __call__
await self.app(scope, receive, send)
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\main.py", line 546, in __call__
await self.app(scope, _wrapped_receive, send)
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\middleware\exceptions.py", line 63, in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app
raise exc
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app
await app(scope, receive, sender)
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\fastapi\middleware\asyncexitstack.py", line 18, in __call__
await self.app(scope, receive, send)
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\routing.py", line 660, in __call__
await self.middleware_stack(scope, receive, send)
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\routing.py", line 680, in app
await route.handle(scope, receive, send)
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\routing.py", line 276, in handle
await self.app(scope, receive, send)
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\fastapi\routing.py", line 134, in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app
raise exc
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app
await app(scope, receive, sender)
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\fastapi\routing.py", line 120, in app
response = await f(request)
^^^^^^^^^^^^^^^^
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\fastapi\routing.py", line 674, in app
raw_response = await run_endpoint_function(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<3 lines>...
)
^
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\fastapi\routing.py", line 328, in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\main.py", line 2474, in auth_request_code
body = await request.json()
^^^^^^^^^^^^^^^^^^^^
File "C:\Users\mattc\gw-svelte\gw-svelte\mail-api\.venv\Lib\site-packages\starlette\requests.py", line 264, in json
self._json = json.loads(body)
~~~~~~~~~~^^^^^^
File "C:\Users\mattc\AppData\Local\Programs\Python\Python314\Lib\json\__init__.py", line 352, in loads
return _default_decoder.decode(s)
~~~~~~~~~~~~~~~~~~~~~~~^^^
File "C:\Users\mattc\AppData\Local\Programs\Python\Python314\Lib\json\decoder.py", line 345, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\mattc\AppData\Local\Programs\Python\Python314\Lib\json\decoder.py", line 361, in raw_decode
obj, end = self.scan_once(s, idx)
~~~~~~~~~~~~~~^^^^^^^^
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)
15/06/2026 20:09:53 New Zealand Standard Time INFO mail-api: Logging initialised → console=INFO, file=logs\mail-api.log (DEBUG, rotating)
15/06/2026 20:09:53 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
15/06/2026 20:09:53 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
15/06/2026 20:09:53 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
15/06/2026 20:09:53 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
15/06/2026 20:09:53 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
15/06/2026 20:10:15 New Zealand Standard Time INFO mail-api: Logging initialised → console=INFO, file=logs\mail-api.log (DEBUG, rotating)
15/06/2026 20:10:15 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
15/06/2026 20:10:15 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
15/06/2026 20:10:15 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
15/06/2026 20:10:15 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
15/06/2026 20:10:15 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
15/06/2026 20:10:20 New Zealand Standard Time INFO mail-api: [72b111d2] POST /auth/request-code → 400 (0ms)
15/06/2026 20:11:27 New Zealand Standard Time INFO mail-api: Logging initialised → console=INFO, file=logs\mail-api.log (DEBUG, rotating)
15/06/2026 20:11:27 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
15/06/2026 20:11:27 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
15/06/2026 20:11:27 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
15/06/2026 20:11:27 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
15/06/2026 20:11:27 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
15/06/2026 20:11:38 New Zealand Standard Time INFO mail-api: [6c351fe3] POST /auth/request-code → 400 (0ms)
15/06/2026 20:11:45 New Zealand Standard Time INFO mail-api: [912374c3] POST /auth/request-code → 400 (0ms)
15/06/2026 20:11:50 New Zealand Standard Time INFO mail-api: [ca16d00f] POST /auth/request-code → 400 (0ms)
15/06/2026 20:15:10 New Zealand Standard Time INFO mail-api: [90835023] auth: code issued for email=info@goodwalk.co.nz
15/06/2026 20:15:10 New Zealand Standard Time WARNING mail-api: [DEV] auth code for info@goodwalk.co.nz: 956187
15/06/2026 20:15:10 New Zealand Standard Time INFO mail-api: [90835023] POST /auth/request-code → 200 (11ms)
15/06/2026 20:15:18 New Zealand Standard Time INFO mail-api: [b5f581f9] auth: session created for email=info@goodwalk.co.nz
15/06/2026 20:15:18 New Zealand Standard Time INFO mail-api: [b5f581f9] POST /auth/verify-code → 200 (2ms)
15/06/2026 20:15:18 New Zealand Standard Time INFO mail-api: [9d58963f] GET /auth/verify → 200 (1ms)
15/06/2026 20:15:18 New Zealand Standard Time INFO mail-api: [6efc3cae] GET /owner/pending-onboarding → 200 (1ms)
15/06/2026 20:15:18 New Zealand Standard Time INFO mail-api: [f8fd2577] GET /owner/all-clients → 200 (2ms)
15/06/2026 20:15:18 New Zealand Standard Time INFO mail-api: [bd38d327] GET /owner/birthdays → 200 (11ms)
15/06/2026 20:15:20 New Zealand Standard Time INFO mail-api: [59319824] GET /auth/verify → 200 (0ms)
15/06/2026 20:15:20 New Zealand Standard Time INFO mail-api: [bb2fa1e3] GET /owner/pending-onboarding → 200 (1ms)
15/06/2026 20:15:20 New Zealand Standard Time INFO mail-api: [a7d68e07] GET /owner/all-clients → 200 (2ms)
15/06/2026 20:15:20 New Zealand Standard Time INFO mail-api: [3f420d48] GET /owner/birthdays → 200 (2ms)
15/06/2026 20:15:24 New Zealand Standard Time INFO mail-api: [9dee7759] GET /auth/verify → 200 (0ms)
15/06/2026 20:15:24 New Zealand Standard Time INFO mail-api: [bff1d27d] GET /owner/pending-onboarding → 200 (0ms)
15/06/2026 20:15:24 New Zealand Standard Time INFO mail-api: [573299f0] GET /owner/birthdays → 200 (1ms)
15/06/2026 20:15:24 New Zealand Standard Time INFO mail-api: [c1b2cbca] GET /owner/all-clients → 200 (1ms)
15/06/2026 20:15:31 New Zealand Standard Time INFO mail-api: [8ca00ff2] GET /owner/message-templates → 200 (1ms)
15/06/2026 20:15:32 New Zealand Standard Time INFO mail-api: [5d67ac5b] POST /owner/render-message → 200 (3ms)
15/06/2026 20:16:25 New Zealand Standard Time INFO mail-api: [a3d3d2d9] GET /owner/activity → 200 (1ms)
15/06/2026 20:17:24 New Zealand Standard Time INFO mail-api: [62384e2c] GET /owner/activity → 200 (0ms)
15/06/2026 20:17:46 New Zealand Standard Time INFO mail-api: [be53083f] GET /owner/activity → 200 (0ms)
15/06/2026 20:18:38 New Zealand Standard Time INFO mail-api: [5f777ef7] GET /owner/activity → 200 (0ms)
15/06/2026 20:19:19 New Zealand Standard Time INFO mail-api: [739f25e5] GET /owner/client-enquiry → 200 (8ms)
15/06/2026 20:19:28 New Zealand Standard Time INFO mail-api: [e70d718b] GET /owner/activity → 200 (1ms)
15/06/2026 20:20:46 New Zealand Standard Time INFO mail-api: [4b64a4ec] GET /owner/activity → 200 (0ms)
15/06/2026 20:21:06 New Zealand Standard Time INFO mail-api: [5206327c] GET /auth/verify → 200 (0ms)
15/06/2026 20:21:06 New Zealand Standard Time INFO mail-api: [0520fe2c] GET /owner/pending-onboarding → 200 (0ms)
15/06/2026 20:21:06 New Zealand Standard Time INFO mail-api: [659bc15b] GET /owner/all-clients → 200 (1ms)
15/06/2026 20:21:06 New Zealand Standard Time INFO mail-api: [b367235f] GET /owner/birthdays → 200 (1ms)
15/06/2026 20:27:58 New Zealand Standard Time INFO mail-api: Logging initialised → console=INFO, file=logs\mail-api.log (DEBUG, rotating)
15/06/2026 20:27:58 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
15/06/2026 20:27:58 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
15/06/2026 20:27:58 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
15/06/2026 20:27:58 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
15/06/2026 20:27:58 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
15/06/2026 20:28:06 New Zealand Standard Time INFO mail-api: [bc4610f2] GET /owner/message-templates → 401 (2ms)
15/06/2026 20:28:07 New Zealand Standard Time INFO mail-api: [0169b9e6] GET /owner/activity → 401 (1ms)
15/06/2026 20:30:00 New Zealand Standard Time INFO mail-api: [87be64e8] GET /owner/message-templates → 401 (0ms)
15/06/2026 20:30:01 New Zealand Standard Time INFO mail-api: [2822c7c6] GET /owner/activity → 401 (0ms)
15/06/2026 20:30:09 New Zealand Standard Time INFO mail-api: [f61a5c74] GET /auth/verify → 401 (1ms)
15/06/2026 20:31:51 New Zealand Standard Time INFO mail-api: [ee1b53c2] auth: code issued for email=info@goodwalk.co.nz
15/06/2026 20:31:51 New Zealand Standard Time WARNING mail-api: [DEV] auth code for info@goodwalk.co.nz: 546348
15/06/2026 20:31:51 New Zealand Standard Time INFO mail-api: [ee1b53c2] POST /auth/request-code → 200 (2ms)
15/06/2026 20:32:01 New Zealand Standard Time INFO mail-api: [ac46d159] auth: session created for email=info@goodwalk.co.nz
15/06/2026 20:32:01 New Zealand Standard Time INFO mail-api: [ac46d159] POST /auth/verify-code → 200 (1ms)
15/06/2026 20:32:01 New Zealand Standard Time INFO mail-api: [60c7fd10] GET /auth/verify → 200 (0ms)
15/06/2026 20:32:01 New Zealand Standard Time INFO mail-api: [abfa833f] GET /owner/all-clients → 200 (2ms)
15/06/2026 20:32:01 New Zealand Standard Time INFO mail-api: [71044bf5] GET /owner/pending-onboarding → 200 (3ms)
15/06/2026 20:32:01 New Zealand Standard Time INFO mail-api: [2ec29972] GET /owner/birthdays → 200 (6ms)
15/06/2026 20:32:03 New Zealand Standard Time INFO mail-api: [6c0a3799] GET /owner/client-onboarding-view → 200 (1ms)
15/06/2026 20:32:10 New Zealand Standard Time INFO mail-api: [64cdfdf6] GET /auth/verify → 200 (0ms)
15/06/2026 20:32:10 New Zealand Standard Time INFO mail-api: [b8478985] GET /owner/pending-onboarding → 200 (1ms)
15/06/2026 20:32:10 New Zealand Standard Time INFO mail-api: [d5ec31df] GET /owner/all-clients → 200 (1ms)
15/06/2026 20:32:10 New Zealand Standard Time INFO mail-api: [0ebeb971] GET /owner/birthdays → 200 (1ms)
15/06/2026 20:32:11 New Zealand Standard Time INFO mail-api: [3b2f4ee0] GET /owner/client-onboarding-view → 200 (0ms)
15/06/2026 20:32:28 New Zealand Standard Time INFO mail-api: [fdc6a9fa] GET /owner/client-onboarding-view → 200 (0ms)
15/06/2026 20:32:53 New Zealand Standard Time INFO mail-api: [07b910be] GET /auth/verify → 200 (0ms)
15/06/2026 20:32:53 New Zealand Standard Time INFO mail-api: [eb7646b6] GET /owner/pending-onboarding → 200 (1ms)
15/06/2026 20:32:53 New Zealand Standard Time INFO mail-api: [e8b1a226] GET /owner/all-clients → 200 (1ms)
15/06/2026 20:32:53 New Zealand Standard Time INFO mail-api: [882b2a08] GET /owner/birthdays → 200 (0ms)
15/06/2026 20:33:23 New Zealand Standard Time INFO mail-api: [783d78c3] GET /auth/verify → 200 (0ms)
15/06/2026 20:33:23 New Zealand Standard Time INFO mail-api: [e8b66a3c] GET /owner/pending-onboarding → 200 (1ms)
15/06/2026 20:33:23 New Zealand Standard Time INFO mail-api: [020c91d9] GET /owner/all-clients → 200 (1ms)
15/06/2026 20:33:23 New Zealand Standard Time INFO mail-api: [91679cb8] GET /owner/birthdays → 200 (1ms)
15/06/2026 20:33:28 New Zealand Standard Time INFO mail-api: [e764f088] GET /owner/client-enquiry → 200 (1ms)
15/06/2026 20:33:29 New Zealand Standard Time INFO mail-api: [5089b29e] GET /owner/client-onboarding-view → 200 (0ms)
15/06/2026 20:33:34 New Zealand Standard Time INFO mail-api: [2c30ae12] GET /owner/message-templates → 200 (1ms)
15/06/2026 20:33:34 New Zealand Standard Time INFO mail-api: [2f086a39] POST /owner/render-message → 200 (2ms)
15/06/2026 20:33:36 New Zealand Standard Time INFO mail-api: [c383d4c2] GET /owner/activity → 200 (0ms)
15/06/2026 20:33:46 New Zealand Standard Time INFO mail-api: [8ce6215d] GET /auth/verify → 200 (0ms)
15/06/2026 20:33:46 New Zealand Standard Time INFO mail-api: [77dd8afc] GET /owner/all-clients → 200 (1ms)
15/06/2026 20:33:46 New Zealand Standard Time INFO mail-api: [5be1fa50] GET /owner/pending-onboarding → 200 (1ms)
15/06/2026 20:33:46 New Zealand Standard Time INFO mail-api: [69163ab8] GET /owner/birthdays → 200 (1ms)
15/06/2026 20:34:03 New Zealand Standard Time INFO mail-api: [ef3c519f] GET /owner/client-enquiry → 200 (0ms)
15/06/2026 20:34:05 New Zealand Standard Time INFO mail-api: [d547ff1d] GET /owner/client-onboarding-view → 200 (0ms)
15/06/2026 20:36:48 New Zealand Standard Time INFO mail-api: [0d37e5dc] GET /owner/client-enquiry → 200 (0ms)
15/06/2026 20:36:50 New Zealand Standard Time INFO mail-api: [24d3194d] GET /owner/client-onboarding-view → 200 (0ms)
15/06/2026 20:37:56 New Zealand Standard Time INFO mail-api: [1145dc87] GET /owner/message-templates → 200 (1ms)
15/06/2026 20:37:56 New Zealand Standard Time INFO mail-api: [681ad27d] POST /owner/render-message → 200 (1ms)
15/06/2026 20:37:57 New Zealand Standard Time INFO mail-api: [99d12925] GET /owner/activity → 200 (0ms)
15/06/2026 20:38:16 New Zealand Standard Time INFO mail-api: [823c2f81] GET /owner/client-onboarding-view → 200 (0ms)
15/06/2026 20:38:22 New Zealand Standard Time INFO mail-api: [1ee7e7fc] GET /owner/client-onboarding-view → 200 (1ms)
15/06/2026 20:38:37 New Zealand Standard Time INFO mail-api: [f3eed04b] GET /auth/verify → 200 (0ms)
15/06/2026 20:38:37 New Zealand Standard Time INFO mail-api: [2fcb1297] GET /owner/pending-onboarding → 200 (1ms)
15/06/2026 20:38:37 New Zealand Standard Time INFO mail-api: [f8c4996f] GET /owner/birthdays → 200 (2ms)
15/06/2026 20:38:37 New Zealand Standard Time INFO mail-api: [99f8e950] GET /owner/all-clients → 200 (2ms)
15/06/2026 20:38:41 New Zealand Standard Time INFO mail-api: [9da1e72a] GET /auth/verify → 200 (0ms)
15/06/2026 20:38:41 New Zealand Standard Time INFO mail-api: [697cad6a] GET /owner/pending-onboarding → 200 (1ms)
15/06/2026 20:38:41 New Zealand Standard Time INFO mail-api: [b5c6c30b] GET /owner/all-clients → 200 (1ms)
15/06/2026 20:38:41 New Zealand Standard Time INFO mail-api: [1c67730b] GET /owner/birthdays → 200 (0ms)
15/06/2026 20:38:50 New Zealand Standard Time INFO mail-api: [4b3db25d] GET /auth/verify → 200 (0ms)
15/06/2026 20:38:50 New Zealand Standard Time INFO mail-api: [b3feec86] GET /owner/pending-onboarding → 200 (1ms)
15/06/2026 20:38:50 New Zealand Standard Time INFO mail-api: [f47f7889] GET /owner/all-clients → 200 (1ms)
15/06/2026 20:38:50 New Zealand Standard Time INFO mail-api: [f15da397] GET /owner/birthdays → 200 (1ms)
15/06/2026 20:38:55 New Zealand Standard Time INFO mail-api: [65cae4c6] GET /auth/verify → 200 (1ms)
15/06/2026 20:38:55 New Zealand Standard Time INFO mail-api: [21ab02ac] GET /owner/pending-onboarding → 200 (1ms)
15/06/2026 20:38:55 New Zealand Standard Time INFO mail-api: [5a7c1435] GET /owner/all-clients → 200 (1ms)
15/06/2026 20:38:55 New Zealand Standard Time INFO mail-api: [e64d71b6] GET /owner/birthdays → 200 (2ms)
15/06/2026 20:39:01 New Zealand Standard Time INFO mail-api: [251265ca] GET /auth/verify → 200 (0ms)
15/06/2026 20:39:01 New Zealand Standard Time INFO mail-api: [40b2e542] GET /owner/pending-onboarding → 200 (1ms)
15/06/2026 20:39:01 New Zealand Standard Time INFO mail-api: [c9af86f0] GET /owner/birthdays → 200 (1ms)
15/06/2026 20:39:01 New Zealand Standard Time INFO mail-api: [e9c26722] GET /owner/all-clients → 200 (1ms)
15/06/2026 20:39:13 New Zealand Standard Time INFO mail-api: [a2afb433] GET /auth/verify → 200 (0ms)
15/06/2026 20:39:13 New Zealand Standard Time INFO mail-api: [195ded67] GET /owner/pending-onboarding → 200 (1ms)
15/06/2026 20:39:13 New Zealand Standard Time INFO mail-api: [212985b9] GET /owner/all-clients → 200 (1ms)
15/06/2026 20:39:13 New Zealand Standard Time INFO mail-api: [9cdd4476] GET /owner/birthdays → 200 (1ms)
15/06/2026 20:39:23 New Zealand Standard Time INFO mail-api: [0ba57c08] GET /auth/verify → 200 (0ms)
15/06/2026 20:39:23 New Zealand Standard Time INFO mail-api: [6eec0e7f] GET /owner/pending-onboarding → 200 (1ms)
15/06/2026 20:39:23 New Zealand Standard Time INFO mail-api: [f12a42d6] GET /owner/all-clients → 200 (1ms)
15/06/2026 20:39:23 New Zealand Standard Time INFO mail-api: [99a199e6] GET /owner/birthdays → 200 (0ms)
15/06/2026 20:39:39 New Zealand Standard Time INFO mail-api: [0a1bd987] GET /auth/verify → 200 (0ms)
15/06/2026 20:39:39 New Zealand Standard Time INFO mail-api: [261ccb15] GET /owner/pending-onboarding → 200 (0ms)
15/06/2026 20:39:39 New Zealand Standard Time INFO mail-api: [ef0c4698] GET /owner/all-clients → 200 (1ms)
15/06/2026 20:39:39 New Zealand Standard Time INFO mail-api: [819915a2] GET /owner/birthdays → 200 (1ms)
15/06/2026 20:39:57 New Zealand Standard Time INFO mail-api: [7f594e14] GET /owner/message-templates → 200 (1ms)
15/06/2026 20:39:57 New Zealand Standard Time INFO mail-api: [99e918ff] POST /owner/render-message → 200 (1ms)
15/06/2026 20:39:57 New Zealand Standard Time INFO mail-api: [c387447a] GET /owner/activity → 200 (0ms)
15/06/2026 20:40:03 New Zealand Standard Time INFO mail-api: [e225bdd8] GET /owner/activity → 200 (0ms)
15/06/2026 20:41:27 New Zealand Standard Time INFO mail-api: [fea1334d] GET /owner/activity → 200 (0ms)
15/06/2026 20:41:35 New Zealand Standard Time INFO mail-api: [0476565e] GET /auth/verify → 200 (0ms)
15/06/2026 20:41:35 New Zealand Standard Time INFO mail-api: [637f7274] GET /owner/pending-onboarding → 200 (1ms)
15/06/2026 20:41:35 New Zealand Standard Time INFO mail-api: [e2f2deb3] GET /owner/all-clients → 200 (1ms)
15/06/2026 20:41:35 New Zealand Standard Time INFO mail-api: [ada6cda9] GET /owner/birthdays → 200 (1ms)
15/06/2026 20:41:40 New Zealand Standard Time INFO mail-api: [8ed96de9] GET /owner/message-templates → 200 (1ms)
15/06/2026 20:41:40 New Zealand Standard Time INFO mail-api: [99ebfb37] POST /owner/render-message → 200 (1ms)
15/06/2026 20:41:43 New Zealand Standard Time INFO mail-api: [054a22e5] GET /owner/client-onboarding-view → 200 (1ms)
15/06/2026 20:41:50 New Zealand Standard Time INFO mail-api: [0331ffb5] GET /auth/verify → 200 (0ms)
15/06/2026 20:41:50 New Zealand Standard Time INFO mail-api: [0bfa7c1d] GET /owner/pending-onboarding → 200 (1ms)
15/06/2026 20:41:50 New Zealand Standard Time INFO mail-api: [7bff5a04] GET /owner/birthdays → 200 (1ms)
15/06/2026 20:41:50 New Zealand Standard Time INFO mail-api: [b3b90343] GET /owner/all-clients → 200 (2ms)
15/06/2026 20:41:55 New Zealand Standard Time INFO mail-api: [e7e42f0d] GET /owner/client-onboarding-view → 200 (0ms)
15/06/2026 20:42:09 New Zealand Standard Time INFO mail-api: [06c17820] GET /owner/client-onboarding-view → 200 (0ms)
15/06/2026 20:42:27 New Zealand Standard Time INFO mail-api: [fb40d429] GET /auth/verify → 200 (1ms)
15/06/2026 20:42:27 New Zealand Standard Time INFO mail-api: [93be80d1] GET /owner/pending-onboarding → 200 (1ms)
15/06/2026 20:42:27 New Zealand Standard Time INFO mail-api: [aacd6e15] GET /owner/all-clients → 200 (1ms)
15/06/2026 20:42:27 New Zealand Standard Time INFO mail-api: [1f59b652] GET /owner/birthdays → 200 (2ms)
15/06/2026 20:42:35 New Zealand Standard Time INFO mail-api: [f63506a6] GET /owner/message-templates → 200 (1ms)
15/06/2026 20:42:35 New Zealand Standard Time INFO mail-api: [388261bd] POST /owner/render-message → 200 (1ms)
15/06/2026 20:42:36 New Zealand Standard Time INFO mail-api: [b360a242] GET /owner/activity → 200 (1ms)
15/06/2026 20:42:41 New Zealand Standard Time INFO mail-api: [158bdb8d] GET /owner/client-enquiry → 200 (0ms)
15/06/2026 20:48:29 New Zealand Standard Time INFO mail-api: Logging initialised → console=INFO, file=logs\mail-api.log (DEBUG, rotating)
15/06/2026 20:48:29 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
15/06/2026 20:48:29 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
15/06/2026 20:48:30 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
15/06/2026 20:48:30 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
15/06/2026 20:48:30 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
15/06/2026 20:49:48 New Zealand Standard Time INFO mail-api: [545f781f] GET /auth/verify → 401 (2ms)
15/06/2026 20:52:31 New Zealand Standard Time INFO mail-api: [7ac75d9d] auth: code issued for email=info@goodwalk.co.nz
15/06/2026 20:52:31 New Zealand Standard Time WARNING mail-api: [DEV] auth code for info@goodwalk.co.nz: 718390
15/06/2026 20:52:31 New Zealand Standard Time INFO mail-api: [7ac75d9d] POST /auth/request-code → 200 (2ms)
15/06/2026 20:52:38 New Zealand Standard Time INFO mail-api: [48f12e62] auth: session created for email=info@goodwalk.co.nz
15/06/2026 20:52:38 New Zealand Standard Time INFO mail-api: [48f12e62] POST /auth/verify-code → 200 (2ms)
15/06/2026 20:52:38 New Zealand Standard Time INFO mail-api: [6057c23b] GET /auth/verify → 200 (0ms)
15/06/2026 20:52:38 New Zealand Standard Time INFO mail-api: [96abc272] GET /owner/pending-onboarding → 200 (2ms)
15/06/2026 20:52:38 New Zealand Standard Time INFO mail-api: [92cdf868] GET /owner/all-clients → 200 (2ms)
15/06/2026 20:52:38 New Zealand Standard Time INFO mail-api: [5fb5fdce] GET /owner/birthdays → 200 (12ms)
15/06/2026 20:52:50 New Zealand Standard Time INFO mail-api: [12d01bfe] GET /auth/verify → 200 (1ms)
15/06/2026 20:52:50 New Zealand Standard Time INFO mail-api: [3841ff56] GET /owner/pending-onboarding → 200 (1ms)
15/06/2026 20:52:50 New Zealand Standard Time INFO mail-api: [f920aade] GET /owner/all-clients → 200 (1ms)
15/06/2026 20:52:50 New Zealand Standard Time INFO mail-api: [7852a9d2] GET /owner/birthdays → 200 (1ms)
15/06/2026 20:53:03 New Zealand Standard Time INFO mail-api: [230b4b2b] GET /auth/verify → 200 (0ms)
15/06/2026 20:53:03 New Zealand Standard Time INFO mail-api: [4ba82776] GET /owner/pending-onboarding → 200 (1ms)
15/06/2026 20:53:03 New Zealand Standard Time INFO mail-api: [6fefad3f] GET /owner/all-clients → 200 (2ms)
15/06/2026 20:53:03 New Zealand Standard Time INFO mail-api: [acb28200] GET /owner/birthdays → 200 (2ms)
15/06/2026 20:53:22 New Zealand Standard Time INFO mail-api: [9839fe15] GET /owner/message-templates → 200 (1ms)
15/06/2026 20:53:22 New Zealand Standard Time INFO mail-api: [26e12e0e] POST /owner/render-message → 200 (1ms)
15/06/2026 20:53:23 New Zealand Standard Time INFO mail-api: [a4614459] GET /owner/activity → 200 (1ms)
15/06/2026 21:06:24 New Zealand Standard Time INFO mail-api: Logging initialised → console=INFO, file=logs\mail-api.log (DEBUG, rotating)
15/06/2026 21:06:24 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
15/06/2026 21:06:24 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
15/06/2026 21:06:24 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
15/06/2026 21:06:24 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
15/06/2026 21:06:24 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
15/06/2026 21:44:37 New Zealand Standard Time INFO mail-api: Logging initialised → console=INFO, file=logs\mail-api.log (DEBUG, rotating)
15/06/2026 21:44:37 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
15/06/2026 21:44:37 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
15/06/2026 21:44:37 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
15/06/2026 21:44:37 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
15/06/2026 21:44:37 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
15/06/2026 21:44:45 New Zealand Standard Time INFO mail-api: Logging initialised → console=INFO, file=logs\mail-api.log (DEBUG, rotating)
15/06/2026 21:44:45 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
15/06/2026 21:44:45 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
15/06/2026 21:44:45 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
15/06/2026 21:44:45 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
15/06/2026 21:44:45 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
15/06/2026 21:44:56 New Zealand Standard Time INFO mail-api: Logging initialised → console=INFO, file=logs\mail-api.log (DEBUG, rotating)
15/06/2026 21:44:56 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
15/06/2026 21:44:56 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
15/06/2026 21:44:56 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
15/06/2026 21:44:56 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
15/06/2026 21:44:56 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
15/06/2026 21:47:36 New Zealand Standard Time INFO mail-api: [68b31f4e] GET /auth/verify → 401 (2ms)
15/06/2026 21:47:40 New Zealand Standard Time INFO mail-api: [b6c5eb6f] auth: code issued for email=info@goodwalk.co.nz
15/06/2026 21:47:40 New Zealand Standard Time WARNING mail-api: [DEV] auth code for info@goodwalk.co.nz: 719493
15/06/2026 21:47:40 New Zealand Standard Time INFO mail-api: [b6c5eb6f] POST /auth/request-code → 200 (2ms)
15/06/2026 21:47:53 New Zealand Standard Time INFO mail-api: [d53657f2] auth: session created for email=info@goodwalk.co.nz
15/06/2026 21:47:53 New Zealand Standard Time INFO mail-api: [d53657f2] POST /auth/verify-code → 200 (1ms)
15/06/2026 21:47:53 New Zealand Standard Time INFO mail-api: [f183b75a] GET /auth/verify → 200 (0ms)
15/06/2026 21:47:53 New Zealand Standard Time INFO mail-api: [cb914009] GET /owner/all-clients → 200 (2ms)
15/06/2026 21:47:53 New Zealand Standard Time INFO mail-api: [e4eeae45] GET /owner/pending-onboarding → 200 (2ms)
15/06/2026 21:47:53 New Zealand Standard Time INFO mail-api: [05e1416f] GET /owner/birthdays → 200 (9ms)
15/06/2026 21:47:58 New Zealand Standard Time INFO mail-api: [bc6b815a] GET /owner/client-onboarding-view → 200 (8ms)
15/06/2026 21:49:47 New Zealand Standard Time INFO mail-api: Logging initialised → console=INFO, file=logs\mail-api.log (DEBUG, rotating)
15/06/2026 21:49:47 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
15/06/2026 21:49:47 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
15/06/2026 21:49:47 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
15/06/2026 21:49:47 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
15/06/2026 21:49:47 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
15/06/2026 21:50:00 New Zealand Standard Time INFO mail-api: Logging initialised → console=INFO, file=logs\mail-api.log (DEBUG, rotating)
15/06/2026 21:50:00 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
15/06/2026 21:50:00 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
15/06/2026 21:50:00 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
15/06/2026 21:50:00 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
15/06/2026 21:50:00 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
15/06/2026 21:52:22 New Zealand Standard Time INFO mail-api: [d28300fe] GET /owner/message-templates → 401 (3ms)
15/06/2026 21:52:23 New Zealand Standard Time INFO mail-api: [0ea10897] GET /owner/activity → 401 (1ms)
15/06/2026 21:52:24 New Zealand Standard Time INFO mail-api: [82beeffb] GET /owner/message-templates → 401 (0ms)
15/06/2026 21:52:28 New Zealand Standard Time INFO mail-api: [d4e90d6f] GET /auth/verify → 401 (1ms)
16/06/2026 06:29:29 New Zealand Standard Time INFO mail-api: Logging initialised → console=INFO, file=logs\mail-api.log (DEBUG, rotating)
16/06/2026 06:29:29 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
16/06/2026 06:29:29 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
16/06/2026 06:29:29 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
16/06/2026 06:29:29 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
16/06/2026 06:29:29 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
16/06/2026 06:30:42 New Zealand Standard Time INFO mail-api: Logging initialised -> console=INFO, file=logs\mail-api.log (DEBUG, rotating)
16/06/2026 06:30:42 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
16/06/2026 06:30:42 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
16/06/2026 06:30:42 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
16/06/2026 06:30:42 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
16/06/2026 06:30:42 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
16/06/2026 06:31:28 New Zealand Standard Time INFO mail-api: Logging initialised -> console=INFO, file=logs\mail-api.log (DEBUG, rotating)
16/06/2026 06:31:28 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
16/06/2026 06:31:28 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
16/06/2026 06:31:28 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
16/06/2026 06:31:28 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
16/06/2026 06:31:28 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
16/06/2026 06:38:14 New Zealand Standard Time INFO mail-api: [48563882] auth: code issued for email=info@goodwalk.co.nz
16/06/2026 06:38:14 New Zealand Standard Time WARNING mail-api: [DEV] auth code for info@goodwalk.co.nz: 926365
16/06/2026 06:38:14 New Zealand Standard Time INFO mail-api: [48563882] POST /auth/request-code → 200 (3ms)
16/06/2026 06:38:25 New Zealand Standard Time INFO mail-api: [5664ed1d] auth: session created for email=info@goodwalk.co.nz
16/06/2026 06:38:25 New Zealand Standard Time INFO mail-api: [5664ed1d] POST /auth/verify-code → 200 (3ms)
16/06/2026 06:38:25 New Zealand Standard Time INFO mail-api: [0bb03468] GET /auth/verify → 200 (1ms)
16/06/2026 06:38:25 New Zealand Standard Time INFO mail-api: [6f38622c] GET /owner/pending-onboarding → 200 (2ms)
16/06/2026 06:38:25 New Zealand Standard Time INFO mail-api: [3d0c8bcf] GET /owner/all-clients → 200 (10ms)
16/06/2026 06:38:25 New Zealand Standard Time INFO mail-api: [0b3eb923] GET /owner/birthdays → 200 (11ms)
16/06/2026 06:38:49 New Zealand Standard Time INFO mail-api: [1ea3d806] GET /owner/message-templates → 200 (1ms)
16/06/2026 06:38:49 New Zealand Standard Time INFO mail-api: [88793213] POST /owner/render-message → 200 (4ms)
16/06/2026 06:38:51 New Zealand Standard Time INFO mail-api: [fb1a9f6e] GET /owner/activity → 200 (1ms)
16/06/2026 06:40:29 New Zealand Standard Time INFO mail-api: [31e4240c] GET /owner/client-enquiry → 200 (2ms)
16/06/2026 06:41:47 New Zealand Standard Time INFO mail-api: [121f9f98] GET /owner/activity → 200 (0ms)
16/06/2026 06:44:02 New Zealand Standard Time INFO mail-api: [5a77951c] GET /auth/verify → 200 (0ms)
16/06/2026 06:44:02 New Zealand Standard Time INFO mail-api: [737112c8] GET /owner/pending-onboarding → 200 (1ms)
16/06/2026 06:44:02 New Zealand Standard Time INFO mail-api: [d9a6813f] GET /owner/all-clients → 200 (1ms)
16/06/2026 06:44:02 New Zealand Standard Time INFO mail-api: [46903a39] GET /owner/birthdays → 200 (2ms)
16/06/2026 06:45:04 New Zealand Standard Time INFO mail-api: [a241498f] GET /auth/verify → 200 (0ms)
16/06/2026 06:45:04 New Zealand Standard Time INFO mail-api: [a306e4a4] GET /owner/pending-onboarding → 200 (1ms)
16/06/2026 06:45:04 New Zealand Standard Time INFO mail-api: [6e060204] GET /owner/all-clients → 200 (1ms)
16/06/2026 06:45:04 New Zealand Standard Time INFO mail-api: [58ffe4c7] GET /owner/birthdays → 200 (3ms)
16/06/2026 06:45:17 New Zealand Standard Time INFO mail-api: [56c5f41a] GET /auth/verify → 200 (0ms)
16/06/2026 06:45:17 New Zealand Standard Time INFO mail-api: [f49f41b6] GET /owner/pending-onboarding → 200 (1ms)
16/06/2026 06:45:17 New Zealand Standard Time INFO mail-api: [bd4fd420] GET /owner/all-clients → 200 (1ms)
16/06/2026 06:45:17 New Zealand Standard Time INFO mail-api: [d4b0ada0] GET /owner/birthdays → 200 (2ms)
16/06/2026 06:50:33 New Zealand Standard Time INFO mail-api: [e2163ae1] GET /owner/client-enquiry → 200 (0ms)
16/06/2026 06:51:46 New Zealand Standard Time INFO mail-api: [16d4acbc] GET /owner/message-templates → 200 (1ms)
16/06/2026 06:51:46 New Zealand Standard Time INFO mail-api: [049712df] POST /owner/render-message → 200 (1ms)
16/06/2026 06:51:47 New Zealand Standard Time INFO mail-api: [ff1f1f13] GET /owner/activity → 200 (0ms)
16/06/2026 06:52:05 New Zealand Standard Time INFO mail-api: [e3758e0f] GET /owner/all-clients → 200 (1ms)
16/06/2026 06:52:51 New Zealand Standard Time INFO mail-api: [80d0c427] GET /owner/activity → 200 (0ms)
16/06/2026 06:54:39 New Zealand Standard Time INFO mail-api: [ae3c4fd3] GET /owner/activity → 200 (1ms)
16/06/2026 06:55:26 New Zealand Standard Time INFO mail-api: [d6b285f1] GET /owner/all-clients → 200 (1ms)
16/06/2026 06:55:30 New Zealand Standard Time INFO mail-api: [24446f12] GET /owner/client-enquiry → 200 (0ms)
16/06/2026 06:55:43 New Zealand Standard Time INFO mail-api: [f47fdfc6] GET /owner/activity → 200 (0ms)
16/06/2026 06:56:04 New Zealand Standard Time INFO mail-api: [ef4b6c2a] GET /owner/client-enquiry → 200 (0ms)
16/06/2026 06:56:21 New Zealand Standard Time INFO mail-api: [59ef0c42] GET /owner/activity → 200 (1ms)
16/06/2026 06:57:43 New Zealand Standard Time INFO mail-api: [ce3133ef] GET /auth/verify → 200 (0ms)
16/06/2026 06:57:43 New Zealand Standard Time INFO mail-api: [c6a5dfeb] GET /owner/all-clients → 200 (1ms)
16/06/2026 06:57:43 New Zealand Standard Time INFO mail-api: [de7d24a0] GET /owner/pending-onboarding → 200 (1ms)
16/06/2026 06:57:43 New Zealand Standard Time INFO mail-api: [bf7f5696] GET /owner/birthdays → 200 (0ms)
16/06/2026 06:57:46 New Zealand Standard Time INFO mail-api: [08a9d0a6] GET /owner/message-templates → 200 (1ms)
16/06/2026 06:57:46 New Zealand Standard Time INFO mail-api: [9388927c] POST /owner/render-message → 200 (1ms)
16/06/2026 07:00:13 New Zealand Standard Time INFO mail-api: [39b85a36] GET /owner/activity → 200 (0ms)
16/06/2026 07:02:06 New Zealand Standard Time INFO mail-api: [2fbeef76] GET /owner/all-clients → 200 (1ms)
16/06/2026 07:06:11 New Zealand Standard Time INFO mail-api: [1c8a494a] GET /owner/activity → 200 (1ms)
16/06/2026 07:06:42 New Zealand Standard Time INFO mail-api: [41ddcc81] GET /owner/activity → 200 (0ms)
16/06/2026 07:13:10 New Zealand Standard Time INFO mail-api: [93328ec4] GET /owner/all-clients → 200 (1ms)
16/06/2026 07:14:02 New Zealand Standard Time INFO mail-api: Logging initialised -> console=INFO, file=logs\mail-api.log (DEBUG, rotating)
16/06/2026 07:14:02 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
16/06/2026 07:14:02 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
16/06/2026 07:14:02 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
16/06/2026 07:14:02 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
16/06/2026 07:14:02 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
16/06/2026 07:14:08 New Zealand Standard Time INFO mail-api: Logging initialised -> console=INFO, file=logs\mail-api.log (DEBUG, rotating)
16/06/2026 07:14:08 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
16/06/2026 07:14:08 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
16/06/2026 07:14:08 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
16/06/2026 07:14:08 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
16/06/2026 07:14:08 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
16/06/2026 07:14:25 New Zealand Standard Time INFO mail-api: Logging initialised -> console=INFO, file=logs\mail-api.log (DEBUG, rotating)
16/06/2026 07:14:25 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
16/06/2026 07:14:31 New Zealand Standard Time INFO mail-api: Logging initialised -> console=INFO, file=logs\mail-api.log (DEBUG, rotating)
16/06/2026 07:14:31 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
16/06/2026 07:14:31 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
16/06/2026 07:14:31 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
16/06/2026 07:14:31 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
16/06/2026 07:14:31 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
16/06/2026 07:14:46 New Zealand Standard Time INFO mail-api: Logging initialised -> console=INFO, file=logs\mail-api.log (DEBUG, rotating)
16/06/2026 07:14:46 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
16/06/2026 07:14:46 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
16/06/2026 07:14:46 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
16/06/2026 07:14:46 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
16/06/2026 07:14:46 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
16/06/2026 07:14:59 New Zealand Standard Time INFO mail-api: Logging initialised -> console=INFO, file=logs\mail-api.log (DEBUG, rotating)
16/06/2026 07:14:59 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
16/06/2026 07:14:59 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
16/06/2026 07:14:59 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
16/06/2026 07:14:59 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
16/06/2026 07:14:59 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
16/06/2026 07:15:06 New Zealand Standard Time INFO mail-api: Logging initialised -> console=INFO, file=logs\mail-api.log (DEBUG, rotating)
16/06/2026 07:15:06 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
16/06/2026 07:15:06 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
16/06/2026 07:15:06 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
16/06/2026 07:15:06 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
16/06/2026 07:15:06 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
16/06/2026 07:15:24 New Zealand Standard Time INFO mail-api: Logging initialised -> console=INFO, file=logs\mail-api.log (DEBUG, rotating)
16/06/2026 07:15:24 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
16/06/2026 07:15:24 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
16/06/2026 07:15:24 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
16/06/2026 07:15:24 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
16/06/2026 07:15:24 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
16/06/2026 07:15:50 New Zealand Standard Time INFO mail-api: [825c3ae9] GET /auth/verify → 401 (2ms)
16/06/2026 07:15:54 New Zealand Standard Time INFO mail-api: [d2197895] auth: code issued for email=info@goodwalk.co.nz
16/06/2026 07:15:54 New Zealand Standard Time WARNING mail-api: [DEV] auth code for info@goodwalk.co.nz: 269988
16/06/2026 07:15:54 New Zealand Standard Time INFO mail-api: [d2197895] POST /auth/request-code → 200 (2ms)
16/06/2026 07:16:05 New Zealand Standard Time INFO mail-api: [f3567591] auth: session created for email=info@goodwalk.co.nz
16/06/2026 07:16:05 New Zealand Standard Time INFO mail-api: [f3567591] POST /auth/verify-code → 200 (2ms)
16/06/2026 07:16:05 New Zealand Standard Time INFO mail-api: [f568d6aa] GET /auth/verify → 200 (0ms)
16/06/2026 07:16:05 New Zealand Standard Time INFO mail-api: [15422225] GET /owner/pending-onboarding → 200 (2ms)
16/06/2026 07:16:05 New Zealand Standard Time INFO mail-api: [560b7831] GET /owner/all-clients → 200 (5ms)
16/06/2026 07:16:05 New Zealand Standard Time INFO mail-api: [b7db7b94] GET /owner/birthdays → 200 (5ms)
16/06/2026 07:16:20 New Zealand Standard Time INFO mail-api: [b1f1abfd] GET /owner/all-clients → 200 (1ms)
16/06/2026 07:16:46 New Zealand Standard Time INFO mail-api: [c80114ed] GET /owner/all-clients → 200 (1ms)
16/06/2026 07:16:57 New Zealand Standard Time INFO mail-api: [f47002dd] GET /owner/message-templates → 200 (1ms)
16/06/2026 07:16:57 New Zealand Standard Time INFO mail-api: [949a4211] POST /owner/render-message → 200 (2ms)
16/06/2026 07:17:07 New Zealand Standard Time INFO mail-api: Logging initialised -> console=INFO, file=logs\mail-api.log (DEBUG, rotating)
16/06/2026 07:17:07 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
16/06/2026 07:17:07 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
16/06/2026 07:17:07 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
16/06/2026 07:17:07 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
16/06/2026 07:17:07 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
16/06/2026 07:17:13 New Zealand Standard Time INFO mail-api: Logging initialised -> console=INFO, file=logs\mail-api.log (DEBUG, rotating)
16/06/2026 07:17:13 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
16/06/2026 07:17:13 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
16/06/2026 07:17:13 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
16/06/2026 07:17:13 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
16/06/2026 07:17:13 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
16/06/2026 07:17:20 New Zealand Standard Time INFO mail-api: Logging initialised -> console=INFO, file=logs\mail-api.log (DEBUG, rotating)
16/06/2026 07:17:20 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
16/06/2026 07:17:20 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
16/06/2026 07:17:20 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
16/06/2026 07:17:20 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
16/06/2026 07:17:20 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
16/06/2026 07:17:27 New Zealand Standard Time INFO mail-api: Logging initialised -> console=INFO, file=logs\mail-api.log (DEBUG, rotating)
16/06/2026 07:17:27 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
16/06/2026 07:17:27 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
16/06/2026 07:17:27 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
16/06/2026 07:17:27 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
16/06/2026 07:17:27 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
16/06/2026 07:18:01 New Zealand Standard Time INFO mail-api: [8cdf225d] GET /auth/verify → 401 (2ms)
16/06/2026 07:18:05 New Zealand Standard Time INFO mail-api: [cbe1d0d9] auth: unknown email=info@goodwalk.co.z ip=127.0.0.1
16/06/2026 07:18:05 New Zealand Standard Time WARNING mail-api: [cbe1d0d9] auth: failure ip=127.0.0.1 reason='unknown_email' total_in_window=1
16/06/2026 07:18:05 New Zealand Standard Time INFO mail-api: [cbe1d0d9] POST /auth/request-code → 403 (2ms)
16/06/2026 07:18:08 New Zealand Standard Time INFO mail-api: [008f2e1a] auth: code issued for email=info@goodwalk.co.nz
16/06/2026 07:18:08 New Zealand Standard Time WARNING mail-api: [DEV] auth code for info@goodwalk.co.nz: 952701
16/06/2026 07:18:08 New Zealand Standard Time INFO mail-api: [008f2e1a] POST /auth/request-code → 200 (1ms)
16/06/2026 07:18:14 New Zealand Standard Time INFO mail-api: Logging initialised -> console=INFO, file=logs\mail-api.log (DEBUG, rotating)
16/06/2026 07:18:14 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
16/06/2026 07:18:14 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
16/06/2026 07:18:14 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
16/06/2026 07:18:14 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
16/06/2026 07:18:14 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
16/06/2026 07:18:20 New Zealand Standard Time INFO mail-api: [369f9969] auth: code issued for email=info@goodwalk.co.nz
16/06/2026 07:18:20 New Zealand Standard Time WARNING mail-api: [DEV] auth code for info@goodwalk.co.nz: 943074
16/06/2026 07:18:20 New Zealand Standard Time INFO mail-api: [369f9969] POST /auth/request-code → 200 (3ms)
16/06/2026 07:18:31 New Zealand Standard Time INFO mail-api: [ceb1fc90] auth: code issued for email=info@goodwalk.co.nz
16/06/2026 07:18:31 New Zealand Standard Time WARNING mail-api: [DEV] auth code for info@goodwalk.co.nz: 261983
16/06/2026 07:18:31 New Zealand Standard Time INFO mail-api: [ceb1fc90] POST /auth/request-code → 200 (1ms)
16/06/2026 07:25:18 New Zealand Standard Time INFO mail-api: Logging initialised -> console=INFO, file=logs\mail-api.log (DEBUG, rotating)
16/06/2026 07:25:18 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
16/06/2026 07:25:18 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
16/06/2026 07:25:18 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
16/06/2026 07:25:18 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
16/06/2026 07:25:18 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
16/06/2026 07:25:20 New Zealand Standard Time INFO mail-api: Logging initialised -> console=INFO, file=logs\mail-api.log (DEBUG, rotating)
16/06/2026 07:25:20 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
16/06/2026 07:25:20 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
16/06/2026 07:25:20 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
16/06/2026 07:25:20 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
16/06/2026 07:25:20 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
16/06/2026 07:25:28 New Zealand Standard Time INFO mail-api: [098d12dc] auth: code issued for email=info@goodwalk.co.nz
16/06/2026 07:25:28 New Zealand Standard Time WARNING mail-api: [DEV] auth code for info@goodwalk.co.nz: 496267
16/06/2026 07:25:28 New Zealand Standard Time INFO mail-api: [098d12dc] POST /auth/request-code → 200 (4ms)
16/06/2026 07:25:35 New Zealand Standard Time INFO mail-api: [8eb8c518] auth: session created for email=info@goodwalk.co.nz
16/06/2026 07:25:35 New Zealand Standard Time INFO mail-api: [8eb8c518] POST /auth/verify-code → 200 (2ms)
16/06/2026 07:25:35 New Zealand Standard Time INFO mail-api: [b4bcbfbd] GET /auth/verify → 200 (1ms)
16/06/2026 07:25:35 New Zealand Standard Time INFO mail-api: [37f1c7a9] GET /owner/pending-onboarding → 200 (2ms)
16/06/2026 07:25:35 New Zealand Standard Time INFO mail-api: [0daa6a9c] GET /owner/all-clients → 200 (7ms)
16/06/2026 07:25:35 New Zealand Standard Time INFO mail-api: [40680736] GET /owner/birthdays → 200 (7ms)
16/06/2026 07:25:37 New Zealand Standard Time INFO mail-api: [e9db9842] POST /owner/render-welcome-pack → 200 (14ms)
16/06/2026 07:25:44 New Zealand Standard Time INFO mail-api: [13fbb03a] POST /owner/render-welcome-pack → 200 (1ms)
16/06/2026 07:25:49 New Zealand Standard Time INFO mail-api: [fb8e73e3] POST /owner/render-welcome-pack → 200 (1ms)
16/06/2026 07:25:51 New Zealand Standard Time INFO mail-api: [22db529c] POST /owner/render-welcome-pack → 200 (1ms)
16/06/2026 07:26:13 New Zealand Standard Time INFO mail-api: [806f9017] GET /owner/scheduled-emails → 200 (2ms)
16/06/2026 07:26:15 New Zealand Standard Time INFO mail-api: [2864b4ca] GET /owner/message-templates → 200 (1ms)
16/06/2026 07:26:15 New Zealand Standard Time INFO mail-api: [f133698c] POST /owner/render-message → 200 (3ms)
16/06/2026 07:47:45 New Zealand Standard Time INFO mail-api: [11b1a1dd] POST /owner/render-welcome-pack → 200 (1ms)
16/06/2026 07:48:20 New Zealand Standard Time INFO mail-api: [03817871] GET /auth/verify → 200 (0ms)
16/06/2026 07:48:20 New Zealand Standard Time INFO mail-api: [1b5b54f8] GET /owner/pending-onboarding → 200 (1ms)
16/06/2026 07:48:20 New Zealand Standard Time INFO mail-api: [d0736f04] GET /owner/all-clients → 200 (1ms)
16/06/2026 07:48:20 New Zealand Standard Time INFO mail-api: [4919bec5] GET /owner/birthdays → 200 (1ms)
16/06/2026 07:48:38 New Zealand Standard Time INFO mail-api: [3c6b1f63] GET /auth/verify → 200 (0ms)
16/06/2026 07:48:38 New Zealand Standard Time INFO mail-api: [c1ea7cf1] GET /owner/pending-onboarding → 200 (1ms)
16/06/2026 07:48:38 New Zealand Standard Time INFO mail-api: [a3a06d7a] GET /owner/all-clients → 200 (1ms)
16/06/2026 07:48:38 New Zealand Standard Time INFO mail-api: [c9a5683b] GET /owner/birthdays → 200 (2ms)
16/06/2026 07:51:48 New Zealand Standard Time INFO mail-api: Logging initialised -> console=INFO, file=logs\mail-api.log (DEBUG, rotating)
16/06/2026 07:51:48 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
16/06/2026 07:51:48 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
16/06/2026 07:51:48 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
16/06/2026 07:51:48 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
16/06/2026 07:51:48 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
16/06/2026 07:51:52 New Zealand Standard Time INFO mail-api: Logging initialised -> console=INFO, file=logs\mail-api.log (DEBUG, rotating)
16/06/2026 07:51:52 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
16/06/2026 07:51:53 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
16/06/2026 07:51:53 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
16/06/2026 07:51:53 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
16/06/2026 07:51:53 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
16/06/2026 07:51:56 New Zealand Standard Time INFO mail-api: Logging initialised -> console=INFO, file=logs\mail-api.log (DEBUG, rotating)
16/06/2026 07:51:56 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
16/06/2026 07:51:56 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
16/06/2026 07:51:56 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
16/06/2026 07:51:56 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
16/06/2026 07:51:56 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
16/06/2026 07:52:00 New Zealand Standard Time INFO mail-api: Logging initialised -> console=INFO, file=logs\mail-api.log (DEBUG, rotating)
16/06/2026 07:52:00 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='info@goodwalk.co.nz' cp_admins=['info@goodwalk.co.nz'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
16/06/2026 07:52:00 New Zealand Standard Time INFO mail-api: Auth: loaded 3 allowed email(s)
16/06/2026 07:52:00 New Zealand Standard Time ERROR mail-api: Startup smoke: WeasyPrint UNAVAILABLE — PDF attachments will be skipped (No module named 'weasyprint')
16/06/2026 07:52:00 New Zealand Standard Time WARNING mail-api: Startup smoke: postgres disabled — activity/submissions will NOT be recorded
16/06/2026 07:52:00 New Zealand Standard Time INFO mail-api: Startup test email skipped: OWNER_BCC is not set to a real address
16/06/2026 07:52:54 New Zealand Standard Time INFO mail-api: [01084365] GET /owner/client-onboarding-view → 401 (3ms)
16/06/2026 07:52:55 New Zealand Standard Time INFO mail-api: [117b9eb3] GET /auth/verify → 401 (1ms)
16/06/2026 07:52:59 New Zealand Standard Time INFO mail-api: [bee5ac03] auth: code issued for email=info@goodwalk.co.nz
16/06/2026 07:52:59 New Zealand Standard Time WARNING mail-api: [DEV] auth code for info@goodwalk.co.nz: 948798
16/06/2026 07:52:59 New Zealand Standard Time INFO mail-api: [bee5ac03] POST /auth/request-code → 200 (2ms)
16/06/2026 07:53:09 New Zealand Standard Time INFO mail-api: [d782d75d] auth: session created for email=info@goodwalk.co.nz
16/06/2026 07:53:09 New Zealand Standard Time INFO mail-api: [d782d75d] POST /auth/verify-code → 200 (3ms)
16/06/2026 07:53:09 New Zealand Standard Time INFO mail-api: [a8c2db4a] GET /auth/verify → 200 (0ms)
16/06/2026 07:53:09 New Zealand Standard Time INFO mail-api: [a2bc9b29] GET /owner/pending-onboarding → 200 (9ms)
16/06/2026 07:53:09 New Zealand Standard Time INFO mail-api: [110111bd] GET /owner/birthdays → 200 (9ms)
16/06/2026 07:53:09 New Zealand Standard Time INFO mail-api: [2fe1e0d1] GET /owner/all-clients → 200 (10ms)
16/06/2026 07:53:12 New Zealand Standard Time INFO mail-api: [3f755a79] GET /owner/client-onboarding-view → 200 (0ms)
17/06/2026 07:22:07 New Zealand Standard Time INFO mail-api: Logging initialised -> console=INFO, file=logs\mail-api.log (DEBUG, rotating)
17/06/2026 07:22:07 New Zealand Standard Time INFO mail-api: Mail API config: version='unknown' timezone='system-default' from='GoodWalk <info@goodwalk.co.nz>' reply_to='info@goodwalk.co.nz' owner='dev@localhost' cp_admins=['dev@localhost'] owner_bcc='' client_bcc='' general_enquiries=False max_attempts=3 form_min=1s form_max=7200s rate_window=900s per_ip=50 per_email=50 min_interval=1s send_timeout=20s
17/06/2026 07:22:07 New Zealand Standard Time INFO mail-api: MYOB integration: disabled (set MYOB_* env vars to enable)
Binary file not shown.
Binary file not shown.
Binary file not shown.
+31 -1
View File
@@ -48,7 +48,7 @@ def setup_logging() -> logging.Logger:
root.addHandler(rotating) root.addHandler(rotating)
log = logging.getLogger("mail-api") log = logging.getLogger("mail-api")
log.info("Logging initialised console=INFO, file=%s (DEBUG, rotating)", log_file) log.info("Logging initialised -> console=INFO, file=%s (DEBUG, rotating)", log_file)
return log return log
@@ -185,6 +185,10 @@ AUTH_IP_MAX_FAILURES = max(3, int(os.environ.get("AUTH_IP_MAX_FAILURES", "10")))
AUTH_IP_FAILURE_WINDOW = max(60, int(os.environ.get("AUTH_IP_FAILURE_WINDOW", "600"))) AUTH_IP_FAILURE_WINDOW = max(60, int(os.environ.get("AUTH_IP_FAILURE_WINDOW", "600")))
AUTH_IP_BLOCK_DURATION = max(60, int(os.environ.get("AUTH_IP_BLOCK_DURATION", "3600"))) AUTH_IP_BLOCK_DURATION = max(60, int(os.environ.get("AUTH_IP_BLOCK_DURATION", "3600")))
BIRTHDAY_CHECK_INTERVAL_SECONDS = max(3600, int(os.environ.get("BIRTHDAY_CHECK_INTERVAL_SECONDS", str(12 * 3600)))) BIRTHDAY_CHECK_INTERVAL_SECONDS = max(3600, int(os.environ.get("BIRTHDAY_CHECK_INTERVAL_SECONDS", str(12 * 3600))))
# How often the scheduled-email worker wakes to dispatch any sends that have
# come due. Scheduled welcome emails need finer granularity than the birthday
# sweep, so this defaults to one minute (floored at 15s).
SCHEDULED_CHECK_INTERVAL_SECONDS = max(15, int(os.environ.get("SCHEDULED_CHECK_INTERVAL_SECONDS", "60")))
def _split_csv_env(name: str, default: str) -> tuple[str, ...]: def _split_csv_env(name: str, default: str) -> tuple[str, ...]:
@@ -211,6 +215,10 @@ _DATA_DIR = Path(os.environ.get("DATA_DIR", "data"))
ALLOWED_EMAILS_FILE = _DATA_DIR / "allowed_emails.json" ALLOWED_EMAILS_FILE = _DATA_DIR / "allowed_emails.json"
CLIENT_PROFILES_FILE = _DATA_DIR / "client_profiles.json" CLIENT_PROFILES_FILE = _DATA_DIR / "client_profiles.json"
DRAFTS_FILE = _DATA_DIR / "drafts.json" DRAFTS_FILE = _DATA_DIR / "drafts.json"
# Durable queue of owner-scheduled emails (welcome packs queued for later send).
# Mirrors the client_profiles persistence model: postgres admin_kv is canonical
# in production, with this JSON file as the dev/local fallback and seed source.
SCHEDULED_EMAILS_FILE = _DATA_DIR / "scheduled_emails.json"
# Legacy seed lives OUTSIDE the data volume — it's shipped in the image so a # Legacy seed lives OUTSIDE the data volume — it's shipped in the image so a
# fresh deploy always carries the same baked-in copy. On boot the mail-api # fresh deploy always carries the same baked-in copy. On boot the mail-api
# merges this dict into _client_profiles, adding any emails that aren't # merges this dict into _client_profiles, adding any emails that aren't
@@ -221,6 +229,27 @@ LEGACY_SEED_FILE = Path(
LOGO_URL = "https://www.goodwalk.co.nz/images/goodwalk-auckland-dog-walking-logo.png" LOGO_URL = "https://www.goodwalk.co.nz/images/goodwalk-auckland-dog-walking-logo.png"
# ── MYOB integration ───────────────────────────────────────────────────────────
# Optional: create new clients as customer contacts in MYOB AccountRight Live.
# The integration stays disabled until every required value is present, so an
# unconfigured environment behaves exactly as before.
#
# MYOB_API_KEY / MYOB_API_SECRET the developer app's OAuth client id/secret
# MYOB_REFRESH_TOKEN long-lived token used to mint access tokens
# MYOB_COMPANY_FILE_URI base API URI of the target company file,
# e.g. https://api.myob.com/accountright/<guid>
# MYOB_CF_USERNAME / MYOB_CF_PASSWORD company-file login (password may be blank)
# MYOB_TOKEN_URL OAuth token endpoint (override only for tests)
MYOB_API_KEY = (os.environ.get("MYOB_API_KEY") or "").strip()
MYOB_API_SECRET = (os.environ.get("MYOB_API_SECRET") or "").strip()
MYOB_REFRESH_TOKEN = (os.environ.get("MYOB_REFRESH_TOKEN") or "").strip()
MYOB_COMPANY_FILE_URI = (os.environ.get("MYOB_COMPANY_FILE_URI") or "").strip().rstrip("/")
MYOB_CF_USERNAME = (os.environ.get("MYOB_CF_USERNAME") or "Administrator").strip()
MYOB_CF_PASSWORD = os.environ.get("MYOB_CF_PASSWORD", "")
MYOB_TOKEN_URL = (os.environ.get("MYOB_TOKEN_URL") or "https://secure.myob.com/oauth2/v1/authorize").strip()
MYOB_HTTP_TIMEOUT_SECONDS = max(5, int(os.environ.get("MYOB_HTTP_TIMEOUT_SECONDS", "20")))
MYOB_ENABLED = bool(MYOB_API_KEY and MYOB_API_SECRET and MYOB_REFRESH_TOKEN and MYOB_COMPANY_FILE_URI)
# ── Legacy module constants (kept for compatibility with the existing main.py) ── # ── Legacy module constants (kept for compatibility with the existing main.py) ──
OWNER_EMAIL = settings.owner_email OWNER_EMAIL = settings.owner_email
@@ -268,3 +297,4 @@ logger.info(
RATE_LIMIT_MIN_INTERVAL_SECONDS, RATE_LIMIT_MIN_INTERVAL_SECONDS,
EMAIL_SEND_TIMEOUT_SECONDS, EMAIL_SEND_TIMEOUT_SECONDS,
) )
logger.info("MYOB integration: %s", "enabled" if MYOB_ENABLED else "disabled (set MYOB_* env vars to enable)")
+59
View File
@@ -41,9 +41,13 @@ class OnboardingSubmission(BaseSubmission):
medicalNotes: str = "" medicalNotes: str = ""
accessInstructions: str = "" accessInstructions: str = ""
vetName: str vetName: str
vetAddress: str
vetPhone: str vetPhone: str
emergencyContactName: str emergencyContactName: str
emergencyContactPhone: str emergencyContactPhone: str
regularFleaTickTreatment: str = ""
petInsurance: str = ""
petInsuranceOwnerExpenseAccepted: bool = False
councilRegistrationConfirmed: bool = False councilRegistrationConfirmed: bool = False
vaccinationsConfirmed: bool = False vaccinationsConfirmed: bool = False
emergencyVetConsent: bool = False emergencyVetConsent: bool = False
@@ -54,15 +58,33 @@ class OnboardingSubmission(BaseSubmission):
class WelcomePackEmailRequest(BaseModel): class WelcomePackEmailRequest(BaseModel):
email: EmailStr email: EmailStr
# Owner-set subject line. Defaults to "Goodwalk Onboarding - <Dog Name>" when
# blank (resolved server-side so scheduled sends keep a sensible subject too).
subject: str = ""
serviceType: str serviceType: str
priceDetails: str priceDetails: str
startDate: str startDate: str
preview: bool = False preview: bool = False
previewRecipients: list[EmailStr] = []
# ISO 8601 local datetime (e.g. "2026-06-20T14:30"). When set on a non-preview
# request, the welcome email is queued for reliable delivery at that time
# instead of being sent immediately.
scheduledFor: str | None = None
class ScheduledEmailCancelRequest(BaseModel):
id: str
class ScheduledEmailRescheduleRequest(BaseModel):
id: str
scheduledFor: str
class BirthdayEmailRequest(BaseModel): class BirthdayEmailRequest(BaseModel):
email: EmailStr email: EmailStr
preview: bool = False preview: bool = False
previewRecipients: list[EmailStr] = []
class BirthdayAutoSendRequest(BaseModel): class BirthdayAutoSendRequest(BaseModel):
@@ -81,6 +103,42 @@ class ClientStatusUpdate(BaseModel):
reason: str = "" reason: str = ""
class ClientProfileUpdate(BaseModel):
email: EmailStr
nextEmail: EmailStr
fullName: str
phone: str = ""
address: str = ""
dogName: str
class ResetOnboardingRequest(BaseModel):
"""Owner-initiated reset: keep the client's saved details but mark their
onboarding incomplete so they can sign in and complete the new form
(used for clients imported from the legacy Gravity Forms data)."""
email: EmailStr
class NewClientRequest(BaseModel):
"""Owner-initiated client creation (e.g. leads from Instagram / Facebook
that never came through the public enquiry form). Registers the email so
the client can access the onboarding form, and seeds a profile."""
email: EmailStr
fullName: str
phone: str = ""
address: str = ""
dogName: str = ""
dogBreed: str = ""
# Where the client came from, e.g. "Instagram", "Facebook", "Referral".
source: str = ""
# When true (and the MYOB integration is configured), also create the client
# as a customer contact in MYOB. Non-fatal: a MYOB failure never blocks the
# local client from being created.
createInMyob: bool = False
class ContractSubmission(BaseSubmission): class ContractSubmission(BaseSubmission):
address: str address: str
dogName: str dogName: str
@@ -126,3 +184,4 @@ class SendMessageRequest(BaseModel):
fontId: str = "system" fontId: str = "system"
recipients: list[EmailStr] = [] recipients: list[EmailStr] = []
preview: bool = False preview: bool = False
previewRecipients: list[EmailStr] = []
+212
View File
@@ -0,0 +1,212 @@
"""MYOB AccountRight Live integration — create customer contacts.
Used when the owner ticks "Also create in MYOB" while adding a new client in the
control panel. The integration is optional: when the required environment
variables are not set, :func:`is_configured` returns ``False`` and the caller
skips MYOB entirely (the local client is still created).
Auth model (MYOB OAuth 2.0):
* A developer key/secret (``client_id`` / ``client_secret``) identifies this
app to MYOB.
* A long-lived refresh token mints short-lived bearer access tokens. We cache
the access token in memory and refresh it shortly before it expires.
* Each AccountRight company file lives at its own base URI and is gated by a
company-file token: ``base64("username:password")`` sent as the
``x-myobapi-cftoken`` header (password may be blank).
All HTTP calls are synchronous (``requests``); the async app invokes the public
helpers via ``asyncio.to_thread`` so the event loop is never blocked.
"""
from __future__ import annotations
import base64
import threading
import time
import requests
from mail_api.config import (
MYOB_API_KEY,
MYOB_API_SECRET,
MYOB_CF_PASSWORD,
MYOB_CF_USERNAME,
MYOB_COMPANY_FILE_URI,
MYOB_ENABLED,
MYOB_HTTP_TIMEOUT_SECONDS,
MYOB_REFRESH_TOKEN,
MYOB_TOKEN_URL,
logger,
)
class MyobError(Exception):
"""Raised when a MYOB API call cannot be completed."""
# Access tokens are short-lived (MYOB returns ~20 min). Cache the current token
# and its expiry so back-to-back client creations reuse a single refresh.
_token_lock = threading.Lock()
_cached_token: str = ""
_cached_token_expires_at: float = 0.0
# Refresh a little early so a token never expires mid-request.
_TOKEN_EXPIRY_SKEW_SECONDS = 60
def is_configured() -> bool:
"""True when every credential needed to talk to MYOB is present."""
return MYOB_ENABLED
def _cf_token() -> str:
raw = f"{MYOB_CF_USERNAME}:{MYOB_CF_PASSWORD}".encode("utf-8")
return base64.b64encode(raw).decode("ascii")
def _refresh_access_token() -> str:
"""Exchange the stored refresh token for a fresh access token.
Caches the result in module state. Raises :class:`MyobError` on failure.
"""
global _cached_token, _cached_token_expires_at
try:
resp = requests.post(
MYOB_TOKEN_URL,
data={
"client_id": MYOB_API_KEY,
"client_secret": MYOB_API_SECRET,
"grant_type": "refresh_token",
"refresh_token": MYOB_REFRESH_TOKEN,
},
headers={"Content-Type": "application/x-www-form-urlencoded"},
timeout=MYOB_HTTP_TIMEOUT_SECONDS,
)
except requests.RequestException as exc:
raise MyobError(f"Could not reach the MYOB token service: {exc}") from exc
if resp.status_code != 200:
raise MyobError(
f"MYOB token refresh failed (HTTP {resp.status_code}). "
"Check MYOB_API_KEY / MYOB_API_SECRET / MYOB_REFRESH_TOKEN."
)
try:
payload = resp.json()
except ValueError as exc:
raise MyobError("MYOB token service returned an unreadable response.") from exc
token = str(payload.get("access_token") or "").strip()
if not token:
raise MyobError("MYOB token service did not return an access token.")
try:
expires_in = int(payload.get("expires_in", 1200))
except (TypeError, ValueError):
expires_in = 1200
_cached_token = token
_cached_token_expires_at = time.monotonic() + max(0, expires_in - _TOKEN_EXPIRY_SKEW_SECONDS)
logger.info("MYOB: access token refreshed (valid ~%ds)", expires_in)
return token
def _access_token() -> str:
with _token_lock:
if _cached_token and time.monotonic() < _cached_token_expires_at:
return _cached_token
return _refresh_access_token()
def _api_headers() -> dict[str, str]:
return {
"Authorization": f"Bearer {_access_token()}",
"x-myobapi-key": MYOB_API_KEY,
"x-myobapi-version": "v2",
"x-myobapi-cftoken": _cf_token(),
"Content-Type": "application/json",
"Accept": "application/json",
}
def _split_name(full_name: str) -> tuple[str, str]:
"""Best-effort split of a single owner name into first/last for MYOB."""
parts = full_name.split()
if not parts:
return ("", "")
if len(parts) == 1:
return (parts[0], "")
return (parts[0], " ".join(parts[1:]))
def create_customer(
*,
email: str,
full_name: str,
phone: str = "",
street: str = "",
source: str = "",
) -> dict[str, str]:
"""Create an individual customer contact in MYOB.
Returns ``{"uid": ..., "uri": ...}`` parsed from the ``Location`` header MYOB
returns on a successful create. Raises :class:`MyobError` on any failure so
the caller can record it without aborting the local client creation.
"""
if not is_configured():
raise MyobError("MYOB integration is not configured.")
first_name, last_name = _split_name(full_name)
body = {
"IsIndividual": True,
"FirstName": first_name,
"LastName": last_name,
"IsActive": True,
"Addresses": [
{
"Location": 1,
"Email": email,
"Phone1": phone,
"Street": street,
}
],
}
if source:
body["Notes"] = f"Added via Goodwalk control panel — source: {source}"
url = f"{MYOB_COMPANY_FILE_URI}/Contact/Customer"
try:
resp = requests.post(
url,
json=body,
headers=_api_headers(),
timeout=MYOB_HTTP_TIMEOUT_SECONDS,
)
except requests.RequestException as exc:
raise MyobError(f"Could not reach MYOB: {exc}") from exc
# 200/201 both indicate success depending on API version; MYOB returns the
# new record's URI in the Location header.
if resp.status_code not in (200, 201):
detail = _error_detail(resp)
raise MyobError(f"MYOB rejected the customer (HTTP {resp.status_code}): {detail}")
location = resp.headers.get("Location", "")
uid = location.rstrip("/").rsplit("/", 1)[-1] if location else ""
logger.info("MYOB: created customer uid=%s for %s", uid or "?", email)
return {"uid": uid, "uri": location}
def _error_detail(resp: requests.Response) -> str:
"""Extract a human-readable error message from a MYOB error response."""
try:
data = resp.json()
except ValueError:
return (resp.text or "").strip()[:300] or "no detail provided"
errors = data.get("Errors") if isinstance(data, dict) else None
if isinstance(errors, list) and errors:
messages = [str(e.get("Message", "")).strip() for e in errors if isinstance(e, dict)]
joined = "; ".join(m for m in messages if m)
if joined:
return joined
return str(data)[:300]
+910 -66
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -4,3 +4,4 @@ resend>=2.0
pydantic[email]>=2.10 pydantic[email]>=2.10
asyncpg>=0.30 asyncpg>=0.30
weasyprint>=63 weasyprint>=63
requests>=2.32
+15
View File
@@ -0,0 +1,15 @@
$ErrorActionPreference = 'Stop'
Set-Location $PSScriptRoot
$env:DEV_MODE = '1'
if (-not $env:OWNER_EMAIL) {
$env:OWNER_EMAIL = 'info@goodwalk.co.nz'
}
$python = Join-Path $PSScriptRoot '.venv\Scripts\python.exe'
if (-not (Test-Path $python)) {
throw "Missing virtualenv interpreter at $python. Create it first with: py -3.14 -m venv .venv"
}
& $python -m uvicorn main:app --host 127.0.0.1 --port 8000 --reload
+54 -132
View File
@@ -4,14 +4,19 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="noindex" /> <meta name="robots" content="noindex" />
<title>Be right back | Goodwalk</title> <title>Back soon | Goodwalk</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Readex+Pro:wght@400;500&family=Unbounded:wght@700;800&display=swap"
rel="stylesheet"
/>
<style> <style>
:root { :root {
--green: #213021; --green: #213021;
--green-soft: #2c3f2c;
--yellow: #ffd100; --yellow: #ffd100;
--ink: #1a1a1a; --off-white: #f8f7f2;
--muted: #4c5056; --line: rgba(255, 255, 255, 0.16);
} }
* { * {
@@ -21,166 +26,83 @@
html, html,
body { body {
margin: 0; margin: 0;
padding: 0; min-height: 100%;
height: 100%;
} }
body { body {
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
color: var(--ink);
background:
radial-gradient(circle at top left, rgba(255, 209, 0, 0.12), transparent 55%),
radial-gradient(circle at bottom right, rgba(255, 209, 0, 0.08), transparent 60%),
var(--green);
min-height: 100vh;
display: grid; display: grid;
place-items: center; place-items: center;
padding: 32px; padding: 24px;
line-height: 1.5; background: var(--green);
color: var(--off-white);
font-family: "Readex Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
line-height: 1.6;
} }
.stage { main {
width: 100%; width: min(100%, 30rem);
max-width: 560px;
}
.brand-bar {
display: flex;
justify-content: center;
margin-bottom: 28px;
}
.brand-bar img {
display: block;
height: 44px;
width: auto;
max-width: 100%;
filter: brightness(0) invert(1);
}
.card {
position: relative;
background: #fff;
border-radius: 28px;
padding: 44px 40px 38px;
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.25);
text-align: center; text-align: center;
overflow: hidden;
} }
.card::before { .logo {
content: ""; height: 36px;
position: absolute; width: auto;
inset: 0 0 auto 0; margin-bottom: 40px;
height: 6px;
background: var(--yellow);
}
.paw {
display: inline-flex;
align-items: center;
justify-content: center;
width: 64px;
height: 64px;
margin: 4px auto 18px;
border-radius: 50%;
background: rgba(33, 48, 33, 0.08);
color: var(--green);
}
.paw svg {
width: 32px;
height: 32px;
}
.eyebrow {
font-size: 12px;
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--green);
margin: 0 0 10px;
} }
h1 { h1 {
margin: 0 0 14px; margin: 0 0 16px;
font-size: clamp(28px, 4vw, 38px); font-family: "Unbounded", sans-serif;
font-size: clamp(1.6rem, 5vw, 2.1rem);
font-weight: 800;
line-height: 1.1; line-height: 1.1;
letter-spacing: -0.02em; letter-spacing: -0.03em;
color: var(--ink); color: var(--off-white);
} }
p { p {
margin: 0 0 12px; margin: 0 auto;
color: var(--muted); max-width: 26rem;
font-size: 16px; color: rgba(248, 247, 242, 0.82);
font-size: 1rem;
} }
.meta { .contact {
margin-top: 24px; margin-top: 32px;
padding-top: 22px; padding-top: 28px;
border-top: 1px solid rgba(17, 20, 24, 0.08); border-top: 1px solid var(--line);
font-size: 14px; font-size: 0.95rem;
color: var(--muted); color: rgba(248, 247, 242, 0.7);
} }
.meta a { .contact a {
color: var(--green); color: var(--yellow);
font-weight: 700; font-weight: 500;
text-decoration: none; text-decoration: none;
border-bottom: 1px solid rgba(255, 209, 0, 0.4);
padding-bottom: 1px;
} }
.meta a:hover { .contact a:hover {
text-decoration: underline; border-bottom-color: var(--yellow);
}
.footnote {
margin-top: 22px;
text-align: center;
font-size: 13px;
color: rgba(255, 255, 255, 0.7);
}
@media (max-width: 480px) {
.card {
padding: 36px 24px 28px;
border-radius: 22px;
}
.brand-bar img {
height: 36px;
}
} }
</style> </style>
</head> </head>
<body> <body>
<div class="stage"> <main>
<div class="brand-bar"> <img class="logo" src="/m/logo.png" alt="Goodwalk" width="241" height="48" />
<img src="/m/logo.png" alt="Goodwalk" width="241" height="48" /> <h1>Back on the lead shortly</h1>
</div> <p>
Were making a few quick updates. The site will be back to normal in a moment — thanks for your patience.
<main class="card" role="main"> </p>
<span class="paw" aria-hidden="true"> <p class="contact">
<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> Need us now? Email
<path d="M7.5 10.5c1.1 0 2-1.3 2-3s-.9-3-2-3-2 1.3-2 3 .9 3 2 3Zm9 0c1.1 0 2-1.3 2-3s-.9-3-2-3-2 1.3-2 3 .9 3 2 3ZM4 13.5c1 0 1.8-1.1 1.8-2.5S5 8.5 4 8.5s-1.8 1.1-1.8 2.5S3 13.5 4 13.5Zm16 0c1 0 1.8-1.1 1.8-2.5S21 8.5 20 8.5s-1.8 1.1-1.8 2.5.8 2.5 1.8 2.5ZM12 13.5c-3.3 0-6 2.5-6 5.4 0 1.4 1.1 2.6 2.5 2.6 1 0 1.5-.4 2.2-.8.4-.2.8-.4 1.3-.4s.9.2 1.3.4c.7.4 1.2.8 2.2.8 1.4 0 2.5-1.2 2.5-2.6 0-2.9-2.7-5.4-6-5.4Z"/> <a href="mailto:info@goodwalk.co.nz">info@goodwalk.co.nz</a>
</svg>
</span>
<p class="eyebrow">Goodwalk</p>
<h1>Be right back!</h1>
<p>We're updating the site — should only take a minute. Thanks for your patience.</p>
<div class="meta">
Need us now? Email <a href="mailto:info@goodwalk.co.nz">info@goodwalk.co.nz</a>
or call <a href="tel:+64226421011">(022) 642 1011</a>. or call <a href="tel:+64226421011">(022) 642 1011</a>.
</div> </p>
</main> </main>
<p class="footnote">Auckland Central dog walking · Tiny Gang pack walks · 1:1 walks · Puppy visits</p>
</div>
<script> <script>
// Auto-recheck once a minute so visitors don't sit on this page forever.
setTimeout(function () { setTimeout(function () {
window.location.reload(); window.location.reload();
}, 60000); }, 60000);
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "gw-svelte", "name": "gw-svelte",
"version": "4.0.1", "version": "4.0.5",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+11
View File
@@ -277,6 +277,17 @@ if [[ -f "$DEPLOY_PATH/.env" ]]; then
echo "[deploy-remote] Preserving existing $DEPLOY_PATH/.env" echo "[deploy-remote] Preserving existing $DEPLOY_PATH/.env"
fi fi
# The application source tree is fully owned by the repo and is only used as
# Docker build context — it holds no runtime data. The rsync below uses no
# --delete, so a file removed or renamed in the repo (e.g. a moved SvelteKit
# route) would otherwise linger here and break the build with a route conflict.
# Wipe src/ first so it is rebuilt exactly from the archive. Runtime data,
# .env, and Docker volumes live outside src/ and are untouched.
if [[ -d "$DEPLOY_PATH/src" ]]; then
echo "[deploy-remote] Refreshing $DEPLOY_PATH/src from archive (pruning stale files)"
rm -rf "$DEPLOY_PATH/src"
fi
echo "[deploy-remote] Copying application files into $DEPLOY_PATH" echo "[deploy-remote] Copying application files into $DEPLOY_PATH"
if command -v rsync >/dev/null 2>&1; then if command -v rsync >/dev/null 2>&1; then
rsync -a \ rsync -a \
+7 -2
View File
@@ -1,4 +1,5 @@
import type { Handle } from '@sveltejs/kit'; import type { Handle } from '@sveltejs/kit';
import { dev } from '$app/environment';
import { resolveSurface } from '$lib/server/surface'; import { resolveSurface } from '$lib/server/surface';
import { resolveAnonId, resolveHeroVariant } from '$lib/server/ab'; import { resolveAnonId, resolveHeroVariant } from '$lib/server/ab';
@@ -16,8 +17,12 @@ export const handle: Handle = async ({ event, resolve }) => {
event.locals.abHero = resolveHeroVariant(event.url, event.cookies); event.locals.abHero = resolveHeroVariant(event.url, event.cookies);
} }
// The admin host (cp.*) serves the dashboard at its root. // The admin host (cp.*) serves the dashboard at its root. In production the
if (surface === 'cp' && (path === '/' || path === '')) { // universal `reroute` hook maps / (and /clients, /birthdays, …) onto the
// internal /owner/welcome routes without changing the URL. In dev the cp
// surface is reached via ?preview=cp on localhost, where reroute can't detect
// the host, so fall back to redirecting the root to the dashboard path.
if (dev && surface === 'cp' && (path === '/' || path === '')) {
return new Response(null, { return new Response(null, {
status: 302, status: 302,
headers: { location: ADMIN_PATH }, headers: { location: ADMIN_PATH },
+31
View File
@@ -0,0 +1,31 @@
import type { Reroute } from '@sveltejs/kit';
/**
* Clean URLs for the control panel.
*
* The owner dashboard lives internally at /owner/welcome (home) and
* /owner/welcome/<tab>. On the cp.* / admin.* hosts we serve it at the root so
* the visible URLs are /, /clients, /birthdays, /messaging, /scheduled and
* /activity. This hook rewrites those clean paths to the real route without
* changing the URL in the browser, and runs on both the server and the client
* so SSR and client-side navigation stay in sync.
*
* Keep CP_HOSTS in sync with $lib/server/surface.ts it can't be imported here
* because this hook also runs in the browser and $lib/server is server-only.
*/
const CP_HOSTS = new Set(['cp.goodwalk.co.nz', 'admin.goodwalk.co.nz']);
const ADMIN_BASE = '/owner/welcome';
const CP_TABS = new Set(['clients', 'birthdays', 'messaging', 'scheduled', 'activity']);
export const reroute: Reroute = ({ url }) => {
if (!CP_HOSTS.has(url.hostname.toLowerCase())) return;
// Already addressing the internal admin routes (e.g. a direct hit) — leave it.
if (url.pathname === ADMIN_BASE || url.pathname.startsWith(`${ADMIN_BASE}/`)) return;
const segment = url.pathname.replace(/^\/+/, '').split('/')[0];
if (segment === '') return ADMIN_BASE; // / → dashboard home
if (CP_TABS.has(segment)) return `${ADMIN_BASE}/${segment}`;
// Anything else (static assets, /owner/experiments, …) routes normally.
};
File diff suppressed because it is too large Load Diff
@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { accordion } from '$lib/actions/accordion'; import { accordion } from '$lib/actions/accordion';
import { reveal } from '$lib/actions/reveal'; import { reveal } from '$lib/actions/reveal';
import CtaCard from '$lib/components/CtaCard.svelte'; import CtaCard from '$lib/components/ui/CtaCard.svelte';
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import ServiceHero from '$lib/components/ServiceHero.svelte'; import ServiceHero from '$lib/components/sections/ServiceHero.svelte';
import { getEnhancedImage } from '$lib/enhanced-images'; import { getEnhancedImage } from '$lib/enhanced-images';
import type { AboutPageContent } from '$lib/types'; import type { AboutPageContent } from '$lib/types';
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import BookingWizard from '$lib/components/BookingWizard.svelte'; import BookingWizard from '$lib/components/sections/BookingWizard.svelte';
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import InfoSection from '$lib/components/InfoSection.svelte'; import InfoSection from '$lib/components/sections/InfoSection.svelte';
import ServiceHero from '$lib/components/ServiceHero.svelte'; import ServiceHero from '$lib/components/sections/ServiceHero.svelte';
import type { BookingContent, InfoContent } from '$lib/types'; import type { BookingContent, InfoContent } from '$lib/types';
export let booking: BookingContent; export let booking: BookingContent;
@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import OnboardingSignaturePad from '$lib/components/OnboardingSignaturePad.svelte'; import OnboardingSignaturePad from '$lib/components/ui/OnboardingSignaturePad.svelte';
import OnboardingAuth from '$lib/components/OnboardingAuth.svelte'; import OnboardingAuth from '$lib/components/sections/OnboardingAuth.svelte';
import OnboardingFooter from '$lib/components/OnboardingFooter.svelte'; import OnboardingFooter from '$lib/components/ui/OnboardingFooter.svelte';
import logoDesktop from '$lib/images/goodwalk-auckland-dog-walking-logo.png?enhanced'; import logoDesktop from '$lib/images/goodwalk-auckland-dog-walking-logo.png?enhanced';
import type { Picture } from '@sveltejs/enhanced-img'; import type { Picture } from '@sveltejs/enhanced-img';
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { sharedServices } from '$lib/content/services'; import { sharedServices } from '$lib/content/services';
import { reveal } from '$lib/actions/reveal'; import { reveal } from '$lib/actions/reveal';
import CtaCard from '$lib/components/CtaCard.svelte'; import CtaCard from '$lib/components/ui/CtaCard.svelte';
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import { getEnhancedImage } from '$lib/enhanced-images'; import { getEnhancedImage } from '$lib/enhanced-images';
import { getSeededTestimonialIndex } from '$lib/testimonials'; import { getSeededTestimonialIndex } from '$lib/testimonials';
import type { LocationPageContent, TestimonialContent } from '$lib/types'; import type { LocationPageContent, TestimonialContent } from '$lib/types';
@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import OnboardingSignaturePad from '$lib/components/OnboardingSignaturePad.svelte'; import OnboardingSignaturePad from '$lib/components/ui/OnboardingSignaturePad.svelte';
import OnboardingAuth from '$lib/components/OnboardingAuth.svelte'; import OnboardingAuth from '$lib/components/sections/OnboardingAuth.svelte';
import OnboardingFooter from '$lib/components/OnboardingFooter.svelte'; import OnboardingFooter from '$lib/components/ui/OnboardingFooter.svelte';
import logoDesktop from '$lib/images/goodwalk-auckland-dog-walking-logo.png?enhanced'; import logoDesktop from '$lib/images/goodwalk-auckland-dog-walking-logo.png?enhanced';
import type { Picture } from '@sveltejs/enhanced-img'; import type { Picture } from '@sveltejs/enhanced-img';
@@ -11,6 +11,25 @@
export const preview = false; export const preview = false;
// Owner "view as client" impersonation. When `impersonation` is true the
// page renders the given client's onboarding view from injected data (no
// /auth/verify call) and the owner can walk through every step interactively.
// It still never touches the real client: client-side persistence is
// neutralised (saveDraft / the localStorage reactive block are no-ops) and
// the final submit is routed to `onImpersonationSubmit` instead of the real
// endpoint. Defaults keep the normal client flow intact.
export let impersonation = false;
export let impersonationData: {
email?: string;
profile?: Record<string, unknown>;
draft?: Record<string, unknown>;
} | null = null;
// Called on final submit while impersonating. The parent decides whether to
// email the run-through to a preview address or do nothing (dry run); either
// way the real client is never contacted or updated. Throwing surfaces an
// error in the form just like a real failed submit.
export let onImpersonationSubmit: ((payload: Record<string, unknown>) => Promise<void>) | null = null;
const ownerEmail = 'info@goodwalk.co.nz'; const ownerEmail = 'info@goodwalk.co.nz';
const ownerPhone = '(022) 642 1011'; const ownerPhone = '(022) 642 1011';
const services = ['Tiny Gang Pack Walks', 'Solo Walks', 'Puppy Visits', 'Unsure yet']; const services = ['Tiny Gang Pack Walks', 'Solo Walks', 'Puppy Visits', 'Unsure yet'];
@@ -56,6 +75,9 @@
let vetPhone = ''; let vetPhone = '';
let emergencyContactName = ''; let emergencyContactName = '';
let emergencyContactPhone = ''; let emergencyContactPhone = '';
let regularFleaTickTreatment = '';
let petInsurance = '';
let petInsuranceOwnerExpenseAccepted = false;
// Health & diet // Health & diet
let isVaccinated = ''; // 'yes' | 'no' let isVaccinated = ''; // 'yes' | 'no'
@@ -75,6 +97,7 @@
let visitsDogParks = ''; let visitsDogParks = '';
let dogParksFrequency = ''; let dogParksFrequency = '';
let biteHistory = ''; let biteHistory = '';
let resourceGuards = '';
let reactiveToDogs = ''; let reactiveToDogs = '';
let reactiveToAnimals = ''; let reactiveToAnimals = '';
let reactiveToChildren = ''; let reactiveToChildren = '';
@@ -211,6 +234,7 @@
servicesNeeded, temperament, accessInstructions, servicesNeeded, temperament, accessInstructions,
vetName, vetAddress, vetPhone, vetName, vetAddress, vetPhone,
emergencyContactName, emergencyContactPhone, emergencyContactName, emergencyContactPhone,
regularFleaTickTreatment, petInsurance, petInsuranceOwnerExpenseAccepted,
isVaccinated, isVaccinated,
hasFoodAllergies, foodAllergiesDetail, hasFoodAllergies, foodAllergiesDetail,
hasEnvAllergies, envAllergiesDetail, hasEnvAllergies, envAllergiesDetail,
@@ -218,7 +242,7 @@
onMedication, medicationDetail, onMedication, medicationDetail,
wellSocialised, dogsInteractedWeekly, wellSocialised, dogsInteractedWeekly,
visitsBeach, visitsDogParks, dogParksFrequency, visitsBeach, visitsDogParks, dogParksFrequency,
biteHistory, biteHistory, resourceGuards,
reactiveToDogs, reactiveToAnimals, reactiveToChildren, reactiveToPeople, reactiveToDogs, reactiveToAnimals, reactiveToChildren, reactiveToPeople,
isDesexed, isRegistered, leashTrained, isDesexed, isRegistered, leashTrained,
recallRating, ranAwayBefore, recallRating, ranAwayBefore,
@@ -229,6 +253,7 @@
} }
async function saveDraft() { async function saveDraft() {
if (impersonation) return; // read-only replica never persists drafts
try { try {
const token = window.localStorage.getItem('gw_onboarding_session'); const token = window.localStorage.getItem('gw_onboarding_session');
if (!token) return; if (!token) return;
@@ -280,6 +305,9 @@
vetPhone = pickString(draft, 'vetPhone', vetPhone); vetPhone = pickString(draft, 'vetPhone', vetPhone);
emergencyContactName = pickString(draft, 'emergencyContactName', emergencyContactName); emergencyContactName = pickString(draft, 'emergencyContactName', emergencyContactName);
emergencyContactPhone = pickString(draft, 'emergencyContactPhone', emergencyContactPhone); emergencyContactPhone = pickString(draft, 'emergencyContactPhone', emergencyContactPhone);
regularFleaTickTreatment = pickString(draft, 'regularFleaTickTreatment', regularFleaTickTreatment);
petInsurance = pickString(draft, 'petInsurance', petInsurance);
petInsuranceOwnerExpenseAccepted = pickBool(draft, 'petInsuranceOwnerExpenseAccepted', petInsuranceOwnerExpenseAccepted);
isVaccinated = pickString(draft, 'isVaccinated', isVaccinated); isVaccinated = pickString(draft, 'isVaccinated', isVaccinated);
hasFoodAllergies = pickString(draft, 'hasFoodAllergies', hasFoodAllergies); hasFoodAllergies = pickString(draft, 'hasFoodAllergies', hasFoodAllergies);
foodAllergiesDetail = pickString(draft, 'foodAllergiesDetail', foodAllergiesDetail); foodAllergiesDetail = pickString(draft, 'foodAllergiesDetail', foodAllergiesDetail);
@@ -295,6 +323,7 @@
visitsDogParks = pickString(draft, 'visitsDogParks', visitsDogParks); visitsDogParks = pickString(draft, 'visitsDogParks', visitsDogParks);
dogParksFrequency = pickString(draft, 'dogParksFrequency', dogParksFrequency); dogParksFrequency = pickString(draft, 'dogParksFrequency', dogParksFrequency);
biteHistory = pickString(draft, 'biteHistory', biteHistory); biteHistory = pickString(draft, 'biteHistory', biteHistory);
resourceGuards = pickString(draft, 'resourceGuards', resourceGuards);
reactiveToDogs = pickString(draft, 'reactiveToDogs', reactiveToDogs); reactiveToDogs = pickString(draft, 'reactiveToDogs', reactiveToDogs);
reactiveToAnimals = pickString(draft, 'reactiveToAnimals', reactiveToAnimals); reactiveToAnimals = pickString(draft, 'reactiveToAnimals', reactiveToAnimals);
reactiveToChildren = pickString(draft, 'reactiveToChildren', reactiveToChildren); reactiveToChildren = pickString(draft, 'reactiveToChildren', reactiveToChildren);
@@ -313,7 +342,7 @@
termsAccepted = pickBool(draft, 'termsAccepted', termsAccepted); termsAccepted = pickBool(draft, 'termsAccepted', termsAccepted);
} }
$: if (typeof window !== 'undefined') { $: if (typeof window !== 'undefined' && !impersonation) {
try { try {
window.localStorage.setItem(draftStorageKey, JSON.stringify(getDraftSnapshot())); window.localStorage.setItem(draftStorageKey, JSON.stringify(getDraftSnapshot()));
} catch { /* storage unavailable */ } } catch { /* storage unavailable */ }
@@ -379,7 +408,30 @@
userEmail = ''; userEmail = '';
} }
function initImpersonation() {
// Render straight from the owner-supplied snapshot. No tokens are read,
// no network calls are made, and no timers are started.
const data = impersonationData ?? {};
const serverEmail = typeof data.email === 'string' ? data.email : '';
isAuthenticated = true;
userEmail = serverEmail;
applyProfile(serverEmail, (data.profile ?? {}) as Record<string, unknown>);
const onboardingDraft =
data.draft && typeof data.draft === 'object'
? (data.draft as Record<string, unknown>).onboarding
: null;
if (!onboardingCompleted && onboardingDraft && typeof onboardingDraft === 'object') {
applyDraft(onboardingDraft as Record<string, unknown>);
}
authChecking = false;
}
onMount(async () => { onMount(async () => {
if (impersonation) {
initImpersonation();
return;
}
await checkAuth(); await checkAuth();
if (!isAuthenticated) return; if (!isAuthenticated) return;
@@ -433,96 +485,12 @@
} }
} }
function validateEmail(raw: string): string { // The form is intentionally free-fill: no fields are required and it can be
const value = raw.trim(); // submitted incomplete. Owners often onboard before they've named a dog or
if (!value) return 'Please enter your email address'; // sorted council registration, and we follow up on the gaps ourselves.
if (!/^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$/.test(value)) {
return 'Please enter a valid email address';
}
return '';
}
type RequirementCheck = { key: string; label: string; met: boolean; step: number };
$: contactDetailsMet =
Boolean(ownerFirstName.trim()) &&
Boolean(ownerLastName.trim()) &&
!validateEmail(email) &&
Boolean(phone.trim()) &&
Boolean(address.trim());
$: requirementChecks = [
{ key: 'contact', label: 'Your contact details (name, email, phone, address)', met: contactDetailsMet, step: 1 },
{ key: 'services', label: 'At least one service selected', met: servicesNeeded.length > 0, step: 2 },
{ key: 'vaccination', label: 'Vaccinations confirmed', met: isVaccinated === 'yes', step: 3 },
{ key: 'council', label: 'Council registration confirmed', met: isRegistered === 'yes', step: 4 },
{ key: 'vetConsent', label: 'Emergency vet treatment consent', met: emergencyVetConsent, step: 5 },
{ key: 'declaration', label: 'Onboarding declaration ticked', met: termsAccepted, step: 5 },
{ key: 'signature', label: 'Valid signature added', met: Boolean(signatureDataUrl), step: 5 },
] as RequirementCheck[];
$: requirementsAllMet = requirementChecks.every((r) => r.met);
function goToStep(step: number) {
if (step < 1 || step > steps.length) return;
errors = {};
currentStep = step;
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function validateStep(step: number): boolean {
const next: Record<string, string> = {};
if (step === 1) {
if (!ownerFirstName.trim()) next.ownerFirstName = 'Please enter your first name';
if (!ownerLastName.trim()) next.ownerLastName = 'Please enter your last name';
const emailError = validateEmail(email);
if (emailError) next.email = emailError;
if (!phone.trim()) next.phone = 'Please enter your phone number';
if (!address.trim()) next.address = 'Please enter your address';
} else if (step === 2) {
if (!dogName.trim()) next.dogName = "Please enter your dog's name";
if (!dogBreed.trim()) next.dogBreed = "Please enter your dog's breed";
if (!dogDateOfBirth.trim()) next.dogDateOfBirth = "Please enter your dog's date of birth";
if (!servicesNeeded.length) next.servicesNeeded = 'Choose at least one service';
} else if (step === 3) {
if (!vetName.trim()) next.vetName = 'Please enter your vet clinic name';
if (!vetAddress.trim()) next.vetAddress = 'Please enter the vet address';
if (!vetPhone.trim()) next.vetPhone = 'Please enter your vet phone number';
if (!emergencyContactName.trim()) next.emergencyContactName = 'Please add an emergency contact';
if (!emergencyContactPhone.trim()) next.emergencyContactPhone = 'Please add an emergency contact number';
if (!isVaccinated) next.isVaccinated = 'Please answer the vaccination question';
if (!hasFoodAllergies) next.hasFoodAllergies = 'Please answer the food allergy question';
if (hasFoodAllergies === 'yes' && !foodAllergiesDetail.trim()) {
next.foodAllergiesDetail = 'Please describe the food allergy';
}
if (!hasEnvAllergies) next.hasEnvAllergies = 'Please answer the environmental allergy question';
if (hasEnvAllergies === 'yes' && !envAllergiesDetail.trim()) {
next.envAllergiesDetail = 'Please describe the environmental allergy';
}
if (!onSpecialDiet) next.onSpecialDiet = 'Please answer the special diet question';
if (onSpecialDiet === 'yes' && !specialDietDetail.trim()) {
next.specialDietDetail = 'Please describe the special diet';
}
if (!onMedication) next.onMedication = 'Please answer the medication question';
if (onMedication === 'yes' && !medicationDetail.trim()) {
next.medicationDetail = 'Please describe the medication';
}
} else if (step === 4) {
if (!isRegistered) next.isRegistered = 'Please confirm whether your dog is registered with council';
} else if (step === 5) {
if (!emergencyVetConsent) next.emergencyVetConsent = 'Please confirm emergency treatment consent';
if (!termsAccepted) next.termsAccepted = 'Please confirm the onboarding declaration';
if (!signatureDataUrl) next.signature = 'Please add your signature before sending';
}
errors = next;
return Object.keys(next).length === 0;
}
function nextStep() { function nextStep() {
noteInteraction(); noteInteraction();
if (!validateStep(currentStep)) return;
currentStep = Math.min(currentStep + 1, steps.length); currentStep = Math.min(currentStep + 1, steps.length);
void saveDraft(); void saveDraft();
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' });
@@ -547,6 +515,7 @@ function buildSubmissionSnapshot(submittedAt: string): SubmissionSummary {
{ label: 'Email', value: fieldValue(email) }, { label: 'Email', value: fieldValue(email) },
{ label: 'Contact Number', value: fieldValue(phone) }, { label: 'Contact Number', value: fieldValue(phone) },
{ label: 'Home Address', value: fieldValue(address) }, { label: 'Home Address', value: fieldValue(address) },
{ label: 'Home Access Instructions', value: fieldValue(accessInstructions) },
], ],
}, },
{ {
@@ -570,7 +539,9 @@ function buildSubmissionSnapshot(submittedAt: string): SubmissionSummary {
{ label: 'Vet Address', value: fieldValue(vetAddress) }, { label: 'Vet Address', value: fieldValue(vetAddress) },
{ label: 'Emergency Contact Name', value: fieldValue(emergencyContactName) }, { label: 'Emergency Contact Name', value: fieldValue(emergencyContactName) },
{ label: 'Emergency Contact Number', value: fieldValue(emergencyContactPhone) }, { label: 'Emergency Contact Number', value: fieldValue(emergencyContactPhone) },
{ label: 'Home Access Instructions', value: fieldValue(accessInstructions) }, { label: 'Regular Flea and Tick Treatment?', value: yesNoText(regularFleaTickTreatment) },
{ label: 'Pet Insurance?', value: yesNoText(petInsurance) },
{ label: 'Owner Covers All Vet Costs if Uninsured', value: petInsurance === 'no' ? yesNoText(petInsuranceOwnerExpenseAccepted) : '—' },
{ label: 'Are Vaccinations Current?', value: yesNoText(isVaccinated) }, { label: 'Are Vaccinations Current?', value: yesNoText(isVaccinated) },
{ label: 'Any Food Allergies?', value: hasFoodAllergies === 'yes' ? fieldValue(foodAllergiesDetail) : yesNoText(hasFoodAllergies) }, { label: 'Any Food Allergies?', value: hasFoodAllergies === 'yes' ? fieldValue(foodAllergiesDetail) : yesNoText(hasFoodAllergies) },
{ label: 'Any Environmental Allergies?', value: hasEnvAllergies === 'yes' ? fieldValue(envAllergiesDetail) : yesNoText(hasEnvAllergies) }, { label: 'Any Environmental Allergies?', value: hasEnvAllergies === 'yes' ? fieldValue(envAllergiesDetail) : yesNoText(hasEnvAllergies) },
@@ -587,6 +558,7 @@ function buildSubmissionSnapshot(submittedAt: string): SubmissionSummary {
{ label: 'Visits the Beach?', value: yesNoText(visitsBeach) }, { label: 'Visits the Beach?', value: yesNoText(visitsBeach) },
{ label: 'Visits Dog Parks Frequently?', value: visitsDogParks === 'yes' ? fieldValue(dogParksFrequency ? `${dogParksFrequency} per week` : 'Yes') : yesNoText(visitsDogParks) }, { label: 'Visits Dog Parks Frequently?', value: visitsDogParks === 'yes' ? fieldValue(dogParksFrequency ? `${dogParksFrequency} per week` : 'Yes') : yesNoText(visitsDogParks) },
{ label: 'Any Bite History?', value: yesNoText(biteHistory) }, { label: 'Any Bite History?', value: yesNoText(biteHistory) },
{ label: 'Resource Guards?', value: yesNoText(resourceGuards) },
{ label: 'Reactive to Other Dogs?', value: yesNoText(reactiveToDogs) }, { label: 'Reactive to Other Dogs?', value: yesNoText(reactiveToDogs) },
{ label: 'Reactive to Other Animals?', value: yesNoText(reactiveToAnimals) }, { label: 'Reactive to Other Animals?', value: yesNoText(reactiveToAnimals) },
{ label: 'Reactive to Children?', value: yesNoText(reactiveToChildren) }, { label: 'Reactive to Children?', value: yesNoText(reactiveToChildren) },
@@ -621,15 +593,6 @@ function buildSubmissionSnapshot(submittedAt: string): SubmissionSummary {
sendClickedAt = Date.now(); sendClickedAt = Date.now();
submitError = ''; submitError = '';
if (!validateStep(5)) return;
const firstUnmet = requirementChecks.find((r) => !r.met);
if (firstUnmet) {
submitError = `Please complete: ${firstUnmet.label}.`;
if (firstUnmet.step !== currentStep) goToStep(firstUnmet.step);
return;
}
submitting = true; submitting = true;
const controller = new AbortController(); const controller = new AbortController();
@@ -648,15 +611,14 @@ function buildSubmissionSnapshot(submittedAt: string): SubmissionSummary {
toStr(additionalNotes).trim(), toStr(additionalNotes).trim(),
].filter(Boolean).join('\n'); ].filter(Boolean).join('\n');
const response = await fetch('/api/onboarding-submit', { const payload = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
signal: controller.signal,
body: JSON.stringify({
...getDraftSnapshot(), ...getDraftSnapshot(),
fullName, fullName,
dogAge: dogDateOfBirth, dogAge: dogDateOfBirth,
medicalNotes, medicalNotes,
regularFleaTickTreatment,
petInsurance,
petInsuranceOwnerExpenseAccepted,
councilRegistrationConfirmed: isRegistered === 'yes', councilRegistrationConfirmed: isRegistered === 'yes',
vaccinationsConfirmed: isVaccinated === 'yes', vaccinationsConfirmed: isVaccinated === 'yes',
signatureDataUrl, signatureDataUrl,
@@ -666,12 +628,29 @@ function buildSubmissionSnapshot(submittedAt: string): SubmissionSummary {
formStartedAt, visitStartedAt, pageEnteredAt, firstInteractionAt, sendClickedAt, formStartedAt, visitStartedAt, pageEnteredAt, firstInteractionAt, sendClickedAt,
referrer: document.referrer, referrer: document.referrer,
page: window.location.href, page: window.location.href,
}) };
if (impersonation) {
// Owner run-through: hand off to the parent (preview email or dry run).
// Never hits the real client endpoint or the owner's own saved draft.
if (onImpersonationSubmit) await onImpersonationSubmit(payload);
onboardingCompleted = true;
onboardingSummary = submissionSnapshot;
onboardingSubmittedAt = signedOnDate;
window.scrollTo({ top: 0, behavior: 'smooth' });
return;
}
const response = await fetch('/api/onboarding-submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
signal: controller.signal,
body: JSON.stringify(payload)
}); });
if (!response.ok) { if (!response.ok) {
const payload = await response.json().catch(() => null); const errorPayload = await response.json().catch(() => null);
throw new Error(payload?.detail?.message ?? payload?.detail ?? payload?.error ?? `Submission failed (${response.status})`); throw new Error(errorPayload?.detail?.message ?? errorPayload?.detail ?? errorPayload?.error ?? `Submission failed (${response.status})`);
} }
onboardingCompleted = true; onboardingCompleted = true;
@@ -970,6 +949,16 @@ function buildSubmissionSnapshot(submittedAt: string): SubmissionSummary {
<input bind:value={address} on:input={noteInteraction} autocomplete="street-address" placeholder="Street, suburb, city" /> <input bind:value={address} on:input={noteInteraction} autocomplete="street-address" placeholder="Street, suburb, city" />
{#if errors.address}<small>{errors.address}</small>{/if} {#if errors.address}<small>{errors.address}</small>{/if}
</label> </label>
<label class="field field-full">
<span>Home access instructions</span>
<textarea
bind:value={accessInstructions}
rows="3"
placeholder="Keys, gate code, where to collect your dog, alarm notes."
on:input={noteInteraction}
></textarea>
</label>
</div> </div>
<div class="panel-nav"> <div class="panel-nav">
@@ -1125,19 +1114,33 @@ function buildSubmissionSnapshot(submittedAt: string): SubmissionSummary {
<div class="panel-subsection-icon"><Icon name="fas fa-notes-medical" /></div> <div class="panel-subsection-icon"><Icon name="fas fa-notes-medical" /></div>
<div> <div>
<h3>Health notes</h3> <h3>Health notes</h3>
<p>Day-to-day care, access, and anything we should know. Vaccination status is required below.</p> <p>Day-to-day care and anything we should know. Vaccination status is required below.</p>
</div> </div>
</div> </div>
<label class="field"> {@render yesNo('Does your dog get regular flea and tick treatment?', 'regularFleaTickTreatment', regularFleaTickTreatment, (v) => (regularFleaTickTreatment = v))}
<span>Home access instructions</span>
<textarea {@render yesNo('Does your dog have pet insurance?', 'petInsurance', petInsurance, (v) => {
bind:value={accessInstructions} petInsurance = v;
rows="3" if (v !== 'no') {
placeholder="Keys, gate code, where to collect your dog, alarm notes." petInsuranceOwnerExpenseAccepted = false;
on:input={noteInteraction} if (errors.petInsuranceOwnerExpenseAccepted) {
></textarea> errors = { ...errors, petInsuranceOwnerExpenseAccepted: '' };
}
}
})}
{#if petInsurance === 'no'}
<label class="confirm-row">
<input bind:checked={petInsuranceOwnerExpenseAccepted} type="checkbox" on:change={() => {
noteInteraction();
if (errors.petInsuranceOwnerExpenseAccepted) {
errors = { ...errors, petInsuranceOwnerExpenseAccepted: '' };
}
}} />
<span>If my dog does not have pet insurance, I agree that Goodwalk takes no responsibility and all vet costs are incurred at the owner's expense.</span>
</label> </label>
{#if errors.petInsuranceOwnerExpenseAccepted}<small>{errors.petInsuranceOwnerExpenseAccepted}</small>{/if}
{/if}
{@render yesNo("Are your dog's vaccinations up to date?", 'isVaccinated', isVaccinated, (v) => (isVaccinated = v))} {@render yesNo("Are your dog's vaccinations up to date?", 'isVaccinated', isVaccinated, (v) => (isVaccinated = v))}
@@ -1218,6 +1221,7 @@ function buildSubmissionSnapshot(submittedAt: string): SubmissionSummary {
{/if} {/if}
{@render yesNo('Any bite history?', 'biteHistory', biteHistory, (v) => (biteHistory = v))} {@render yesNo('Any bite history?', 'biteHistory', biteHistory, (v) => (biteHistory = v))}
{@render yesNo('Does your dog resource guard?', 'resourceGuards', resourceGuards, (v) => (resourceGuards = v))}
{@render yesNo('Reactive to other dogs?', 'reactiveToDogs', reactiveToDogs, (v) => (reactiveToDogs = v))} {@render yesNo('Reactive to other dogs?', 'reactiveToDogs', reactiveToDogs, (v) => (reactiveToDogs = v))}
{@render yesNo('Reactive to other animals?', 'reactiveToAnimals', reactiveToAnimals, (v) => (reactiveToAnimals = v))} {@render yesNo('Reactive to other animals?', 'reactiveToAnimals', reactiveToAnimals, (v) => (reactiveToAnimals = v))}
{@render yesNo('Reactive to children?', 'reactiveToChildren', reactiveToChildren, (v) => (reactiveToChildren = v))} {@render yesNo('Reactive to children?', 'reactiveToChildren', reactiveToChildren, (v) => (reactiveToChildren = v))}
@@ -1300,41 +1304,6 @@ function buildSubmissionSnapshot(submittedAt: string): SubmissionSummary {
</div> </div>
</div> </div>
<div class="requirements-panel" class:requirements-complete={requirementsAllMet} aria-label="Onboarding requirements">
<div class="requirements-head">
<h3>Before you send</h3>
<p>
{#if requirementsAllMet}
All set. You're ready to sign and send.
{:else}
We need each of these before you submit. Tap any item to jump back to that step.
{/if}
</p>
</div>
<ul class="requirements-list">
{#each requirementChecks as req (req.key)}
<li class="requirement-row" class:requirement-met={req.met}>
<button
type="button"
class="requirement-button"
on:click={() => goToStep(req.step)}
aria-label="{req.met ? 'Met' : 'Not met'}: {req.label}. Go to step {req.step}."
>
<span class="requirement-status" aria-hidden="true">
{#if req.met}
<Icon name="fas fa-circle-check" />
{:else}
<Icon name="fas fa-circle" />
{/if}
</span>
<span class="requirement-label">{req.label}</span>
<span class="requirement-step">Step {req.step}</span>
</button>
</li>
{/each}
</ul>
</div>
<label class="field"> <label class="field">
<span>Anything else we should know?</span> <span>Anything else we should know?</span>
<textarea <textarea
@@ -1347,7 +1316,7 @@ function buildSubmissionSnapshot(submittedAt: string): SubmissionSummary {
<div class="onboarding-fields two-up"> <div class="onboarding-fields two-up">
<label class="field"> <label class="field">
<span>Instagram or social handle <span class="field-hint">Optional</span></span> <span>Instagram or social handle</span>
<input <input
bind:value={socialMediaAccount} bind:value={socialMediaAccount}
on:input={() => { on:input={() => {
@@ -1415,7 +1384,7 @@ function buildSubmissionSnapshot(submittedAt: string): SubmissionSummary {
<Icon name="fas fa-arrow-left" /> Back <Icon name="fas fa-arrow-left" /> Back
</button> </button>
<div class="panel-nav-submit-group"> <div class="panel-nav-submit-group">
<button class="btn btn-yellow onboarding-submit" type="submit" disabled={submitting || !requirementsAllMet}> <button class="btn btn-yellow onboarding-submit" type="submit" disabled={submitting}>
{#if submitting}Sending…{:else}Send onboarding form{/if} {#if submitting}Sending…{:else}Send onboarding form{/if}
</button> </button>
<p>This goes directly to Aless for review.</p> <p>This goes directly to Aless for review.</p>
@@ -2356,157 +2325,6 @@ function buildSubmissionSnapshot(submittedAt: string): SubmissionSummary {
transform: translateY(-1px); transform: translateY(-1px);
} }
/* ── Requirements summary (step 5) ── */
.requirements-panel {
margin: 4px 0 28px;
padding: 22px 22px 18px;
border-radius: 24px;
background: linear-gradient(180deg, #fffefb 0%, #faf8f1 100%);
border: 1px solid rgba(33, 48, 33, 0.12);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.5),
0 8px 20px rgba(17, 20, 24, 0.04);
transition: border-color 0.25s ease, box-shadow 0.25s ease, background 0.25s ease;
}
.requirements-panel.requirements-complete {
border-color: rgba(33, 48, 33, 0.22);
background: linear-gradient(180deg, #fffbe8 0%, #fff5c9 100%);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.6),
0 10px 26px rgba(255, 209, 0, 0.18);
}
.requirements-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-bottom: 14px;
}
.requirements-head h3 {
margin: 0;
font-family: var(--font-head);
font-size: 17px;
font-weight: 700;
letter-spacing: -0.02em;
color: var(--gw-green);
}
.requirements-head p {
margin: 0;
max-width: 56ch;
font-size: 12px;
font-weight: 700;
line-height: 1.5;
letter-spacing: 0.005em;
color: rgba(33, 48, 33, 0.62);
}
.requirements-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 8px;
}
.requirement-button {
width: 100%;
display: grid;
grid-template-columns: 24px 1fr auto;
gap: 14px;
align-items: center;
padding: 12px 14px;
border-radius: 14px;
border: 1px solid rgba(33, 48, 33, 0.1);
background: rgba(255, 255, 255, 0.85);
color: var(--gw-green);
font-family: var(--font-body);
font-size: 14px;
font-weight: 700;
text-align: left;
cursor: pointer;
transition:
background 0.18s ease,
border-color 0.18s ease,
transform 0.18s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.18s ease;
}
.requirement-button:hover {
border-color: rgba(33, 48, 33, 0.3);
background: #fff;
transform: translateY(-1px);
box-shadow: 0 6px 14px rgba(17, 20, 24, 0.05);
}
.requirement-button:focus-visible {
outline: none;
border-color: var(--gw-green);
box-shadow: 0 0 0 4px rgba(255, 209, 0, 0.32);
}
.requirement-status {
display: inline-flex;
width: 24px;
height: 24px;
align-items: center;
justify-content: center;
color: rgba(33, 48, 33, 0.3);
font-size: 20px;
}
.requirement-met .requirement-status {
color: #2f7a3a;
}
.requirement-met .requirement-button {
background: rgba(47, 122, 58, 0.07);
border-color: rgba(47, 122, 58, 0.28);
}
.requirement-label {
line-height: 1.4;
color: #171b20;
}
.requirement-met .requirement-label {
color: var(--gw-green);
}
.requirement-step {
font-family: var(--font-head);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(33, 48, 33, 0.5);
}
.requirement-met .requirement-step {
color: rgba(47, 122, 58, 0.7);
}
@media (max-width: 540px) {
.requirements-head {
flex-direction: column;
align-items: flex-start;
gap: 6px;
}
.requirement-button {
grid-template-columns: 22px 1fr;
grid-template-rows: auto auto;
row-gap: 4px;
}
.requirement-step {
grid-column: 2;
}
}
/* ── Declaration ── */ /* ── Declaration ── */
.onboarding-confirm-list { .onboarding-confirm-list {
display: grid; display: grid;
@@ -2730,7 +2548,6 @@ function buildSubmissionSnapshot(submittedAt: string): SubmissionSummary {
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.field input, .field input,
.field textarea, .field textarea,
.requirement-button,
.panel-nav-back, .panel-nav-back,
.panel-nav-next, .panel-nav-next,
.onboarding-submit, .onboarding-submit,
@@ -2744,8 +2561,7 @@ function buildSubmissionSnapshot(submittedAt: string): SubmissionSummary {
.panel-nav-back:hover, .panel-nav-back:hover,
.panel-nav-next:hover, .panel-nav-next:hover,
.onboarding-submit:not(:disabled):hover, .onboarding-submit:not(:disabled):hover {
.requirement-button:hover {
transform: none; transform: none;
} }
} }
@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { reveal } from '$lib/actions/reveal'; import { reveal } from '$lib/actions/reveal';
import BookingWizard from '$lib/components/BookingWizard.svelte'; import BookingWizard from '$lib/components/sections/BookingWizard.svelte';
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import PricingPlanCard from '$lib/components/PricingPlanCard.svelte'; import PricingPlanCard from '$lib/components/ui/PricingPlanCard.svelte';
import ServiceHero from '$lib/components/ServiceHero.svelte'; import ServiceHero from '$lib/components/sections/ServiceHero.svelte';
import TestimonialsSection from '$lib/components/TestimonialsSection.svelte'; import TestimonialsSection from '$lib/components/sections/TestimonialsSection.svelte';
import { decoratePlans } from '$lib/utils/pricing'; import { decoratePlans } from '$lib/utils/pricing';
import type { PricingPageContent, SiteSharedContent } from '$lib/types'; import type { PricingPageContent, SiteSharedContent } from '$lib/types';
@@ -1,13 +1,13 @@
<script lang="ts"> <script lang="ts">
import { onMount, tick } from 'svelte'; import { onMount, tick } from 'svelte';
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import { reveal } from '$lib/actions/reveal'; import { reveal } from '$lib/actions/reveal';
import BookingWizard from '$lib/components/BookingWizard.svelte'; import BookingWizard from '$lib/components/sections/BookingWizard.svelte';
import PricingPlanCard from '$lib/components/PricingPlanCard.svelte'; import PricingPlanCard from '$lib/components/ui/PricingPlanCard.svelte';
import ServiceHero from '$lib/components/ServiceHero.svelte'; import ServiceHero from '$lib/components/sections/ServiceHero.svelte';
import TestimonialsSection from '$lib/components/TestimonialsSection.svelte'; import TestimonialsSection from '$lib/components/sections/TestimonialsSection.svelte';
import FaqSection from '$lib/components/FaqSection.svelte'; import FaqSection from '$lib/components/sections/FaqSection.svelte';
import ServiceAreaMap from '$lib/components/ServiceAreaMap.svelte'; import ServiceAreaMap from '$lib/components/sections/ServiceAreaMap.svelte';
import { getEnhancedImage } from '$lib/enhanced-images'; import { getEnhancedImage } from '$lib/enhanced-images';
import { decoratePlans } from '$lib/utils/pricing'; import { decoratePlans } from '$lib/utils/pricing';
import { locationPages } from '$lib/content/locations'; import { locationPages } from '$lib/content/locations';
@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import BookingWizard from '$lib/components/BookingWizard.svelte'; import BookingWizard from '$lib/components/sections/BookingWizard.svelte';
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import { getEnhancedImage } from '$lib/enhanced-images'; import { getEnhancedImage } from '$lib/enhanced-images';
import { reveal } from '$lib/actions/reveal'; import { reveal } from '$lib/actions/reveal';
import ServiceHero from '$lib/components/ServiceHero.svelte'; import ServiceHero from '$lib/components/sections/ServiceHero.svelte';
import type { SiteSharedContent, TestimonialContent } from '$lib/types'; import type { SiteSharedContent, TestimonialContent } from '$lib/types';
export let content: SiteSharedContent; export let content: SiteSharedContent;
@@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import { reveal } from '$lib/actions/reveal'; import { reveal } from '$lib/actions/reveal';
import type { BookingContent } from '$lib/types'; import type { BookingContent } from '$lib/types';
type SuccessModalComponentType = typeof import('$lib/components/SuccessModal.svelte').default; type SuccessModalComponentType = typeof import('$lib/components/ui/SuccessModal.svelte').default;
type ErrorModalComponentType = typeof import('$lib/components/ErrorModal.svelte').default; type ErrorModalComponentType = typeof import('$lib/components/ui/ErrorModal.svelte').default;
export let booking: BookingContent; export let booking: BookingContent;
export let allowGeneralEnquiry = false; export let allowGeneralEnquiry = false;
@@ -87,12 +87,12 @@
async function ensureSuccessModal() { async function ensureSuccessModal() {
if (SuccessModalComponent) return; if (SuccessModalComponent) return;
SuccessModalComponent = (await import('$lib/components/SuccessModal.svelte')).default; SuccessModalComponent = (await import('$lib/components/ui/SuccessModal.svelte')).default;
} }
async function ensureErrorModal() { async function ensureErrorModal() {
if (ErrorModalComponent) return; if (ErrorModalComponent) return;
ErrorModalComponent = (await import('$lib/components/ErrorModal.svelte')).default; ErrorModalComponent = (await import('$lib/components/ui/ErrorModal.svelte')).default;
} }
function validateEmail(raw: string): string { function validateEmail(raw: string): string {
@@ -1,14 +1,14 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import { reveal } from '$lib/actions/reveal'; import { reveal } from '$lib/actions/reveal';
import type { BookingContent } from '$lib/types'; import type { BookingContent } from '$lib/types';
import { promoteJourney, trackEvent } from '$lib/analytics'; import { promoteJourney, trackEvent } from '$lib/analytics';
import { trackAb, type AbContext } from '$lib/ab'; import { trackAb, type AbContext } from '$lib/ab';
type SuccessModalComponentType = typeof import('$lib/components/SuccessModal.svelte').default; type SuccessModalComponentType = typeof import('$lib/components/ui/SuccessModal.svelte').default;
type ErrorModalComponentType = typeof import('$lib/components/ErrorModal.svelte').default; type ErrorModalComponentType = typeof import('$lib/components/ui/ErrorModal.svelte').default;
export let booking: BookingContent; export let booking: BookingContent;
export let pagePath = ''; export let pagePath = '';
@@ -156,12 +156,12 @@
async function ensureSuccessModal() { async function ensureSuccessModal() {
if (SuccessModalComponent) return; if (SuccessModalComponent) return;
SuccessModalComponent = (await import('$lib/components/SuccessModal.svelte')).default; SuccessModalComponent = (await import('$lib/components/ui/SuccessModal.svelte')).default;
} }
async function ensureErrorModal() { async function ensureErrorModal() {
if (ErrorModalComponent) return; if (ErrorModalComponent) return;
ErrorModalComponent = (await import('$lib/components/ErrorModal.svelte')).default; ErrorModalComponent = (await import('$lib/components/ui/ErrorModal.svelte')).default;
} }
function noteInteraction() { function noteInteraction() {
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { accordion } from '$lib/actions/accordion'; import { accordion } from '$lib/actions/accordion';
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import type { FaqItem } from '$lib/types'; import type { FaqItem } from '$lib/types';
export let title = 'FAQs'; export let title = 'FAQs';
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { reveal } from '$lib/actions/reveal'; import { reveal } from '$lib/actions/reveal';
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import { getEnhancedImage } from '$lib/enhanced-images'; import { getEnhancedImage } from '$lib/enhanced-images';
import type { FounderStoryContent } from '$lib/types'; import type { FounderStoryContent } from '$lib/types';
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import type { CallToAction, HeroContent } from '$lib/types'; import type { CallToAction, HeroContent } from '$lib/types';
import { trackAb, type AbContext } from '$lib/ab'; import { trackAb, type AbContext } from '$lib/ab';
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { reveal } from '$lib/actions/reveal'; import { reveal } from '$lib/actions/reveal';
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import type { HowItWorksContent } from '$lib/types'; import type { HowItWorksContent } from '$lib/types';
export let content: HowItWorksContent; export let content: HowItWorksContent;
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import FaqSection from '$lib/components/FaqSection.svelte'; import FaqSection from '$lib/components/sections/FaqSection.svelte';
import { locationPages } from '$lib/content/locations'; import { locationPages } from '$lib/content/locations';
import type { InfoContent } from '$lib/types'; import type { InfoContent } from '$lib/types';
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import type { HomePageContent } from '$lib/types'; import type { HomePageContent } from '$lib/types';
export let instagram: HomePageContent['instagram']; export let instagram: HomePageContent['instagram'];
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import type { IntroContent } from '$lib/types'; import type { IntroContent } from '$lib/types';
export let intro: IntroContent; export let intro: IntroContent;
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
export let context: 'onboarding' | 'contract' | 'owner' = 'onboarding'; export let context: 'onboarding' | 'contract' | 'owner' = 'onboarding';
$: introText = $: introText =
@@ -8,7 +8,7 @@
? "Enter the email address you used when enquiring with Goodwalk. We'll send you a one-time code to continue your contract." ? "Enter the email address you used when enquiring with Goodwalk. We'll send you a one-time code to continue your contract."
: "Enter the email address you used when enquiring with Goodwalk. We'll send you a one-time code to continue your onboarding."; : "Enter the email address you used when enquiring with Goodwalk. We'll send you a one-time code to continue your onboarding.";
const dispatch = createEventDispatcher<{ authenticated: { email: string; profile: Record<string, unknown>; draft: Record<string, unknown>; cpAdmin?: boolean; ownerEmail?: string } }>(); const dispatch = createEventDispatcher<{ authenticated: { email: string; profile: Record<string, unknown>; draft: Record<string, unknown>; cpAdmin?: boolean; ownerEmail?: string; previewEmails?: string[] } }>();
const ownerEmail = 'info@goodwalk.co.nz'; const ownerEmail = 'info@goodwalk.co.nz';
const ownerPhone = '(022) 642 1011'; const ownerPhone = '(022) 642 1011';
@@ -58,6 +58,7 @@
let draft: Record<string, unknown> = {}; let draft: Record<string, unknown> = {};
let cpAdmin = false; let cpAdmin = false;
let verifiedOwnerEmail = ''; let verifiedOwnerEmail = '';
let previewEmails: string[] = [];
try { try {
const verifyRes = await fetch('/api/auth/verify', { const verifyRes = await fetch('/api/auth/verify', {
headers: { Authorization: `Bearer ${data.token}` }, headers: { Authorization: `Bearer ${data.token}` },
@@ -68,9 +69,10 @@
draft = verifyData.draft ?? {}; draft = verifyData.draft ?? {};
cpAdmin = Boolean(verifyData.cpAdmin); cpAdmin = Boolean(verifyData.cpAdmin);
verifiedOwnerEmail = typeof verifyData.ownerEmail === 'string' ? verifyData.ownerEmail : ''; verifiedOwnerEmail = typeof verifyData.ownerEmail === 'string' ? verifyData.ownerEmail : '';
previewEmails = Array.isArray(verifyData.previewEmails) ? verifyData.previewEmails.filter((value: unknown): value is string => typeof value === 'string') : [];
} }
} catch { /* ignore */ } } catch { /* ignore */ }
dispatch('authenticated', { email: data.email, profile, draft, cpAdmin, ownerEmail: verifiedOwnerEmail }); dispatch('authenticated', { email: data.email, profile, draft, cpAdmin, ownerEmail: verifiedOwnerEmail, previewEmails });
} catch (e) { } catch (e) {
error = e instanceof Error ? e.message : 'Something went wrong'; error = e instanceof Error ? e.message : 'Something went wrong';
} finally { } finally {
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import { getEnhancedImage } from '$lib/enhanced-images'; import { getEnhancedImage } from '$lib/enhanced-images';
import type { CallToAction, HeroChip } from '$lib/types'; import type { CallToAction, HeroChip } from '$lib/types';
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { reveal } from '$lib/actions/reveal'; import { reveal } from '$lib/actions/reveal';
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import type { IconCard } from '$lib/types'; import type { IconCard } from '$lib/types';
export let services: IconCard[]; export let services: IconCard[];
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount, tick } from 'svelte'; import { onMount, tick } from 'svelte';
import { reveal } from '$lib/actions/reveal'; import { reveal } from '$lib/actions/reveal';
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import { getEnhancedImage } from '$lib/enhanced-images'; import { getEnhancedImage } from '$lib/enhanced-images';
import { getSeededTestimonialIndex } from '$lib/testimonials'; import { getSeededTestimonialIndex } from '$lib/testimonials';
import type { TestimonialContent } from '$lib/types'; import type { TestimonialContent } from '$lib/types';
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { reveal } from '$lib/actions/reveal'; import { reveal } from '$lib/actions/reveal';
import { getEnhancedImage } from '$lib/enhanced-images'; import { getEnhancedImage } from '$lib/enhanced-images';
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import type { IconCard } from '$lib/types'; import type { IconCard } from '$lib/types';
export let values: IconCard[]; export let values: IconCard[];
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
export let eyebrow = 'Get in touch'; export let eyebrow = 'Get in touch';
export let title: string; export let title: string;
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import ModalShell from '$lib/components/ModalShell.svelte'; import ModalShell from '$lib/components/ui/ModalShell.svelte';
export let email = 'info@goodwalk.co.nz'; export let email = 'info@goodwalk.co.nz';
export let enquiryType: 'booking' | 'general' = 'booking'; export let enquiryType: 'booking' | 'general' = 'booking';
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import type { FooterContent, LinkItem } from '$lib/types'; import type { FooterContent, LinkItem } from '$lib/types';
import { locationPages } from '$lib/content/locations'; import { locationPages } from '$lib/content/locations';
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/stores';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import type { NavigationContent } from '$lib/types'; import type { NavigationContent } from '$lib/types';
import type { Picture } from '@sveltejs/enhanced-img'; import type { Picture } from '@sveltejs/enhanced-img';
import logoDesktop from '$lib/images/goodwalk-auckland-dog-walking-logo.png?enhanced'; import logoDesktop from '$lib/images/goodwalk-auckland-dog-walking-logo.png?enhanced';
@@ -2,7 +2,7 @@ import { fireEvent, render } from '@testing-library/svelte';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import Header from './Header.svelte'; import Header from './Header.svelte';
import { homepageContent } from '$lib/content/homepage'; import { homepageContent } from '$lib/content/homepage';
import { setMockPage } from '../../test/mocks/app-stores'; import { setMockPage } from '../../../test/mocks/app-stores';
describe('Header', () => { describe('Header', () => {
it('marks the services link active for service detail pages', () => { it('marks the services link active for service detail pages', () => {
@@ -2,7 +2,7 @@
import { onMount, tick } from 'svelte'; import { onMount, tick } from 'svelte';
import { afterNavigate } from '$app/navigation'; import { afterNavigate } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import { isMobileCtaButtonEnabled } from '$lib/feature-flags'; import { isMobileCtaButtonEnabled } from '$lib/feature-flags';
/* /*
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
export let email = ''; export let email = '';
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
export let plan: { export let plan: {
title: string; title: string;
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import ModalShell from '$lib/components/ModalShell.svelte'; import ModalShell from '$lib/components/ui/ModalShell.svelte';
export let firstName: string; export let firstName: string;
export let petName: string; export let petName: string;
+2 -2
View File
@@ -36,9 +36,9 @@ export const homepageContent: HomePageContent = {
title: 'Come home to a', title: 'Come home to a',
highlight: 'calm, happy dog', highlight: 'calm, happy dog',
mobileTitle: 'Come home to a\ncalm, happy dog', mobileTitle: 'Come home to a\ncalm, happy dog',
seoHeading: 'Dog walking across Auckland Central', seoHeading: 'Dog walking for Auckland\'s small & medium dogs',
subtitle: subtitle:
'Trusted dog walking for Auckland\'s small and medium dogs. Thoughtful matching, familiar routines, and calmer evenings at home.', 'The same trusted walker every time, in small groups. Never a rotating roster.',
primaryCta: { label: 'Book a Meet & Greet', href: '#newlead', variant: 'yellow' }, primaryCta: { label: 'Book a Meet & Greet', href: '#newlead', variant: 'yellow' },
secondaryCta: { secondaryCta: {
label: 'See how it works', label: 'See how it works',
+16 -19
View File
@@ -260,35 +260,32 @@
/* ── Full-bleed mobile hero ── */ /* ── Full-bleed mobile hero ── */
#hero { #hero {
--hero-photo-band: 46svh;
min-height: 90svh; min-height: 90svh;
padding: 0; padding: 0;
gap: 0; gap: 0;
flex-direction: column; flex-direction: column;
justify-content: flex-end; justify-content: flex-start;
} }
/* Full-section gradient: transparent top Goodwalk green bottom. /* Two-zone mobile hero: the photo sits in its own clear band at the top so
hero-img is now a sibling of hero-inner (direct child of #hero), Maya's face is never covered, and the copy sits below on solid Goodwalk
so this ::after correctly overlays the full section. */ green. This ::after only softens the seam where the photo meets the green
text zone; it no longer veils the dog. */
#hero::after { #hero::after {
inset: 0; inset: auto 0 auto 0;
height: auto; top: calc(var(--hero-photo-band) - 72px);
background: linear-gradient( height: 72px;
to bottom, background: linear-gradient(to bottom, transparent, var(--surface-brand));
transparent 22%,
rgba(var(--brand-rgb), 0.45) 48%,
rgba(var(--brand-rgb), 0.88) 65%,
var(--surface-brand) 78%
);
} }
/* Dog photo fills the entire section as a background layer */ /* Dog photo occupies a dedicated band at the top, not a full-bleed layer */
.hero-img { .hero-img {
position: absolute; position: relative;
inset: 0; inset: auto;
z-index: 0; z-index: 0;
width: 100%; width: 100%;
height: 100%; height: var(--hero-photo-band);
margin: 0; margin: 0;
flex: none; flex: none;
display: block; display: block;
@@ -306,7 +303,7 @@
max-width: none; max-width: none;
margin: 0; margin: 0;
object-fit: cover; object-fit: cover;
object-position: 37% center; object-position: 35% 30%;
transform: none; transform: none;
} }
@@ -319,7 +316,7 @@
width: 100%; width: 100%;
max-width: none; max-width: none;
margin: 0; margin: 0;
padding: 0 var(--space-container-x-mobile) 36px; padding: 24px var(--space-container-x-mobile) 36px;
} }
.hero-text { .hero-text {
+2 -2
View File
@@ -3,8 +3,8 @@
import { navigating, page } from '$app/stores'; import { navigating, page } from '$app/stores';
import { afterNavigate, disableScrollHandling } from '$app/navigation'; import { afterNavigate, disableScrollHandling } from '$app/navigation';
import { initClickTracking, trackPageView } from '$lib/analytics'; import { initClickTracking, trackPageView } from '$lib/analytics';
import MobileBookBar from '$lib/components/MobileBookBar.svelte'; import MobileBookBar from '$lib/components/ui/MobileBookBar.svelte';
import RouteSkeleton from '$lib/components/RouteSkeleton.svelte'; import RouteSkeleton from '$lib/components/ui/RouteSkeleton.svelte';
import { isMobileCtaButtonEnabled } from '$lib/feature-flags'; import { isMobileCtaButtonEnabled } from '$lib/feature-flags';
import readex400Woff2 from '@fontsource/readex-pro/files/readex-pro-latin-400-normal.woff2?url'; import readex400Woff2 from '@fontsource/readex-pro/files/readex-pro-latin-400-normal.woff2?url';
import readex700Woff2 from '@fontsource/readex-pro/files/readex-pro-latin-700-normal.woff2?url'; import readex700Woff2 from '@fontsource/readex-pro/files/readex-pro-latin-700-normal.woff2?url';
+13 -26
View File
@@ -1,17 +1,17 @@
<script lang="ts"> <script lang="ts">
import SeoHead from '$lib/components/SeoHead.svelte'; import SeoHead from '$lib/components/ui/SeoHead.svelte';
import Footer from '$lib/components/Footer.svelte'; import Footer from '$lib/components/ui/Footer.svelte';
import Header from '$lib/components/Header.svelte'; import Header from '$lib/components/ui/Header.svelte';
import HeroSection from '$lib/components/HeroSection.svelte'; import HeroSection from '$lib/components/sections/HeroSection.svelte';
import HowItWorksSection from '$lib/components/HowItWorksSection.svelte'; import HowItWorksSection from '$lib/components/sections/HowItWorksSection.svelte';
import InfoSection from '$lib/components/InfoSection.svelte'; import InfoSection from '$lib/components/sections/InfoSection.svelte';
import InstagramSection from '$lib/components/InstagramSection.svelte'; import InstagramSection from '$lib/components/sections/InstagramSection.svelte';
import BookingWizard from '$lib/components/BookingWizard.svelte'; import BookingWizard from '$lib/components/sections/BookingWizard.svelte';
import FounderStorySection from '$lib/components/FounderStorySection.svelte'; import FounderStorySection from '$lib/components/sections/FounderStorySection.svelte';
import ServicesSection from '$lib/components/ServicesSection.svelte'; import ServicesSection from '$lib/components/sections/ServicesSection.svelte';
import TestimonialsSection from '$lib/components/TestimonialsSection.svelte'; import TestimonialsSection from '$lib/components/sections/TestimonialsSection.svelte';
import ValuesSection from '$lib/components/ValuesSection.svelte'; import ValuesSection from '$lib/components/sections/ValuesSection.svelte';
import OnboardingPage from '$lib/components/OnboardingPage.svelte'; import OnboardingPage from '$lib/components/pages/OnboardingPage.svelte';
import { buildAreaServed } from '$lib/seo'; import { buildAreaServed } from '$lib/seo';
import type { PageData } from './$types'; import type { PageData } from './$types';
import type { CallToAction } from '$lib/types'; import type { CallToAction } from '$lib/types';
@@ -139,19 +139,6 @@
name: 'Public Liability Insurance' name: 'Public Liability Insurance'
} }
] ]
},
{
'@context': 'https://schema.org',
'@type': 'HowTo',
name: 'How to start with Goodwalk',
description: data.content.howItWorks.intro,
totalTime: 'P14D',
step: data.content.howItWorks.steps.map((step, index) => ({
'@type': 'HowToStep',
position: index + 1,
name: step.title,
text: step.body
}))
} }
] ]
: []; : [];
+10 -10
View File
@@ -1,15 +1,15 @@
<script lang="ts"> <script lang="ts">
import SeoHead from '$lib/components/SeoHead.svelte'; import SeoHead from '$lib/components/ui/SeoHead.svelte';
import AboutPage from '$lib/components/AboutPage.svelte'; import AboutPage from '$lib/components/pages/AboutPage.svelte';
import Footer from '$lib/components/Footer.svelte'; import Footer from '$lib/components/ui/Footer.svelte';
import Header from '$lib/components/Header.svelte'; import Header from '$lib/components/ui/Header.svelte';
import BookingPage from '$lib/components/BookingPage.svelte'; import BookingPage from '$lib/components/pages/BookingPage.svelte';
import LegalPage from '$lib/components/LegalPage.svelte'; import LegalPage from '$lib/components/pages/LegalPage.svelte';
import PageHeader from '$lib/components/PageHeader.svelte'; import PageHeader from '$lib/components/ui/PageHeader.svelte';
import PricingPage from '$lib/components/PricingPage.svelte'; import PricingPage from '$lib/components/pages/PricingPage.svelte';
import TestimonialsPage from '$lib/components/TestimonialsPage.svelte'; import TestimonialsPage from '$lib/components/pages/TestimonialsPage.svelte';
import { aboutPageContent } from '$lib/content/about'; import { aboutPageContent } from '$lib/content/about';
import ServiceLandingPage from '$lib/components/ServiceLandingPage.svelte'; import ServiceLandingPage from '$lib/components/pages/ServiceLandingPage.svelte';
import { dogWalkingContent } from '$lib/content/dog-walking'; import { dogWalkingContent } from '$lib/content/dog-walking';
import { ourPricingContent } from '$lib/content/our-pricing'; import { ourPricingContent } from '$lib/content/our-pricing';
import { packWalksContent } from '$lib/content/pack-walks'; import { packWalksContent } from '$lib/content/pack-walks';
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import ContractPage from '$lib/components/ContractPage.svelte'; import ContractPage from '$lib/components/pages/ContractPage.svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
export let data: PageData; export let data: PageData;
+3 -3
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import Header from '$lib/components/Header.svelte'; import Header from '$lib/components/ui/Header.svelte';
import Footer from '$lib/components/Footer.svelte'; import Footer from '$lib/components/ui/Footer.svelte';
import SeoHead from '$lib/components/SeoHead.svelte'; import SeoHead from '$lib/components/ui/SeoHead.svelte';
import { locationPages } from '$lib/content/locations'; import { locationPages } from '$lib/content/locations';
import { buildBreadcrumb } from '$lib/seo'; import { buildBreadcrumb } from '$lib/seo';
import type { PageData } from './$types'; import type { PageData } from './$types';
+4 -4
View File
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import Header from '$lib/components/Header.svelte'; import Header from '$lib/components/ui/Header.svelte';
import Footer from '$lib/components/Footer.svelte'; import Footer from '$lib/components/ui/Footer.svelte';
import SeoHead from '$lib/components/SeoHead.svelte'; import SeoHead from '$lib/components/ui/SeoHead.svelte';
import LocationPage from '$lib/components/LocationPage.svelte'; import LocationPage from '$lib/components/pages/LocationPage.svelte';
import { buildLocationSeo } from '$lib/seo'; import { buildLocationSeo } from '$lib/seo';
import type { PageData } from './$types'; import type { PageData } from './$types';
+7 -7
View File
@@ -10,13 +10,13 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import Footer from '$lib/components/Footer.svelte'; import Footer from '$lib/components/ui/Footer.svelte';
import Header from '$lib/components/Header.svelte'; import Header from '$lib/components/ui/Header.svelte';
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import { homepageContent } from '$lib/content/homepage'; import { homepageContent } from '$lib/content/homepage';
type SuccessModalComponentType = typeof import('$lib/components/SuccessModal.svelte').default; type SuccessModalComponentType = typeof import('$lib/components/ui/SuccessModal.svelte').default;
type ErrorModalComponentType = typeof import('$lib/components/ErrorModal.svelte').default; type ErrorModalComponentType = typeof import('$lib/components/ui/ErrorModal.svelte').default;
const navigation = homepageContent.navigation; const navigation = homepageContent.navigation;
const footerContent = homepageContent.footer; const footerContent = homepageContent.footer;
@@ -131,11 +131,11 @@
async function ensureSuccessModal() { async function ensureSuccessModal() {
if (SuccessModalComponent) return; if (SuccessModalComponent) return;
SuccessModalComponent = (await import('$lib/components/SuccessModal.svelte')).default; SuccessModalComponent = (await import('$lib/components/ui/SuccessModal.svelte')).default;
} }
async function ensureErrorModal() { async function ensureErrorModal() {
if (ErrorModalComponent) return; if (ErrorModalComponent) return;
ErrorModalComponent = (await import('$lib/components/ErrorModal.svelte')).default; ErrorModalComponent = (await import('$lib/components/ui/ErrorModal.svelte')).default;
} }
function noteInteraction() { function noteInteraction() {
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
import PageHeader from '$lib/components/PageHeader.svelte'; import PageHeader from '$lib/components/ui/PageHeader.svelte';
import VariantApplicationFields from '$lib/components/contact-form-variants/VariantApplicationFields.svelte'; import VariantApplicationFields from '$lib/components/contact-form-variants/VariantApplicationFields.svelte';
type VariantTab = { type VariantTab = {
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
const heroImage = '/images/smiling-happy-dog-maya.webp'; const heroImage = '/images/smiling-happy-dog-maya.webp';
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/ui/Icon.svelte';
const instagramHref = 'https://www.instagram.com/goodwalk.nz/'; const instagramHref = 'https://www.instagram.com/goodwalk.nz/';
+12 -12
View File
@@ -1,16 +1,16 @@
<script lang="ts"> <script lang="ts">
import SeoHead from '$lib/components/SeoHead.svelte'; import SeoHead from '$lib/components/ui/SeoHead.svelte';
import Footer from '$lib/components/Footer.svelte'; import Footer from '$lib/components/ui/Footer.svelte';
import Header from '$lib/components/Header.svelte'; import Header from '$lib/components/ui/Header.svelte';
import HeroSection from '$lib/components/HeroSection.svelte'; import HeroSection from '$lib/components/sections/HeroSection.svelte';
import HowItWorksSection from '$lib/components/HowItWorksSection.svelte'; import HowItWorksSection from '$lib/components/sections/HowItWorksSection.svelte';
import InfoSection from '$lib/components/InfoSection.svelte'; import InfoSection from '$lib/components/sections/InfoSection.svelte';
import InstagramSection from '$lib/components/InstagramSection.svelte'; import InstagramSection from '$lib/components/sections/InstagramSection.svelte';
import BookingWizard from '$lib/components/BookingWizard.svelte'; import BookingWizard from '$lib/components/sections/BookingWizard.svelte';
import FounderStorySection from '$lib/components/FounderStorySection.svelte'; import FounderStorySection from '$lib/components/sections/FounderStorySection.svelte';
import ServicesSection from '$lib/components/ServicesSection.svelte'; import ServicesSection from '$lib/components/sections/ServicesSection.svelte';
import TestimonialsSection from '$lib/components/TestimonialsSection.svelte'; import TestimonialsSection from '$lib/components/sections/TestimonialsSection.svelte';
import ValuesSection from '$lib/components/ValuesSection.svelte'; import ValuesSection from '$lib/components/sections/ValuesSection.svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
export let data: PageData; export let data: PageData;
+4
View File
@@ -169,6 +169,7 @@ export default defineConfig(() => {
const createMailProxy = (apiPath: keyof typeof proxyRoutes): ProxyOptions => ({ const createMailProxy = (apiPath: keyof typeof proxyRoutes): ProxyOptions => ({
target: submitProxyTarget, target: submitProxyTarget,
changeOrigin: true,
rewrite: (path) => path.replace(apiPath, proxyRoutes[apiPath]), rewrite: (path) => path.replace(apiPath, proxyRoutes[apiPath]),
configure(proxy) { configure(proxy) {
proxy.on('error', (error: NodeJS.ErrnoException, req, res) => { proxy.on('error', (error: NodeJS.ErrnoException, req, res) => {
@@ -213,14 +214,17 @@ export default defineConfig(() => {
'/api/contract-submit': createMailProxy('/api/contract-submit'), '/api/contract-submit': createMailProxy('/api/contract-submit'),
'/api/auth': { '/api/auth': {
target: submitProxyTarget, target: submitProxyTarget,
changeOrigin: true,
rewrite: (path) => path.replace('/api/auth', '/auth'), rewrite: (path) => path.replace('/api/auth', '/auth'),
}, },
'/api/owner': { '/api/owner': {
target: submitProxyTarget, target: submitProxyTarget,
changeOrigin: true,
rewrite: (path) => path.replace('/api/owner', '/owner'), rewrite: (path) => path.replace('/api/owner', '/owner'),
}, },
'/api/save-draft': { '/api/save-draft': {
target: submitProxyTarget, target: submitProxyTarget,
changeOrigin: true,
rewrite: (path) => path.replace('/api/save-draft', '/auth/save-draft'), rewrite: (path) => path.replace('/api/save-draft', '/auth/save-draft'),
}, },
} }