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>
Rearchitects the Sheet integration per updated CLAUDE.md §15: a
stateless Flask backend has no way to reach into whichever Sheet a PM
has open, so the Sheet pulls its latest results on demand rather than
Flask ever pushing to it. This is also the simpler path for
non-technical users — no Sheet ID to find/paste, no service-account
sharing, just one button in the sidebar.
Apps Script (appscript/, new):
- appsscript.json — Editor Add-on manifest, spreadsheets.currentonly
scope (touches only whichever Sheet is open, nothing else in Drive)
- Code.gs — onOpen/onInstall/onHomepage, opens the sidebar
- Validation.gs — validateLicence() (6hr cache, POST /validate-email),
plus markOnboardingStepOnce_() shared helper
- Sync.gs — pullLatestResults()/getWorkbookTabs()/detectColumns(),
with a real header-alias map for every STANDARD_FIELDS column
(majorityPrice matches "Rack Rate"/"Standard Price"/etc, not just
one exact label) so silent write failures don't happen from a PM
naming a column slightly differently than expected
- Sidebar.html — tab selector + Pull latest results + last-pull status
Backend:
- GET /research/history/latest (email-resolved) replaces the removed
POST /sheet/push and GET /sheet/tabs
- GET /account/template now reads TEMPLATE_SHEET_URL instead of a
hardcoded placeholder
- PATCH /account/onboarding now accepts either a JWT session (dashboard)
or an email in the body (Apps Script has no JWT, only the PM's Google
email) — this was a real gap: Sync.gs's onboarding-flag calls would
have 401'd against the JWT-only version
- onboarding_service.DEFAULT_CHECKLIST corrected to the current schema
(template_imported/sidebar_installed/sync_from_sheet, not the stale
push_to_sheet/download_template)
- Fixed the same stale field names in CLAUDE.md §32's onboarding
runbook example payload, so it doesn't teach a future tenant-creation
script the wrong schema
Frontend:
- PushToSheet.vue removed, replaced by SyncToSheet.vue — a clipboard
copy button + instructions to use the sidebar, no tab selector (tab
selection lives in the sidebar, where the workbook is actually open)
- AccountPage.vue's template section rebuilt as three items: open
template, two-step import instructions + sidebar install link, and
a deliberately no-op verification input (never validated/fetched/
stored — the confirmation is purely the PM telling themselves it's
done, per explicit instruction not to create a dependency on their
workbook)
- useOnboardingTour.js step 5 updated to match
220 backend tests passing. Frontend production build verified clean.
Apps Script (.gs/.html) cannot be unit-tested in this environment —
no Google Apps Script runtime available locally; verified only by
careful cross-file consistency review (function names/signatures
matching across Code.gs/Validation.gs/Sync.gs/Sidebar.html).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds CLAUDE.md (authoritative spec), the legacy app.py scraper and
approved dashboard mockup as reference material under z_starting_files/,
and the phased dev_plan.md for Phase 1+ implementation.