Skip to content

feat: Auto-update checker with changelog notification#794

Open
vivekchand wants to merge 1 commit intomainfrom
feat/gh-clawmetry-766-auto-update-checker
Open

feat: Auto-update checker with changelog notification#794
vivekchand wants to merge 1 commit intomainfrom
feat/gh-clawmetry-766-auto-update-checker

Conversation

@vivekchand
Copy link
Copy Markdown
Owner

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

  • 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

Testing

  • 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

@vivekchand vivekchand force-pushed the feat/gh-clawmetry-766-auto-update-checker branch from fe5f58b to ea9eeaf Compare May 1, 2026 07:04
Copy link
Copy Markdown
Owner Author

@vivekchand vivekchand left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test plan & review notes

What changed

  • Adds a background thread (routes/update_check.py) that polls PyPI for a newer clawmetry package 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 test or make test-api
  • python3 dashboard.py --port 8900
  • With network blocked: verify update check failure doesn't crash the dashboard (the _check_for_update function returns None on exception, but api_update_check_now returns a 500 — confirm the banner and periodic worker handle that silently)

Likely failure modes from the diff

  • Timeout stall: urllib.request.urlopen uses 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() in dashboard.py returns a shared connection instead of a fresh one, concurrent access could raise ProgrammingError: 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/status first 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 >= 9 against 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 to datetime.now() (local) vs datetime.now(timezone.utc).
  • Privacy: only the package name and current version string are sent to pypi.org via the User-Agent header (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

Copy link
Copy Markdown
Owner Author

@vivekchand vivekchand left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test plan & review notes

What changed

  • New routes/update_check.py Blueprint (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 registers bp_update_check, starts the thread in _run_server, injects checkUpdateStatus / dismissUpdateBanner JS and a setInterval + setTimeout poller
  • clawmetry/templates/partials/banners.html: prepends the green update-available banner <div> (hidden by default)

Smoke commands

  • make test-api
  • python3 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__ in dashboard.py to 0.0.1, start the server, hit check-now, then reload the dashboard — the green banner should appear within 5 seconds (the setTimeout fires at 5s)
  • To test dismiss: click the Dismiss button, reload — banner should stay hidden; verify via curl http://localhost:8900/api/update-check/status that dismissed_version equals the latest version
  • To test re-notification after a new release: POST {"dismissed_version": ""} to /api/update-check/config to 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_db is called inside start_update_check_thread, but _get_update_check_config and _get_latest_update_check may be called by API endpoints before the thread starts (e.g. on a cold /api/update-check/status request); 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 in detect_config alongside the blueprint registration
  • os is imported in update_check.py but never used — minor lint issue
  • The dismissUpdateBanner JS function makes an extra round-trip to /api/update-check/status just to get the version; the version is already in the DOM message string or could be stored in a data- attribute — minor, but double-fetch on dismiss is unnecessary
  • Banner div uses style="display:none" but later sets display:flex via JS — if JS fails or is slow, the align-items:center; gap:10px flex properties are wasted on the hidden div; low impact, cosmetic

Likely failure modes

  • Network-isolated environments: _check_for_update calls urllib.request.urlopen to pypi.org with 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 like 0.13.0a1 or 0.13.0.post1 this raises ValueError and falls back to update_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_config and _record_update_check will raise inside the with lock block; those callers don't have a try/except wrapper 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

Copy link
Copy Markdown
Owner Author

@vivekchand vivekchand left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 clean
  • curl -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 fetch
  • curl -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.RequestException wraps the PyPI call
  • SQLite path conflict: confirm the update_check.db location doesn't collide with history.py's database
  • Blueprint registration: routes/update_check.py must be imported and app.register_blueprint() called in dashboard.py

Issue link


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

@vivekchand vivekchand left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test plan & review notes

Repo: vivekchand/clawmetry

Note: This PR's merge state is DIRTY — a rebase onto main is 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.html gains an update-available banner; dashboard.py wires in the blueprint and starts the thread on server start.

Smoke commands

  • make test
  • python3 dashboard.py --port 8900 — wait ~60 s for the startup check, then hit /api/update-check/status and verify show_banner is false when already on the latest version and true when behind
  • curl -s -X POST http://localhost:8900/api/update-check/check-now | python3 -m json.tool — trigger an immediate PyPI fetch and confirm a valid result.latest is returned
  • Verify offline behavior: unshare -n python3 dashboard.py --port 8900 (network-isolated) — server must start cleanly and /api/update-check/status must still return 200 (no crash when PyPI is unreachable)
  • Dismiss flow: call /api/update-check/dismiss with {"version": "<latest>"}, then confirm /api/update-check/status returns show_banner: false for 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.md on GitHub in a new tab
  • "Dismiss" button hides the banner immediately; refreshing the page (or waiting for the hourly setInterval poll) must not resurrect it for the same version

Likely failure modes from the diff

  1. PyPI fetch timeout in the main thread_check_for_update uses urllib.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 (the stop_event.wait(3600) will wake correctly, but an in-flight urlopen will not).

  2. db.close() called before db.commit() in error paths_init_update_check_db calls db.executescript(...) then db.close() without an explicit commit(). executescript issues an implicit commit in Python's sqlite3, so this is safe today, but _set_update_check_config calls db.commit() then db.close() inside with _get_fleet_db_lock() — if db.commit() raises, db.close() is never reached and the connection leaks. A try/finally around db.close() (or using the connection as a context manager) would harden this.

  3. Version comparison with pre-release/build metadata[int(x) for x in version.split(".")] will raise ValueError for versions like 0.13.0rc1 or 0.13.0+local, silently falling back to update_available = True. This could produce false-positive banners if PyPI ever publishes a pre-release. Consider packaging.version.Version (already available transitively via pip) or at minimum strip non-numeric suffixes before splitting.

  4. Thread safety of _update_check_thread globalstart_update_check_thread reads and writes _update_check_thread without a lock. Calling it from multiple threads (e.g. a future reload path) could start duplicate threads. A module-level threading.Lock around the alive-check + assignment would be the safe fix.

  5. dismissUpdateBanner() in JS fetches /status twice — 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 in checkUpdateStatus).

  6. _get_fleet_db_lock is not a reentrant lock — if _check_for_update is 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.

  7. 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
@vivekchand vivekchand force-pushed the feat/gh-clawmetry-766-auto-update-checker branch from ea9eeaf to f586c2a Compare May 7, 2026 07:05
Copy link
Copy Markdown
Owner Author

Test plan & review notes

Repo: vivekchand/clawmetry

What changed

  • New routes/update_check.py: SQLite-backed background thread that polls PyPI on startup + daily at 09:00; 5 new API endpoints; dismissible update banner in banners.html

Smoke commands

  • make test
  • python3 dashboard.py --port 8900
  • curl -sS http://localhost:8900/api/update-check/status → expect JSON with current_version, latest_version, update_available
  • curl -X POST http://localhost:8900/api/update-check/check-now → manual trigger

What to look at visually

  • http://localhost:8900 — if PyPI has a newer version, the update banner should appear
  • Click "Dismiss" → hard-refresh → banner should stay hidden for that version

Likely failure modes from the diff

  • Background thread lifetime: ensure the thread is daemonized so waitress shutdown doesn't hang if a PyPI request is in-flight
  • SQLite from a background thread + Flask from another — confirm check_same_thread=False (or per-thread connections) is set; otherwise expect ProgrammingError: SQLite objects created in a thread can only be used in that same thread
  • Air-gapped installs will block on startup if the PyPI request has no timeout — add one

Issue link


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant