///
//
// 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);
});