Merge wilm: gevent migration, endpoint telemetry phase 2, backup/restore

Brings in 98 commits from wilm (origin/wilm @ 811e6ca61a):

Major features:
- Move API to gevent workers for better concurrency
- Endpoint agent startup jitter (prevents thundering herd)
- Complete backup/restore with restic
- Endpoint telemetry phase 2 (disk health, mapped drives, user context)
- PC Detail UI rework with new cards and tabs
- Dell Command actions and Corporate Tools tab
- Agent version column in Fleet view

Conflict resolution:
- Backup/restore: Accepted wilm implementation (10 files)
- Agent startup jitter: Accepted wilm v1.12.0
- PC Types list fix: Accepted wilm version
- Endpoint plugin domain clamping: Kept ours (defensive)
- Test files: Accepted wilm versions for consistency

Testing required:
- Verify gevent handles concurrent requests
- Test backup/restore functionality
- Verify agent startup jitter timing
- Test endpoint telemetry collection
- Validate PC Detail UI changes

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Proudlock
2026-09-06 21:23:42 -04:00
32 changed files with 986 additions and 710 deletions

View File

@@ -13,10 +13,7 @@ import { fileURLToPath } from 'node:url'
// constant in the .vue file with no export to assert against.
function read(relative) {
// .href, not the URL object: the jsdom test environment supplies its own
// URL global, and node's fileURLToPath rejects a foreign instance with
// "must be ... an instance of URL. Received an instance of URL".
return readFileSync(fileURLToPath(new URL(relative, import.meta.url).href), 'utf8')
return readFileSync(fileURLToPath(new URL(relative, import.meta.url)), 'utf8')
}
function renderedGroups() {

View File

@@ -13,10 +13,7 @@ import { fileURLToPath } from 'node:url'
// than a layout bug.
function read(relative) {
// .href, not the URL object: the jsdom test environment supplies its own
// URL global, and node's fileURLToPath rejects a foreign instance with
// "must be ... an instance of URL. Received an instance of URL".
return readFileSync(fileURLToPath(new URL(relative, import.meta.url).href), 'utf8')
return readFileSync(fileURLToPath(new URL(relative, import.meta.url)), 'utf8')
}
const panelsSource = () => read('./PluginAssetPanels.vue')

View File

@@ -4,18 +4,9 @@
<h2>Backups</h2>
</div>
<div v-if="!isConfigured" class="not-configured">
<strong>Site Backups is not configured on this deployment.</strong>
<span>
The feature needs the backup overlay (a separate container writing sets
onto a shared volume) and a readable backups root. A Windows/IIS install
does not run it, so nothing here would have a store behind it.
</span>
</div>
<!-- Confidence banner: the single most reassuring thing an admin wants to
see - is there a recent, verified backup? Everything else is secondary. -->
<div v-if="isConfigured" class="confidence-banner" :class="bannerClass">
<div class="confidence-banner" :class="bannerClass">
<span class="banner-icon">
<component :is="bannerIcon" :size="22" />
</span>
@@ -311,9 +302,6 @@ const sets = ref([])
const loading = ref(true)
const isBusy = ref(false)
const lastGoodAt = ref(null)
// Assume configured until the list says otherwise, so a slow first load
// does not flash the not-configured notice on a working install.
const isConfigured = ref(true)
const restoreTarget = ref(null)
// Live restore progress. restoreStatus mirrors the api /status.restore payload;
@@ -417,10 +405,6 @@ async function load() {
const response = await api.get('/shopdbbackups')
sets.value = response.data.data || []
lastGoodAt.value = response.data.meta?.lastgoodat || null
// A deployment without the backup overlay has no store behind any of
// this. Say so instead of showing 'No verified backup yet', which reads
// as a backup that failed rather than a feature that is not installed.
isConfigured.value = response.data.meta?.configured !== false
} catch (error) {
toast.error('Could not load backups.')
} finally {
@@ -663,17 +647,6 @@ onUnmounted(() => {
.banner-none { border-color: #c53030; }
.banner-none .banner-icon { color: #e53e3e; }
.not-configured {
display: flex;
flex-direction: column;
gap: 0.35rem;
padding: 1rem 1.25rem;
margin-bottom: 1rem;
border: 1px solid var(--border-color, #d8dbe0);
border-radius: 8px;
background: var(--surface-muted, #f6f7f9);
}
.explain {
margin-bottom: 1.25rem;
border: 1px solid var(--border);