Commit Graph

4 Commits

Author SHA1 Message Date
JasonFraser e0de2df6bf Phase 5 + 6: automation, content-hash change detection, semantic trip matching
Phase 5 — Automation:
- Daily scrape cron, daily digest email, and Monday brief n8n workflows
- price_history_repository.py + price diffing/is_new detection wired
  into analysis_service's refresh path
- Numeric unread-count badge on the topbar bell (was a plain dot)

Change detection architecture correction:
- CLAUDE.md's Firecrawl Monitor design doesn't match the real
  self-hosted product: no /v1/monitor in v1, the real /v2/monitor is
  cron-scheduled and credit-metered (Supabase-gated), and self-hosted
  Firecrawl is a 3-service stack (api/worker/playwright-service), not
  the single container originally spec'd
- Replaced with content-hash comparison computed during the scrape
  itself (core/scraper.py's compute_content_hash/normalise_html,
  analysis_service's detect_change()) — no external dependency, and
  the refresh path now skips LLM extraction entirely when a scrape
  finds no content diff
- Fixed a real bug in the first pass of this migration: change_detected
  was set and then immediately reset within the same function call,
  making it unobservable to the CompetitorsPage badge, the Apps Script
  sidebar, and the n8n cron filter. It now stays visible until the
  next scrape confirms nothing further changed.
- Added a composite index on scrape_runs(competitor_id, content_hash,
  started_at) backing the per-competitor recency lookup
- Docker Compose's firecrawl service replaced with the real
  firecrawl-api/firecrawl-worker/firecrawl-playwright stack, gated
  behind an opt-in profile
- Added CORS (absent from the original spec entirely) so the
  browser-based dashboard can reach the API cross-origin

Phase 6 — Semantic trip matching:
- client_trips_repository.py — this collection existed in schema but
  nothing ever populated it; comparable-trip matching could never
  produce a real match without it
- embedding_service.save_client_trip() upserts a client_trips record
  from every /research submission's trip-intent description, with
  stale-embedding invalidation when destination/duration change
- /internal/embed-products now embeds client_trips as well as
  competitor products; /internal/match-comparable reads both sides
  from PocketBase instead of expecting client_trips in the request
  body, so it's callable unattended by n8n
- comparable_matches upserts on (client_product, competitor_product)
  instead of always creating — first_matched/last_matched only make
  sense if repeat weekly matches update in place, and a PM's dismissed
  match now survives re-matching
- embedding_and_matching_cron.json — Sunday night n8n workflow

249 tests passing, 96% coverage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-02 11:23:23 -04:00
JasonFraser c0a77a1a65 Fill in missing Resend templates + Freescout support links
Closes gaps identified after Phase 3: the four email templates named
in CLAUDE.md's file tree (§5) were only partially built (weekly brief
only). Adds:

- welcome.html, trial_reminder.html, price_alert.html, new_product.html
  — matching the brief.html styling pattern
- services/email_service.py — send_welcome_email(),
  send_trial_reminder_email(), send_price_alert_email(),
  send_new_product_email(), all via the same plain-Jinja2 + Resend
  pattern as brief_service.py (side-effect failures logged, never raised)
- Three new internal routes for n8n/manual-onboarding to call:
  POST /internal/welcome-email/<tenant_id>
  POST /internal/trial-reminder/<tenant_id>
  POST /internal/alert-email/<alert_id> (dispatches to price_alert or
  new_product template by alert_type, marks delivered_email on success)
- alert_repository.mark_email_delivered() to back that last route

Also adds the "Contact support" (Freescout) placeholder link CLAUDE.md
calls for in both the sidebar (visible on every page) and AccountPage,
pointing at support.bettersight.io. The actual Freescout container,
DNS/Pangolin routing, and mailbox are real infra this backend can't
provision — the link is a placeholder until that's deployed.

