feat: Auto-update checker with changelog notification#794
feat: Auto-update checker with changelog notification#794vivekchand wants to merge 1 commit intomainfrom
Conversation
fe5f58b to
ea9eeaf
Compare
vivekchand
left a comment
There was a problem hiding this comment.
Test plan & review notes
What changed
- Adds a background thread (
routes/update_check.py) that polls PyPI for a newerclawmetrypackage version, stores results in the fleet SQLite DB, and shows a dismissible banner in the dashboard UI when an update is available.
Smoke commands
make testormake test-apipython3 dashboard.py --port 8900- With network blocked: verify update check failure doesn't crash the dashboard (the
_check_for_updatefunction returnsNoneon exception, butapi_update_check_nowreturns a 500 — confirm the banner and periodic worker handle that silently)
Likely failure modes from the diff
- Timeout stall:
urllib.request.urlopenuses a 10 s timeout on the PyPI call; if the network hangs exactly at the TCP layer (no RST, no response), the daemon thread can block for up to 10 s every hour — acceptable, but worth confirming under a firewall that drops packets rather than rejecting them. - DB lock contention:
_get_fleet_db()opens a new SQLite connection on every call and closes it inside the lock; if_fleet_db()indashboard.pyreturns a shared connection instead of a fresh one, concurrent access could raiseProgrammingError: Cannot operate on a closed database. Worth auditing what_d._fleet_db()actually returns. dismissUpdateBanner()re-fetches status: the dismiss JS function calls/api/update-check/statusfirst to get the version, then calls/api/update-check/dismiss. If the first fetch fails (e.g. server briefly busy) the dismiss POST is never sent, so the banner silently stays. Consider reading the version from the DOM instead.- Daily check timing: the worker checks
now.hour >= 9against UTC but the comment says "9 AM local time" — these will diverge for users in non-UTC timezones. Low severity, but worth a note or switching explicitly todatetime.now()(local) vsdatetime.now(timezone.utc). - Privacy: only the package name and current version string are sent to
pypi.orgvia theUser-Agentheader (clawmetry/<version>). No workspace paths, agent data, or user identifiers are transmitted. Looks clean.
Issue link
- Closes #766 — confirmed in the PR body.
Generated by Claude Code
vivekchand
left a comment
There was a problem hiding this comment.
Test plan & review notes
What changed
- New
routes/update_check.pyBlueprint (342 lines): SQLite-backed config + history tables in the fleet DB, background daemon thread that polls PyPI on startup (after 60s) and daily at 09:00 UTC, and five REST endpoints (/api/update-check/{config,status,check-now,dismiss,history}) dashboard.py: imports and registersbp_update_check, starts the thread in_run_server, injectscheckUpdateStatus/dismissUpdateBannerJS and asetInterval+setTimeoutpollerclawmetry/templates/partials/banners.html: prepends the green update-available banner<div>(hidden by default)
Smoke commands
make test-apipython3 dashboard.py --port 8900- To trigger the update check immediately without waiting 60s:
curl -s -X POST http://localhost:8900/api/update-check/check-now | python3 -m json.tool - To force the banner to appear (simulate an older running version): temporarily set
__version__indashboard.pyto0.0.1, start the server, hitcheck-now, then reload the dashboard — the green banner should appear within 5 seconds (thesetTimeoutfires at 5s) - To test dismiss: click the Dismiss button, reload — banner should stay hidden; verify via
curl http://localhost:8900/api/update-check/statusthatdismissed_versionequals the latest version - To test re-notification after a new release: POST
{"dismissed_version": ""}to/api/update-check/configto clear the dismissal
What to look at
- The daily-check logic uses
datetime.now(timezone.utc).hour >= 9— this fires once per UTC day after 09:00 UTC, not local time (the code comment says "local time" but it uses UTC); worth aligning comment and code _init_update_check_dbis called insidestart_update_check_thread, but_get_update_check_configand_get_latest_update_checkmay be called by API endpoints before the thread starts (e.g. on a cold/api/update-check/statusrequest); if the tables don't exist yet those queries will throw and be silently swallowed — confirm the tables are created at blueprint registration time or add a_init_update_check_db()call indetect_configalongside the blueprint registrationosis imported inupdate_check.pybut never used — minor lint issue- The
dismissUpdateBannerJS function makes an extra round-trip to/api/update-check/statusjust to get the version; the version is already in the DOM message string or could be stored in adata-attribute — minor, but double-fetch on dismiss is unnecessary - Banner
divusesstyle="display:none"but later setsdisplay:flexvia JS — if JS fails or is slow, thealign-items:center; gap:10pxflex properties are wasted on the hidden div; low impact, cosmetic
Likely failure modes
- Network-isolated environments:
_check_for_updatecallsurllib.request.urlopentopypi.orgwith a 10s timeout; in air-gapped or firewalled deployments this blocks the background thread for 10s on every check cycle — the thread is daemonized so it won't hang the server, but it will log spurious debug errors repeatedly - Version comparison uses
int(x) for x in version.split(".")— if PyPI returns a pre-release version like0.13.0a1or0.13.0.post1this raisesValueErrorand falls back toupdate_available = True, potentially showing a false-positive banner for non-release builds - If the fleet DB file is read-only or on a read-only filesystem,
_set_update_check_configand_record_update_checkwill raise inside thewithlock block; those callers don't have atry/exceptwrapper so the exception will propagate up and 500 the API response - The
setInterval(checkUpdateStatus, 3600000)runs every hour in the browser tab but the backend PyPI check only runs once per UTC day — the hourly frontend poll is harmless but the status will be stale until the next backend check
Issue link
- Closes #766 (stated in PR body) — confirmed
Generated by Claude Code
vivekchand
left a comment
There was a problem hiding this comment.
Test plan & review notes
Repo: vivekchand/clawmetry
What changed
- New
routes/update_check.py: SQLite-backed config + check history, background thread polling PyPI (startup + daily 9 AM), five/api/update-check/*endpoints - Dashboard notification banner (
banners.html) with changelog link + dismiss button - Frontend
loadUpdateCheck()wired into the overview tab
Smoke commands
python3 -c 'import ast; ast.parse(open("routes/update_check.py").read())'— syntax cleancurl -sS http://localhost:8900/api/update-check/status— expect{"current_version": "...", "latest_version": "...", "update_available": ...}curl -sS -X POST http://localhost:8900/api/update-check/check-now— trigger an immediate PyPI fetchcurl -sS -X POST http://localhost:8900/api/update-check/dismiss— then reload overview; banner should be gone for this version
What to look at visually
http://localhost:8900→ Overview tab — if a newer PyPI version exists, a notification banner should appear at the top with a changelog link and dismiss button
Likely failure modes from the diff
- Background thread on startup: must not block app startup or crash when PyPI is unreachable (air-gapped envs) — verify
try/except requests.RequestExceptionwraps the PyPI call - SQLite path conflict: confirm the
update_check.dblocation doesn't collide withhistory.py's database - Blueprint registration:
routes/update_check.pymust be imported andapp.register_blueprint()called indashboard.py
Issue link
- Closes #766
Generated by Claude Code
vivekchand
left a comment
There was a problem hiding this comment.
Test plan & review notes
Repo: vivekchand/clawmetry
Note: This PR's merge state is DIRTY — a rebase onto
mainis needed before it can merge.
What changed
- New
routes/update_check.py(342 lines) adds a background thread that polls PyPI every hour and on startup, stores results in SQLite, and exposes 5 API endpoints;banners.htmlgains an update-available banner;dashboard.pywires in the blueprint and starts the thread on server start.
Smoke commands
make testpython3 dashboard.py --port 8900— wait ~60 s for the startup check, then hit/api/update-check/statusand verifyshow_bannerisfalsewhen already on the latest version andtruewhen behindcurl -s -X POST http://localhost:8900/api/update-check/check-now | python3 -m json.tool— trigger an immediate PyPI fetch and confirm a validresult.latestis returned- Verify offline behavior:
unshare -n python3 dashboard.py --port 8900(network-isolated) — server must start cleanly and/api/update-check/statusmust still return 200 (no crash when PyPI is unreachable) - Dismiss flow: call
/api/update-check/dismisswith{"version": "<latest>"}, then confirm/api/update-check/statusreturnsshow_banner: falsefor that version, and that it persists across a server restart
What to look at visually
- Green gradient banner at the very top of the dashboard (above the alert banner) — appears only when an update is available and has not been dismissed
- "View Changelog" button opens
CHANGELOG.mdon GitHub in a new tab - "Dismiss" button hides the banner immediately; refreshing the page (or waiting for the hourly
setIntervalpoll) must not resurrect it for the same version
Likely failure modes from the diff
-
PyPI fetch timeout in the main thread —
_check_for_updateusesurllib.request.urlopen(req, timeout=10). If PyPI is slow the background thread blocks for up to 10 s while holding no lock, which is fine — but confirm that a hung connection doesn't prevent the stop-event from being honoured on shutdown (thestop_event.wait(3600)will wake correctly, but an in-flighturlopenwill not). -
db.close()called beforedb.commit()in error paths —_init_update_check_dbcallsdb.executescript(...)thendb.close()without an explicitcommit().executescriptissues an implicit commit in Python'ssqlite3, so this is safe today, but_set_update_check_configcallsdb.commit()thendb.close()insidewith _get_fleet_db_lock()— ifdb.commit()raises,db.close()is never reached and the connection leaks. Atry/finallyarounddb.close()(or using the connection as a context manager) would harden this. -
Version comparison with pre-release/build metadata —
[int(x) for x in version.split(".")]will raiseValueErrorfor versions like0.13.0rc1or0.13.0+local, silently falling back toupdate_available = True. This could produce false-positive banners if PyPI ever publishes a pre-release. Considerpackaging.version.Version(already available transitively via pip) or at minimum strip non-numeric suffixes before splitting. -
Thread safety of
_update_check_threadglobal —start_update_check_threadreads and writes_update_check_threadwithout a lock. Calling it from multiple threads (e.g. a future reload path) could start duplicate threads. A module-levelthreading.Lockaround the alive-check + assignment would be the safe fix. -
dismissUpdateBanner()in JS fetches/statustwice — once to get the version to dismiss, then POSTs to/dismiss. A race between the two calls (or a version flip between them) could dismiss the wrong version. Pass the version from the banner's dataset attribute instead (set it when the banner is first shown incheckUpdateStatus). -
_get_fleet_db_lockis not a reentrant lock — if_check_for_updateis ever called from a code path that already holds_fleet_db_lock, it will deadlock. The current call graph looks safe (worker thread →_check_for_update→_record_update_check), but worth documenting. -
No rate-limit guard on
/api/update-check/check-now— the endpoint is unauthenticated and triggers a live PyPI HTTP call on every POST. A tight loop against it from a browser or script will hammer PyPI. Add a minimum interval (e.g. 60 s) server-side before re-fetching.
Issue link
- Closes #766 (confirmed from PR body)
Generated by Claude Code
Add background update checker that monitors PyPI for new ClawMetry versions
and displays a notification banner in the dashboard when updates are available.
- New routes/update_check.py module with:
- SQLite tables for config and check history
- Background thread for periodic checks (startup + daily at 9 AM)
- API endpoints: /api/update-check/config, /api/update-check/status,
/api/update-check/check-now, /api/update-check/dismiss, /api/update-check/history
- Dashboard banner in banners.html with changelog link and dismiss button
- Frontend JS to poll for update status and show/hide banner
- Configurable via dashboard (enable/disable, startup check, daily check)
- Dismissed versions remembered to avoid nagging
- Banner appears when update is available
- Dismiss button hides banner and remembers choice
- Changelog link opens CHANGELOG.md on GitHub
- Manual check via /api/update-check/check-now
- Configuration persisted in fleet database
ea9eeaf to
f586c2a
Compare
Test plan & review notesRepo: vivekchand/clawmetry What changed
Smoke commands
What to look at visually
Likely failure modes from the diff
Issue link Generated by Claude Code |
Closes #766
What
Add background update checker that monitors PyPI for new ClawMetry versions and displays a notification banner in the dashboard when updates are available.
How
routes/update_check.pymodule with:/api/update-check/config,/api/update-check/status,/api/update-check/check-now,/api/update-check/dismiss,/api/update-check/historybanners.htmlwith changelog link and dismiss buttonTesting
/api/update-check/check-now