Add a navbar icon for plugin updates - #2929
focusedonsound wants to merge 6 commits into
Conversation
Mirrors the existing #navbarUpdateAvail FPP-update icon: a puzzle-piece icon that shows only when at least one installed plugin has an update, and links to plugins.php?tab=updates. Backend: a new GET /api/plugin/updatesAvailable endpoint aggregates PluginHasUpdates() across every installed plugin. That check itself is cheap (git log against already-fetched remote-tracking refs, no network) but only as fresh as the last `git fetch` per plugin, so the endpoint keeps a small TTL cache (same convention as the existing GitHub-stats cache) and refreshes at most one stale plugin's remote per call -- enough to stay fresh without a page load ever paying for N serial git fetches. Frontend: plugins.php didn't have a URL deep-link into its Updates tab (only a sessionStorage-remembered "last tab"), so ?tab=updates is added to RestoreTopTab() as an explicit override that also persists, consistent with how a manual tab click already behaves. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NjuzBZvDeLNNaGX7j9mC8p
Resolves two conflicts against upstream master, both additive (no logic conflict, just two unrelated changes landing at the same line): - www/js/fpp.js: kept both the new _fppUpdateCheckInFlight/ FPP_UPDATE_CHECK_RETRY_MS dedup state from master and this branch's own FPP_PLUGIN_UPDATE_STATE. - www/menu.inc: kept master's newer checkForFppUpdate() call (now guarded behind FPP_UPDATE_STATE.answered to avoid double-checking when about.php already answered it) and this branch's staggered checkForPluginUpdates() call right after it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NjuzBZvDeLNNaGX7j9mC8p
|
@focusedonsound Claude suggests a bunch of things need to be considered before this could be merged: Feedback for the PR author: Blocking
Should fix
Minor
|
- Invalidate the shared pluginUpdates cache on the three events that actually change a plugin's state: UpgradePlugin() (clears the flag instead of leaving it lit for up to 6h), UninstallPlugin() (drops the entry so a reinstall under the same name starts clean), and InstallPluginFromInfo() (defensive drop of any stale entry a fresh install might inherit). - CheckForPluginUpdates() (the Updates tab's own on-demand check) now writes its result back into the same cache, so the navbar icon and the tab can't disagree. - Add the same non-blocking-lock + serve-stale pattern pluginHeaders.php already uses, plus write-then-rename cache writes, so concurrent page loads at a TTL boundary don't each spawn a git fetch of the same plugin and can't observe a torn cache file. - Gate the git fetch behind the same 1s connectivity probe get_remote_git_version() uses (common.php), and wrap it in `timeout 20` -- an offline box no longer holds a php-fpm worker on a hung fetch. A failed fetch now retries after 5 minutes instead of the full 6h TTL. - Correct the docblock: PluginHasUpdates() also runs the plugin's own scripts/fpp_update_check.sh, which may do its own network I/O -- it isn't strictly "no network". Moved the implementation-rationale comment out of the /** */ docblock (it was leaking verbatim, including an internal cross-reference, into the public openapi.json) and regenerated openapi.json against current master. - menu.inc: don't schedule the poll at all on a box with zero plugins installed. No help/plugins.php exists yet for this page (pre-existing gap, not introduced by this change) -- per .claude/HELP-PAGES.md's own carve-out, noting that here rather than authoring a whole new help page. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AE12rwWGhsJsyfENBXpBs5
Resolves conflicts against master's own recent plugin-privacy work (PluginReinstallPendingSync, MarkPluginPrivacyUninstalled, PluginRecordUpgradedPrivacy) by keeping both sides: this branch's pluginUpdates-cache invalidation alongside master's newer privacy-tracking calls at the same install/upgrade/uninstall success points. Regenerated openapi.json against the merged state. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AE12rwWGhsJsyfENBXpBs5
|
Thanks for running this through review - went through every item against the actual code before changing anything. Pushed fixes for what held up: Fixed:
Pushed back on:
Ready for another look. |
|
Verified the round-1 fixes — they hold up, and the regenerated openapi.json matches. Two changes needed before merge, then some small stuff. 1. Switch the cache to The current design keeps a per-plugin entry that five call sites (install, uninstall, upgrade ×2, updates-tab check) update under a lock. That's a lot of moving parts for a boolean, and two bugs in the revised code come straight from it:
Both disappear if the cache is one 2. Use Small, take or leave:
To merge: 1 and 2. Nothing under "Small" is blocking. |
Per Daryl's second review on PR FalconChristmas#2929: 1. Replace the bespoke per-plugin cache (its own lockfile, five call sites doing read-modify-write) with a single {updatesAvailable} flag per box via file_cache() - the same TTL-cache helper GetPluginList() already uses in this file. Mutation sites (install/uninstall/upgrade/on-demand check) just @Unlink() the cache file instead of updating an entry, so there's nothing to keep in sync out of band and nothing to lock against. - Fixes: icon stayed lit after a partial upgrade (rc=2 - code pulled, only the post-script failed) because UpgradePlugin() only cleared the entry on rc 0. Both call sites now clear on rc != 1 (0 or 2 both mean the code itself is current). - Fixes: writers blocking behind the poll's own git-fetch, since mutation sites no longer take any lock at all. 2. Use InstalledPluginNames() instead of duplicating the installed-plugin directory scan. Small items from the same review: - git fetch now built the same way PluginFetchBranch() does (cd X && $SUDO timeout 20 git fetch), not a bash -c wrapper. - menu.inc: (bool) glob(...) instead of foreach-ing a result that can be false on a read error (PHP 8 warning). - RunUpgradePlugin() and UpdateAllFinish() both now re-poll the navbar icon immediately when they finish, instead of leaving it showing pre-upgrade state until the next full page load. Left alone (per round 1's already-given reasoning, which stood): the help-page item - www/help/plugins.php documents page-level controls, not global header chrome, matching the existing navbarUpdateAvail icon this one is modeled after. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
# Conflicts: # www/api/controllers/plugin.php
|
Thanks for the second pass — both blocking items done, plus the small stuff. 1. Cache switched to
One addition beyond what was asked: since recompute now only happens once per 6h window box-wide (not once per page load), I kept the one-fetch-per-recompute cap but rotate which plugin gets it across successive TTL windows ( 2. Small items:
Also rebased onto current master (was 70-some commits behind again) and force-pushed nothing — this was a merge, and Ready for another look. |
Inspired by the work focusedonsound did on PR#2929 Header, every page: - A puzzle-piece icon appears when an installed plugin has an update waiting. Hovering names the plugins, says how long ago they were checked and how many could not be; clicking opens the Plugins page on its Updates tab. It is hidden when nothing is known, never shown as a guess.
|
@focusedonsound As discussed, I've added this feature outside of your PR. This turned out to be incredibly complex with a lot of moving parts to ensure consistency and safety. Thanks for working on this. |

Summary
#navbarUpdateAvail's existing show/hide behavior and styling.plugins.php?tab=updates).plugins.phppreviously had no URL deep-link into that tab (only asessionStorage-remembered "last tab" from a manual click), so this adds explicit?tab=support toRestoreTopTab()that takes priority over — and then persists into — that same session memory.GET /api/plugin/updatesAvailableendpoint aggregates the existingPluginHasUpdates()check across every installed plugin. That check itself is cheap (localgit logagainst already-fetched remote-tracking refs, no network), but is only as fresh as the lastgit fetchper plugin. This endpoint keeps those refs from going stale on their own via a small TTL cache (same convention/location pattern as the existing GitHub-stats cache), refreshing at most one stale plugin's remote per call so a page load never pays for N serialgit fetches.Test plan
{"updatesAvailable": bool}, existing/api/pluginand/api/plugin/:RepoNameroutes unaffected by the new fixed-path route.hasUpdate: truecorrectly flips the aggregateupdatesAvailabletotrue.plugins.php?tab=updatesreliably opens the Updates tab regardless of prior session tab state.FPP_PLUGIN_UPDATE_STATE, correct tooltip, and click-through to the Updates tab.