Faster first load; demo date tags stay current

First load:
- Draw the Overview as soon as the data arrives instead of on its next
  2-second timer tick (about 2.5 s to 0.6 s on a typical connection).
- Start on DOMContentLoaded instead of window.onload.
- Load the ~900 KB PDF library only when someone exports a PDF.
- Fetch the startup files side by side, and build the docs search index
  after the dashboard has drawn (unless the visit starts on /docs).
- Show a loading placeholder on the Overview until the data arrives.

Demo: the date-batch tags sit on fixed working-day offsets from today.
The seed records them in Demo-Dates.json and the worker relabels them when
the day changes, so a long-running container never looks stale.
This commit is contained in:
Johan Sandoval committed 2026-10-07 20:22:09 +00:00
1 parent 5e8dea8027
commit 9731368687
8 files changed
+105 -21

No files matched your search

+3 -8
View File
@@ -51,14 +51,9 @@ for ($m = 0; $m -lt 3; $m++) {
$manual += "$src, $dst" $manual += "$src, $dst"
} }
# --- 2. Date-batch tags (recent and upcoming working days) --- # --- 2. Date-batch tags (recent and upcoming working days; the worker keeps them current) ---
function Get-Workday([datetime]$from, [int]$offset) { $dateTags = Get-DemoDateTags
$d = $from.Date; $step = [math]::Sign($offset); $left = [math]::Abs($offset) [System.IO.File]::WriteAllText((Join-Path $dataDir "Demo-Dates.json"), (@{ Tags = $dateTags } | ConvertTo-Json), [System.Text.Encoding]::UTF8)
while ($left -gt 0) { $d = $d.AddDays($step); if ($d.DayOfWeek -ne 'Saturday' -and $d.DayOfWeek -ne 'Sunday') { $left-- } }
return $d
}
$today = Get-Date
$dateTags = @(-4, -3, -2, -1, 0, 1) | ForEach-Object { (Get-Workday $today $_).ToString("M/d") }
$tags = [ordered]@{} $tags = [ordered]@{}
foreach ($t in $dateTags) { $tags[$t] = [System.Collections.Generic.List[string]]::new() } foreach ($t in $dateTags) { $tags[$t] = [System.Collections.Generic.List[string]]::new() }
+36
View File
@@ -135,6 +135,42 @@ function Get-DemoPick([string]$name, [string]$salt, [array]$list) { return $list
function Get-DemoInt([string]$name, [string]$salt, [int]$min, [int]$max) { return $min + [int][math]::Floor((Get-DemoRand $name $salt) * ($max - $min + 1)) } function Get-DemoInt([string]$name, [string]$salt, [int]$min, [int]$max) { return $min + [int][math]::Floor((Get-DemoRand $name $salt) * ($max - $min + 1)) }
function Get-DemoNow { return [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() } function Get-DemoNow { return [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() }
# -----------------------------------------------------------------------------------------
# Date-batch tags ("M/d") sit on fixed working-day offsets from today. The seed records the
# labels it used in Demo-Dates.json and the worker moves them along when the day changes, so
# a demo container that runs for weeks still shows this week's deployments.
# -----------------------------------------------------------------------------------------
$script:DemoDateOffsets = @(-4, -3, -2, -1, 0, 1)
function Get-DemoWorkday([datetime]$from, [int]$offset) {
$d = $from.Date; $step = [math]::Sign($offset); $left = [math]::Abs($offset)
while ($left -gt 0) { $d = $d.AddDays($step); if ($d.DayOfWeek -ne 'Saturday' -and $d.DayOfWeek -ne 'Sunday') { $left-- } }
return $d
}
function Get-DemoDateTags { $today = Get-Date; return @($script:DemoDateOffsets | ForEach-Object { (Get-DemoWorkday $today $_).ToString("M/d") }) }
function Update-DemoDateTags([string]$DataDir) {
$metaPath = Join-Path $DataDir "Demo-Dates.json"
$trackedPath = Join-Path $DataDir "Tracked-Devices.json"
if (-not (Test-Path $metaPath) -or -not (Test-Path $trackedPath)) { return }
try { $old = @(([System.IO.File]::ReadAllText($metaPath) | ConvertFrom-Json).Tags) } catch { return }
$new = Get-DemoDateTags
if ($old.Count -ne $new.Count -or ($old -join '|') -eq ($new -join '|')) { return }
$rename = @{}
for ($i = 0; $i -lt $old.Count; $i++) { $rename[$old[$i]] = $new[$i] }
try { $tracked = [System.IO.File]::ReadAllText($trackedPath) | ConvertFrom-Json } catch { return }
$out = [ordered]@{}
foreach ($prop in $tracked.PSObject.Properties) {
$name = if ($rename.ContainsKey($prop.Name)) { $rename[$prop.Name] } else { $prop.Name }
$out[$name] = $prop.Value
}
$tmp = "$trackedPath.tmp"
[System.IO.File]::WriteAllText($tmp, ($out | ConvertTo-Json -Depth 5), [System.Text.Encoding]::UTF8)
Move-Item -LiteralPath $tmp -Destination $trackedPath -Force
[System.IO.File]::WriteAllText($metaPath, (@{ Tags = $new } | ConvertTo-Json), [System.Text.Encoding]::UTF8)
Write-Host "Demo: date tags moved to today ($($old -join ', ') -> $($new -join ', '))" -ForegroundColor DarkCyan
}
# ----------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------
# Serial scheme: 7 chars + check char. Sources and destinations use different check offsets # Serial scheme: 7 chars + check char. Sources and destinations use different check offsets
# and a destination's first 7 chars are a reversible scramble of its source's. # and a destination's first 7 chars are a reversible scramble of its source's.
+2
View File
@@ -169,6 +169,8 @@ Compose*) build pack, set the exposed port to **8080**, and attach a domain. The
reseeds the simulated fleet on every start and needs no volumes or secrets. Times are shown in reseeds the simulated fleet on every start and needs no volumes or secrets. Times are shown in
`TZ` (default `America/Chicago`); set it to your own timezone. The demo worker runs `TZ` (default `America/Chicago`); set it to your own timezone. The demo worker runs
6 AM to 9 PM every day (`Demo.BusinessHours` in the config) and sleeps overnight. 6 AM to 9 PM every day (`Demo.BusinessHours` in the config) and sleeps overnight.
The date tags (such as `10/7`) move along each day, so a container that runs for weeks still
shows this week's deployments.
### Demo (local PowerShell 7) ### Demo (local PowerShell 7)
+1
View File
@@ -405,6 +405,7 @@ while ($true) {
Update-WorkerState "Preparing..." 0 0 $globalLastDuration "Calculating..." Update-WorkerState "Preparing..." 0 0 $globalLastDuration "Calculating..."
$session = Open-ProviderCycle $session = Open-ProviderCycle
if ($config.IsDemo) { Update-DemoDateTags $dataDir }
$completedList = @() $completedList = @()
if (Test-Path $completedPath) { if (Test-Path $completedPath) {
+10 -1
View File
@@ -295,6 +295,7 @@
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }, jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
pagebreak: { mode: ['css', 'legacy'], avoid: ['pre', 'table', 'img', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'li', 'tr'] } pagebreak: { mode: ['css', 'legacy'], avoid: ['pre', 'table', 'img', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'li', 'tr'] }
}; };
await loadHtml2Pdf();
await html2pdf().set(opt).from(page).toPdf().get('pdf').then(pdf => { await html2pdf().set(opt).from(page).toPdf().get('pdf').then(pdf => {
const n = pdf.internal.getNumberOfPages(); const n = pdf.internal.getNumberOfPages();
const w = pdf.internal.pageSize.getWidth(), h = pdf.internal.pageSize.getHeight(); const w = pdf.internal.pageSize.getWidth(), h = pdf.internal.pageSize.getHeight();
@@ -382,8 +383,16 @@
}; };
addToolbarButtons(); addToolbarButtons();
// The search index fetches every article. On a first visit to any other page it waits
// until the device data has drawn, so those requests don't hold the dashboard up.
let indexReady = location.pathname.startsWith('/docs');
const indexNow = () => { if (indexReady) return; indexReady = true; reindex(); };
document.addEventListener('usmt:data', () => setTimeout(indexNow, 300), { once: true });
setTimeout(indexNow, 4000);
wrap('switchPage', id => { if (id === 'docs') indexNow(); });
wrap('loadDocsList', () => { wrap('loadDocsList', () => {
reindex(); if (indexReady) reindex();
if (firstLoad) { if (firstLoad) {
firstLoad = false; firstLoad = false;
const t = titleFromPath(); const t = titleFromPath();
+32 -12
View File
@@ -8,7 +8,6 @@
<link rel="icon" href="/favicon.png" type="image/png"> <link rel="icon" href="/favicon.png" type="image/png">
<script src="/vendor/marked/marked.min.js"></script> <script src="/vendor/marked/marked.min.js"></script>
<script src="/vendor/html2pdf/html2pdf.bundle.min.js"></script>
<link rel="stylesheet" href="/vendor/highlight/styles/atom-one-dark.min.css"> <link rel="stylesheet" href="/vendor/highlight/styles/atom-one-dark.min.css">
<script src="/vendor/highlight/highlight.min.js"></script> <script src="/vendor/highlight/highlight.min.js"></script>
<script src="/vendor/highlight/languages/powershell.min.js"></script> <script src="/vendor/highlight/languages/powershell.min.js"></script>
@@ -1784,7 +1783,22 @@
} }
} }
window.onload = async () => { // PDF export library (~900 KB): fetched the first time someone exports, not on every visit
function loadHtml2Pdf() {
if (window.html2pdf) return Promise.resolve();
return window._html2pdfLoading || (window._html2pdfLoading = new Promise((resolve, reject) => {
const s = document.createElement('script');
s.src = '/vendor/html2pdf/html2pdf.bundle.min.js';
s.onload = resolve;
s.onerror = () => { window._html2pdfLoading = null; reject(new Error('Could not load the PDF library')); };
document.head.appendChild(s);
}));
}
// Start as soon as the page is parsed (not after every image and script has finished, as
// window.onload would); the timeout lets every module's own DOMContentLoaded setup run first
document.addEventListener('DOMContentLoaded', () => setTimeout(initDashboard, 0));
async function initDashboard() {
updateThemeBtn(); updateThemeBtn();
initMasterTables(); initMasterTables();
@@ -1812,9 +1826,9 @@
document.getElementById('btn-edit-mode').innerHTML = EDIT_SVG; document.getElementById('btn-edit-mode').innerHTML = EDIT_SVG;
document.getElementById('btn-save-doc').innerHTML = `${SAVE_SVG} Save`; document.getElementById('btn-save-doc').innerHTML = `${SAVE_SVG} Save`;
await loadBlacklist(); // Load side by side; the device data doesn't wait for the article list
await loadDocsList(); const docsReady = loadDocsList();
await loadAllTags(); await Promise.all([loadBlacklist(), loadAllTags()]);
const savedTech = localStorage.getItem('usmt_tech_id'); const savedTech = localStorage.getItem('usmt_tech_id');
if (savedTech) { if (savedTech) {
@@ -1822,15 +1836,16 @@
await loadTechRoster(); await loadTechRoster();
} }
startDashboard();
// Check if we were opened in a new tab specifically to view a document // Check if we were opened in a new tab specifically to view a document
await docsReady;
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const targetDoc = urlParams.get('doc'); const targetDoc = urlParams.get('doc');
if (targetDoc && availableDocs.includes(targetDoc)) { if (targetDoc && availableDocs.includes(targetDoc)) {
await openDoc(targetDoc); await openDoc(targetDoc);
} }
}
startDashboard();
};
// Code spans and fenced blocks are set aside while the [[Badge:..]] / [[Copy:..]] / UNC // Code spans and fenced blocks are set aside while the [[Badge:..]] / [[Copy:..]] / UNC
// path rewrites run, so an article can show that syntax literally inside `code` // path rewrites run, so an article can show that syntax literally inside `code`
@@ -2088,7 +2103,7 @@
btn.disabled = true; btn.disabled = true;
btn.innerHTML = btn.innerHTML.replace('PDF', 'Exporting...'); btn.innerHTML = btn.innerHTML.replace('PDF', 'Exporting...');
html2pdf().set(opt).from(clone).save().finally(() => { loadHtml2Pdf().then(() => html2pdf().set(opt).from(clone).save()).finally(() => {
btn.disabled = false; btn.disabled = false;
btn.innerHTML = btn.innerHTML.replace('Exporting...', 'PDF'); btn.innerHTML = btn.innerHTML.replace('Exporting...', 'PDF');
}); });
@@ -4307,7 +4322,8 @@
async function pullData() { async function pullData() {
try { try {
// 1. FAST STATE POLLING // 1. FAST STATE POLLING (the tracked list is fetched alongside it)
const trackFetch = fetch('Tracked-Devices.json?t=' + Date.now()).catch(() => null);
const stateRes = await fetch('Worker-State.json?t=' + Date.now()); const stateRes = await fetch('Worker-State.json?t=' + Date.now());
if (!stateRes.ok) return; if (!stateRes.ok) return;
const state = await stateRes.json(); const state = await stateRes.json();
@@ -4353,7 +4369,7 @@
document.getElementById('syncBreakdown').innerHTML = DeviceInfo.syncBreakdownHtml(state); document.getElementById('syncBreakdown').innerHTML = DeviceInfo.syncBreakdownHtml(state);
// Check if Tracked-Devices was updated by you or a teammate // Check if Tracked-Devices was updated by you or a teammate
const trackRes = await fetch('Tracked-Devices.json?t=' + Date.now()).catch(() => null); const trackRes = await trackFetch;
let currentTrackedHash = ""; let currentTrackedHash = "";
let trackData = null; let trackData = null;
if (trackRes && trackRes.ok) { if (trackRes && trackRes.ok) {
@@ -4374,6 +4390,7 @@
globalTrackedHash = currentTrackedHash; globalTrackedHash = currentTrackedHash;
// 2. FETCH CORE DATA AND ADVANCED DATA // 2. FETCH CORE DATA AND ADVANCED DATA
const advFetch = fetch('Advanced-Cache.json?t=' + Date.now()).catch(() => null);
const dataRes = await fetch('Data.json?t=' + Date.now()).catch(() => null); const dataRes = await fetch('Data.json?t=' + Date.now()).catch(() => null);
if (dataRes && dataRes.ok) { if (dataRes && dataRes.ok) {
const payload = await dataRes.json(); const payload = await dataRes.json();
@@ -4387,7 +4404,7 @@
} }
} }
const advRes = await fetch('Advanced-Cache.json?t=' + Date.now()).catch(() => null); const advRes = await advFetch;
if (advRes && advRes.ok) { if (advRes && advRes.ok) {
advancedDataCache = await advRes.json(); advancedDataCache = await advRes.json();
} }
@@ -4496,6 +4513,8 @@
renderSoftwareTable(); // <-- Forces the Software tab to update instantly! renderSoftwareTable(); // <-- Forces the Software tab to update instantly!
renderCustomTable(); renderCustomTable();
refreshOpenDialogs(); refreshOpenDialogs();
document.dispatchEvent(new Event('usmt:data'));
if (window.Overview) Overview.refresh(); // draw the Overview now, not on its next timer tick
// 3. LAZY LOAD SOFTWARE LAST // 3. LAZY LOAD SOFTWARE LAST
fetch('Software-Cache.json?t=' + Date.now()) fetch('Software-Cache.json?t=' + Date.now())
@@ -4528,6 +4547,7 @@
renderSoftwareTable(); renderSoftwareTable();
renderCustomTable(); renderCustomTable();
refreshOpenDialogs(true); refreshOpenDialogs(true);
if (window.Overview) Overview.refresh();
} }
} }
}).catch(() => null); }).catch(() => null);
+10
View File
@@ -164,3 +164,13 @@ table.ov-heat td { padding: 0; text-align: center; border: 0; background: none;
/* Undocumented apps list: room goes to the device names */ /* Undocumented apps list: room goes to the device names */
#ov-table td.ov-src-list { white-space: normal; overflow-wrap: anywhere; } #ov-table td.ov-src-list { white-space: normal; overflow-wrap: anywhere; }
/* Loading placeholder (first fraction of a second, before the data arrives) */
.ov-skel { min-height: 150px; margin-bottom: 18px;
background: linear-gradient(90deg, var(--bg-card) 25%, rgba(127,127,127,0.10) 50%, var(--bg-card) 75%);
background-size: 200% 100%; animation: ov-skel 1.2s linear infinite; }
.ov-skel-hero { min-height: 120px; }
.ov-skel-row { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
@media (max-width: 700px) { .ov-skel-row { grid-template-columns: 1fr; } }
@keyframes ov-skel { from { background-position: 100% 0; } to { background-position: -100% 0; } }
@media (prefers-reduced-motion: reduce) { .ov-skel { animation: none; } }
+11
View File
@@ -30,6 +30,7 @@
let state = { kind: null, arg: null, map: 'left' }; let state = { kind: null, arg: null, map: 'left' };
let lastSig = ''; let lastSig = '';
let loaded = false; // set once the dashboard's first data load has finished
// --------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------
// Data // Data
@@ -222,9 +223,16 @@
return c; return c;
} }
// Shown from the moment the page opens until the first data arrives
function renderSkeleton(root) {
root.innerHTML = '<div class="ov-head"><div><h1 class="ov-title">Overview</h1><p class="ov-subtitle">Loading the fleet...</p></div></div>'
+ '<section class="ov-card ov-skel ov-skel-hero"></section><div class="ov-skel-row"><section class="ov-card ov-skel"></section><section class="ov-card ov-skel"></section></div>';
}
function render() { function render() {
const root = document.getElementById('overview-root'); const root = document.getElementById('overview-root');
if (!root) return; if (!root) return;
if (!loaded) { renderSkeleton(root); return; }
const all = rows(); const all = rows();
hideTip(); hideTip();
@@ -663,7 +671,10 @@
renderList: () => renderList() renderList: () => renderList()
}; };
document.addEventListener('usmt:data', () => { loaded = true; });
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
maybeRender(true);
setTimeout(() => { if (!loaded) { loaded = true; maybeRender(true); } }, 8000); // never stuck on the placeholder
const orig = window.switchPage; const orig = window.switchPage;
if (typeof orig === 'function') { if (typeof orig === 'function') {
window.switchPage = function (id) { window.switchPage = function (id) {