Tailscale, Patchmon, and Semaphore sync jobs all wrote into a shared
job_runs table with no retention. With default poll intervals of 15-60
minutes, history grew unbounded.
- Add pruneJobRuns(jobId) and pruneAllJobRuns() helpers.
- Prune after every completeJobRun() so new runs trim old ones.
- Prune once on init() to clean up existing over-cap rows.
- Prune in importJobs() so re-imported runs are also capped.
- Defensive LIMIT 10 in getJobRuns() for the read path.
No UI changes needed — _renderRunList already renders whatever the
server returns. No schema migration — only row deletions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Patchmon's API uses standard RFC 7617 Basic auth — `Basic base64(token_key:token_secret)`. The handler was sending the api_token field verbatim, so it only worked if the user had manually base64-encoded the credential. After a Patchmon upgrade, the sync started returning HTML (the SPA, served when auth is rejected) and failing with "Unexpected token '<'" on JSON.parse.
Now: if the token contains ':' (raw key:secret), encode it server-side; otherwise pass through unchanged for backward compatibility. UI gets a placeholder hint showing the expected format.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Merge the request body onto the existing instance row before validating,
so external callers (n8n, scripts) can send only the fields they want to
change instead of GET-then-splat-then-PUT the full record.
Mirrors the partial-update pattern already used by PUT /api/jobs/:id.
Full-body PUTs (today's frontend) are unaffected.
- GET /api/instances now accepts ?sort= (name|vmid|created_at|updated_at)
and ?order= (asc|desc); invalid sort fields fall back to name asc
- Timestamp sorts use id as a tiebreaker (datetime() precision is 1 s)
- Toolbar gains a sort-field <select> and a ↑/↓ direction toggle button
- toggleSortDir() flips direction and re-fetches; state held in data-dir attr
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests cover:
- sort by vmid asc/desc
- sort by name desc
- sort by created_at asc/desc (id tiebreaker for same-second inserts)
- sort by updated_at asc/desc (id tiebreaker for same-second inserts)
- invalid sort field falls back to name asc
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Clicking deployed/testing/degraded sets the state filter to that
value. Clicking total clears all filters. Hover style added.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
maskJob parses job.config before returning it, so calling JSON.parse
on it again threw an exception. The catch returned false for every
job, so relevant was always empty and _waitForOnCreateJobs returned
immediately without polling.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous version snapshotted last_run_id after the 201 response,
but jobs fire immediately server-side — by the time the client fetched
/api/jobs the runs were already complete, so the baseline matched the
new state and the poll loop never detected completion.
Baseline is now captured before the creation POST so it always
reflects pre-run state regardless of job speed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After creating an instance, if any jobs have run_on_create enabled,
the client polls /api/jobs every 500ms until each relevant job has a
new completed run (tracked via last_run_id baseline). The dashboard
or detail page then refreshes automatically. 30s timeout as a safety
net if a job hangs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
datetime('now') only stores to the second, making same-second events
indistinguishable. Switched all instance_history and job_runs writes
to strftime('%Y-%m-%dT%H:%M:%f', 'now') for millisecond precision.
Reverted getInstanceHistory to ORDER BY changed_at DESC, id DESC so
newest events appear at the top and instance creation (lowest id,
earliest timestamp) is always at the bottom.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the dot only updated when visiting the Jobs page.
Now a jobs fetch runs at bootstrap so the dot reflects status
immediately on any page, including after a hard refresh.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
runJobsOnCreate now awaits each job before starting the next,
ensuring they don't stomp each other's DB writes in parallel.
getInstanceHistory changed to ORDER BY changed_at ASC, id ASC so
the creation event (lowest id) is always first regardless of
same-second timestamps.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Jobs with run_on_create=true in their config fire automatically
after a new instance is created. Runs fire-and-forget so they don't
delay the 201 response. Option exposed as a checkbox in each job's
detail panel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Export bumped to version 3, now includes jobs (with raw unmasked
config) and job_runs arrays. Import restores them when present and
restarts the scheduler. Payloads without a jobs key leave jobs
untouched, keeping v1/v2 backups fully compatible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fetches Semaphore project inventory via Bearer auth, parses the
Ansible INI format to extract hostnames, and sets semaphore=1/0
on matching instances.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>