/// // // Adds `trip_watchlist` — the per-seat trip-monitoring feature. Each // active seat can pin a small, tier-capped number of specific already- // analyzed competitor trips (WATCHLIST_CAP_PER_SEAT in // services/watchlist_service.py — 1 per seat at Analyse). The nightly // n8n cron re-scrapes exactly these URLs instead of each competitor's // root website, replacing an unbounded, low-signal daily scrape (a // homepage `is_generic_homepage()` already flags as non-trip-specific) // with a small, predictable, and meaningful one — every resulting // price/product alert is about a trip a PM explicitly chose to track. // // Reuses the exact same confirmed_url / per-url content-hash caching // already built for manual trip-intent analysis this session — no // schema change needed on products/scrape_runs/competitors, and no // jobs.py/analysis_service.py code change either. See // n8n/workflows/daily_scrape_cron.json for the cron-side wiring. migrate((app) => { const tenants = app.findCollectionByNameOrId('tenants'); const tenantSeats = app.findCollectionByNameOrId('tenant_seats'); const competitors = app.findCollectionByNameOrId('competitors'); const rel = (name, collection, opts = {}) => ({ name, type: 'relation', collectionId: collection.id, cascadeDelete: false, minSelect: 0, maxSelect: 1, ...opts, }); const text = (name, opts = {}) => ({ name, type: 'text', ...opts }); const autodate = (name, onCreate, onUpdate = false) => ({ name, type: 'autodate', onCreate, onUpdate, }); const collection = new Collection({ type: 'base', name: 'trip_watchlist', fields: [ rel('tenant_id', tenants, { required: true }), rel('seat_id', tenantSeats, { required: true }), rel('competitor_id', competitors, { required: true }), text('url', { required: true }), text('trip_name'), autodate('added_at', true), ], indexes: [ 'CREATE INDEX idx_trip_watchlist_seat ON trip_watchlist (seat_id)', 'CREATE INDEX idx_trip_watchlist_tenant ON trip_watchlist (tenant_id)', ], // Same access model as every other tenant-scoped collection — Flask // is the only direct PocketBase client, enforcing tenant_id/seat_id // scoping itself. listRule: null, viewRule: null, createRule: null, updateRule: null, deleteRule: null, }); return app.save(collection); }, (app) => { const collection = app.findCollectionByNameOrId('trip_watchlist'); if (collection) app.delete(collection); });