diff --git a/.gitignore b/.gitignore index 54effff..04dacf7 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,4 @@ scripts/update.log # clicker virtualenv (Linux/Pi) clicker/.venv/ +scripts/.last-check diff --git a/README.md b/README.md index cab405e..bed3ce8 100644 --- a/README.md +++ b/README.md @@ -210,8 +210,23 @@ AutoFirmer, and restarts the clicker if anything under `clicker/` changed. **A failed build is never deployed.** The build runs before anything restarts, so a broken push leaves the previous build serving and logs the failure instead. -Everything it does is appended to `scripts/update.log`. To see what it would do -without touching anything: +### Checking it is alive + +`scripts/.last-check` is rewritten on every check, whether or not anything came +down: + +``` +2026-08-31T00:43:27.155Z up to date at 27a61fd2 +``` + +This exists because the alternatives mislead. `update.log` only records real +events, so it stays empty for days when nothing is pushed — and PM2 reports a +cron-restart process as `stopped` with a restart count of `0` even while it is +firing on schedule. Neither is evidence of a problem; `.last-check` is the +signal to trust. + +Everything that actually happens is appended to `scripts/update.log`. To see +what it would do without touching anything: ```powershell node scripts\update-check.mjs --dry-run diff --git a/scripts/seed-settings 2.js b/scripts/seed-settings 2.js new file mode 100644 index 0000000..2f11357 --- /dev/null +++ b/scripts/seed-settings 2.js @@ -0,0 +1,31 @@ +/** + * Writes the reporter settings into autotrader.sqlite before first launch. + * + * lib/db.ts seeds these keys with INSERT OR IGNORE, so values written here + * survive the app's own startup seeding. Must be run from the project root — + * lib/db.ts opens the database at process.cwd(). + * + * node scripts/seed-settings.js + */ +const path = require('path'); +const Database = require('better-sqlite3'); + +const [, , url, name] = process.argv; + +const db = new Database(path.join(process.cwd(), 'autotrader.sqlite')); +db.exec(` + CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ); +`); + +const upsert = db.prepare( + 'INSERT INTO settings (key, value) VALUES (?, ?) ' + + 'ON CONFLICT(key) DO UPDATE SET value = excluded.value' +); + +if (url) { upsert.run('master_dashboard_url', url); console.log(' master_dashboard_url = ' + url); } +if (name) { upsert.run('instance_name', name); console.log(' instance_name = ' + name); } + +db.close(); diff --git a/scripts/update-check.mjs b/scripts/update-check.mjs index f5ae9de..641a863 100644 --- a/scripts/update-check.mjs +++ b/scripts/update-check.mjs @@ -20,6 +20,7 @@ import { ensurePythonDeps } from './ensure-python-deps.mjs'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const LOG_FILE = path.join(ROOT, 'scripts', 'update.log'); const LOCK_FILE = path.join(ROOT, 'scripts', '.update.lock'); +const LAST_CHECK_FILE = path.join(ROOT, 'scripts', '.last-check'); const BRANCH = process.env.AUTOFIRMER_BRANCH ?? 'master'; const PORT = process.env.AUTOFIRMER_PORT ?? '3000'; const BASE = `http://127.0.0.1:${PORT}`; @@ -50,6 +51,21 @@ function log(msg) { try { appendFileSync(LOG_FILE, line + '\n'); } catch { /* logging must never throw */ } } +/** + * Record that a check happened, whether or not it found anything. + * + * Overwritten rather than appended, so it stays one line and needs no trimming. + * Without it there is no way to tell "running, nothing to pull" from "not + * running at all": the no-change path logs nothing by design, and PM2 reports a + * cron-restart process as `stopped` with a restart count of 0 even while it is + * firing on schedule. + */ +function recordCheck(status) { + try { + writeFileSync(LAST_CHECK_FILE, `${new Date().toISOString()} ${status}\n`); + } catch { /* never let bookkeeping break an update */ } +} + function run(cmd, args, { capture = false } = {}) { return spawnSync(cmd, args, { cwd: ROOT, @@ -88,12 +104,17 @@ async function main() { return 0; } + let outcome = 'interrupted'; + try { git('fetch', 'origin', BRANCH); const local = git('rev-parse', 'HEAD'); const remote = git('rev-parse', `origin/${BRANCH}`); - if (local === remote) return 0; // the common path: silent no-op + if (local === remote) { + outcome = `up to date at ${local.slice(0, 8)}`; + return 0; // silent in the log by design + } log(`update available: ${local.slice(0, 8)} -> ${remote.slice(0, 8)}`); const changed = git('diff', '--name-only', local, remote).split('\n').filter(Boolean); @@ -115,7 +136,7 @@ async function main() { if (changed.includes('package-lock.json')) { log('package-lock.json changed — npm install'); - if (run('npm', ['install']).status !== 0) { log('ABORTED: npm install failed'); return 1; } + if (run('npm', ['install']).status !== 0) { log('ABORTED: npm install failed'); outcome = 'npm install failed'; return 1; } } if (changed.includes('clicker/requirements.txt')) { @@ -128,11 +149,12 @@ async function main() { log('building'); if (run('npm', ['run', 'build']).status !== 0) { log('ABORTED: build failed — the previous build is still serving, nothing was restarted'); + outcome = 'build failed — not deployed'; return 1; } log('restarting autofirmer'); - if (run('pm2', ['restart', 'autofirmer']).status !== 0) { log('pm2 restart autofirmer failed'); return 1; } + if (run('pm2', ['restart', 'autofirmer']).status !== 0) { log('pm2 restart autofirmer failed'); outcome = 'pm2 restart failed'; return 1; } if (changed.some((f) => f.startsWith('clicker/'))) { log('clicker changed — restarting it too'); @@ -141,12 +163,17 @@ async function main() { await warmUp(); log(`updated to ${remote.slice(0, 8)}`); + outcome = `updated to ${remote.slice(0, 8)}`; return 0; } catch (err) { log(`ERROR: ${err.message}`); + outcome = `ERROR: ${err.message}`; return 1; } finally { - if (!DRY_RUN) releaseLock(); + if (!DRY_RUN) { + releaseLock(); + recordCheck(outcome); + } } }