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
+31 -1
View File
@@ -48,7 +48,7 @@ def setup_logging() -> logging.Logger:
root.addHandler(rotating)
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
@@ -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_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))))
# 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, ...]:
@@ -211,6 +215,10 @@ _DATA_DIR = Path(os.environ.get("DATA_DIR", "data"))
ALLOWED_EMAILS_FILE = _DATA_DIR / "allowed_emails.json"
CLIENT_PROFILES_FILE = _DATA_DIR / "client_profiles.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
# 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
@@ -221,6 +229,27 @@ LEGACY_SEED_FILE = Path(
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) ──
OWNER_EMAIL = settings.owner_email
@@ -268,3 +297,4 @@ logger.info(
RATE_LIMIT_MIN_INTERVAL_SECONDS,
EMAIL_SEND_TIMEOUT_SECONDS,
)
logger.info("MYOB integration: %s", "enabled" if MYOB_ENABLED else "disabled (set MYOB_* env vars to enable)")