/// // // Adds intentional-cancellation support to scrape_runs — distinct from // a job merely surviving a frontend disconnect (CLAUDE.md §18/§21, // jobs.py's run_research_job already runs to completion regardless of // the dashboard's polling state, by design). This migration adds the // PM-facing "stop this analysis" affordance: // // cancel_requested bool set true by POST /research/cancel/, // checked by run_research_job() between // competitors — see jobs.py's per-competitor // loop for why this is a between-competitor // checkpoint rather than an interrupt of a // scrape/extraction already in flight // status: 'cancelled' new select option, distinct from 'failed' // so the dashboard can show "Cancelled by // you" rather than an error state migrate((app) => { const collection = app.findCollectionByNameOrId('scrape_runs'); collection.fields.add(new Field({ type: 'bool', name: 'cancel_requested', })); const statusField = collection.fields.getByName('status'); statusField.values = [...statusField.values, 'cancelled']; return app.save(collection); }, (app) => { const collection = app.findCollectionByNameOrId('scrape_runs'); collection.fields.removeByName('cancel_requested'); const statusField = collection.fields.getByName('status'); statusField.values = statusField.values.filter((v) => v !== 'cancelled'); return app.save(collection); });