216 backend tests passing, 95% coverage, 100% on the new email_service.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-01 13:32:56 -04:00
JasonFraser 11610b8b4a Phase 3: Vue 3 + Nuxt UI v4 dashboard
Implements CLAUDE.md Build Order Phase 3 — the 7-page dashboard,
matching the approved bettersight-dashboard.html mockup's design
system (dot-grid motif, dark sidebar, card shadows, badges) exactly.

Scope note: nuxt-ui-templates/dashboard-vue (the template CLAUDE.md
says to clone) ships entirely in TypeScript, which directly conflicts
with §18's "JavaScript only, no .ts files" rule. Per user decision,
converted the template to plain JS and rebuilt the dashboard chrome
(sidebar/topbar) as custom components matching the mockup instead of
Nuxt UI's own dashboard components, since the mockup is the approved
design (§25) and Nuxt UI's dashboard components have their own default
look that would fight against it.

- Core plumbing: pocketbase.js client, api_service.js (X-API-Key +
  JWT + 401-refresh interceptor), auth_store/analysis_store/
  notifications_store (Pinia), 5 repositories, explicit vue-router
  (v4, not v5 — matching CLAUDE.md's stack table) with an auth guard
- Shared components: programmatic DotGrid.vue, StatCard, EmptyState,
  CompetitorForm, SeatManager, BattlecardCard, SubscriptionCard,
  CsvUpload+ImportPreviewModal, FeedItem, AlertCard
- All 7 pages: Login (+Google OAuth, forgot password), ResetPassword,
  Account (subscription, upgrade prompt, seats, onboarding), Competitors
  (CSV bulk import), Analyse (full find-urls → confirm → poll → results
  → confidence → push-to-sheet flow with all 4 JobProgress terminal
  states), Activity (stat row, feed, alerts, onboarding tour trigger),
  Battlecards
- Shepherd.js 5-step onboarding tour ported verbatim from §21
- Sentry init (prod-only) + global error handler in main.js

Two backend gaps surfaced and fixed while building this:
- Added GET /account/me (dashboard had no way to learn its own
  tenant_id after login) and fixed GET /account/<tenant_id> to verify
  the session's tenant matches the requested one (previously any valid
  X-API-Key could read any tenant's account data)
- Added GET /alerts and PATCH /alerts/<id>/read (no route existed to
  list or acknowledge alerts, needed by the sidebar badge and
  ActivityPage's alerts panel)

Deliberately deferred: the mockup's Price Position sparkline/corridor
card has no defined data contract anywhere in CLAUDE.md — omitted
rather than backed with fabricated numbers. /sheet/push and
/sheet/tabs still return their Phase-1 structured 501 pending Phase 4.

Verified: full production build succeeds, dev server serves the login
page correctly (screenshotted, zero console errors), and the router's
auth guard correctly redirects an unauthenticated session to
/login?return=... with no console errors. Backend: 202 tests still
passing after the two route additions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-01 13:16:31 -04:00
JasonFraser f7aaafa533 Phase 1: data foundation — schema, repositories, services, routes, jobs
Implements CLAUDE.md Build Order Phase 1 end to end:
- PocketBase migration covering all 15 MVP-scope collections
- Repository layer (11 repos) with real PocketBase-backed + in-memory
  mock implementations for every collection
- Service layer (licence, trip-finder, battlecard, embedding, brief,
  alert, analysis, onboarding, billing, auth) — Phase 2 scraping/
  extraction dependencies are lazy-imported so this layer is fully
  testable ahead of the app.py refactor
- Flask API (api.py) covering every MVP route from the spec, with
  X-API-Key auth, Flask-Limiter rate limits, and structured JSON errors
- RQ job queue (jobs.py) with per-competitor failure isolation and
  duration-based Gotify alerting
- 142 tests, 95% coverage, 100% on licence validation / seat
  management / Stripe webhook dispatch per CLAUDE.md's testing floor

/sheet/push and /sheet/tabs intentionally return structured 501s until
the Apps Script Web App deployment exists (Phase 4).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-01 12:21:04 -04:00