Files
linki/scripts/force-next-step.js
2026-08-01 14:01:17 +02:00

154 lines
6.2 KiB
JavaScript

#!/usr/bin/env node
// Force a run's next step to fire immediately instead of waiting for the
// normal delay/recheck schedule. Useful when testing a workflow end-to-end
// on a single lead, without waiting hours for the real schedule.
//
// Usage:
// node scripts/force-next-step.js --run <run_id> --target <target_id> [--apply] [--skip-schedule]
// node scripts/force-next-step.js --run <run_id> --all-targets [--apply] [--skip-schedule] (whole run — careful, real sends)
// node scripts/force-next-step.js --list (list active runs)
//
// Without --apply this only PRINTS what would change (dry run). Pass --apply
// to actually update the DB. DB path follows the same LINKI_DB_PATH env var
// (or ./linki.db) that the app itself uses.
//
// --target is required unless you pass --all-targets: forcing an entire real
// campaign to fire at once means every eligible lead gets messaged/connected
// right now instead of on its normal schedule. Scope to one lead for testing.
//
// --skip-schedule: setting next_step_at alone isn't enough — the runner
// re-checks the account's working-hours window (active_hours_start/end,
// timezone, working_days) on every tick and reschedules again if outside it
// ("Outside working schedule"). This flag additionally widens the run's
// LinkedIn account (and email account, if any) to a 24/7 window so the
// forced step actually fires on the next tick instead of bouncing. Only
// takes effect together with --apply. Does NOT auto-restore the original
// hours — note the printed "was" values and set them back in Settings when
// you're done testing.
const path = require("path");
const Database = require("better-sqlite3");
const DB_PATH = process.env.LINKI_DB_PATH || path.join(process.cwd(), "linki.db");
function parseArgs(argv) {
const args = { apply: false, list: false, allTargets: false, skipSchedule: false, run: null, target: null };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === "--apply") args.apply = true;
else if (a === "--list") args.list = true;
else if (a === "--all-targets") args.allTargets = true;
else if (a === "--skip-schedule") args.skipSchedule = true;
else if (a === "--run") args.run = argv[++i];
else if (a === "--target") args.target = argv[++i];
}
return args;
}
function widenSchedule(db, table, id) {
const before = db.prepare(`SELECT active_hours_start, active_hours_end, working_days FROM ${table} WHERE id = ?`).get(id);
if (!before) return null;
db.prepare(`UPDATE ${table} SET active_hours_start = 0, active_hours_end = 24, working_days = '1,2,3,4,5,6,7' WHERE id = ?`).run(id);
return before;
}
function main() {
const args = parseArgs(process.argv.slice(2));
const db = new Database(DB_PATH);
if (args.list) {
const runs = db
.prepare(
`SELECT r.id, r.status, w.name as workflow_name, r.created_at
FROM runs r JOIN workflows w ON w.id = r.workflow_id
WHERE r.status IN ('running', 'paused')
ORDER BY r.created_at DESC`
)
.all();
console.log(JSON.stringify(runs, null, 2));
db.close();
return;
}
if (!args.run) {
console.error("Usage: node scripts/force-next-step.js --run <run_id> --target <target_id> [--apply]");
console.error(" node scripts/force-next-step.js --run <run_id> --all-targets [--apply]");
console.error(" node scripts/force-next-step.js --list");
process.exit(1);
}
if (!args.target && !args.allTargets) {
console.error("Refusing to run: pass --target <target_id> to scope this to one lead (recommended for testing),");
console.error("or --all-targets to force the ENTIRE run (every eligible lead fires immediately — real sends).");
db.close();
process.exit(1);
}
const where = ["rp.run_id = ?"];
const params = [args.run];
if (args.target) {
where.push("rp.target_id = ?");
params.push(args.target);
}
const tracks = db
.prepare(
`SELECT rt.id, rt.track, rt.state, rt.current_step, rt.next_step_at,
rp.target_id, t.full_name
FROM run_profile_tracks rt
JOIN run_profiles rp ON rp.id = rt.run_profile_id
JOIN targets t ON t.id = rp.target_id
WHERE ${where.join(" AND ")} AND rt.state != 'completed'`
)
.all(...params);
if (tracks.length === 0) {
console.log("No eligible tracks found for that run/target.");
db.close();
return;
}
console.log(`${args.apply ? "Updating" : "[DRY RUN] Would update"} ${tracks.length} track(s):`);
for (const t of tracks) {
console.log(` - ${t.full_name} (${t.track}, step ${t.current_step}, state=${t.state}) next_step_at: ${t.next_step_at} -> now`);
}
if (args.skipSchedule) {
const run = db.prepare("SELECT account_id, email_account_id FROM runs WHERE id = ?").get(args.run);
if (!run) {
console.error(`No run found with id ${args.run}`);
db.close();
process.exit(1);
}
if (!args.apply) {
console.log("\n[DRY RUN] --skip-schedule would widen this run's account(s) to a 24/7 window.");
} else {
const accountBefore = widenSchedule(db, "accounts", run.account_id);
if (accountBefore) {
console.log(`Widened LinkedIn account ${run.account_id} to 24/7 (was: ${accountBefore.active_hours_start}-${accountBefore.active_hours_end}, days ${accountBefore.working_days}). Restore this in Settings when done testing.`);
}
if (run.email_account_id) {
const emailBefore = widenSchedule(db, "email_accounts", run.email_account_id);
if (emailBefore) {
console.log(`Widened email account ${run.email_account_id} to 24/7 (was: ${emailBefore.active_hours_start}-${emailBefore.active_hours_end}, days ${emailBefore.working_days}). Restore this in Settings when done testing.`);
}
}
}
}
if (args.apply) {
const update = db.prepare("UPDATE run_profile_tracks SET next_step_at = datetime('now') WHERE id = ?");
const tx = db.transaction((rows) => {
for (const t of rows) update.run(t.id);
});
tx(tracks);
console.log("Done. The runner will pick these up on its next loop tick (a few seconds).");
} else {
console.log("\nDry run only — pass --apply to actually update the DB.");
}
db.close();
}
main();