Fix Dashboard blocked by slow API health check (5-min timeout)
Details
Root cause: RefreshAsync populated AllPlugins AFTER awaiting
IsOnlineAsync(), which used PluginInstallService's 5-minute timeout
HttpClient for a simple health check. When privstack.io was slow or
unreachable, the entire dashboard sat on "Loading..." for minutes
with nothing rendered.
Fix: Split RefreshAsync into three phases:
1. PopulateLocalPlugins() — instant, no network. Builds AllPlugins
from filesystem manifests + IPluginRegistry.Plugins (the actually
loaded plugins). Sets IsLoading=false immediately so plugins render.
2. LoadSystemMetricsAsync() — local metrics (disk sizes, memory).
3. TryFetchRemoteRegistryAsync() — non-blocking enrichment with
server data. If it fails, local plugins are already displayed.
Also added a 5-second CancellationToken timeout to IsOnlineAsync()
so the health check doesn't inherit the 5-minute download timeout.
Fix Dashboard showing no plugins when registry is offline
Details
The previous fix handled the API timeout gracefully but the dashboard
still showed 0 plugins because GetInstalledVersions() only scans
~/.privstack/plugins and bundled plugin directories for manifest.json
files. In dev mode (project references) and when plugins are loaded
from non-standard paths, no manifests exist in those directories.
Added a fallback that populates AllPlugins from IPluginRegistry.Plugins
— the actually loaded/running plugins — for any plugin not already
covered by the server registry or filesystem manifests. This ensures
the dashboard always shows all active plugins with their metadata
regardless of how they were loaded.
Fix Dashboard showing empty when plugin registry API times out
Details
The Dashboard RefreshAsync method called AllPlugins.Clear() before
fetching the remote plugin registry, so when the API call timed out
the entire catch block skipped populating locally installed plugins,
system metrics, and workspace activation state — leaving the UI stuck
showing "0 installed · 0 available" with empty metric cards.
Restructured RefreshAsync to:
- Fetch local installed versions first (never fails)
- Wrap the remote registry fetch in its own try/catch so timeouts
are non-fatal — locally installed plugins still appear
- Always load system metrics regardless of registry availability
Also increased PrivStackApiClient timeout from 15s to 30s since the
plugin registry endpoint can be slow.
Remove N+1 individual entity reads from BacklinkService index build
Details
Phase 2 of BuildIndexAsync already calls WikiLinkParser.ExtractContentFromEntity
on each entity from ReadList. The "load individually" block re-fetched entities
without content via individual SdkAction.Read calls, but the individual Read
returns the same JSON shape — ExtractContentFromEntity produces the same empty
result. This caused 21+ unnecessary contact.Read SDK calls at startup.
Removing the block eliminates the N+1 query pattern with no behavioral change,
since entities that genuinely lack parseable text fields won't gain content from
a re-fetch.
Pre-build backlink index at startup
Details
Adds BacklinkService.PreBuildIndexAsync() and calls it during the
deferred background services block in App.axaml.cs. This eliminates
the ~700ms cold-start delay when the user first clicks an item with
the Info Panel open, since the cross-plugin backlink index is already
warm by the time they interact.
The existing SemaphoreSlim guard in EnsureIndexBuiltAsync ensures
concurrent calls (from a user click racing the startup pre-build)
are safe. If the pre-build fails, the index falls back to lazy
initialization on first query (existing behavior).
Get notified about new releases