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:
8 files changed
+105
-21
No files matched your search
@@ -51,14 +51,9 @@ for ($m = 0; $m -lt 3; $m++) {
|
||||
$manual += "$src, $dst"
|
||||
}
|
||||
|
||||
# --- 2. Date-batch tags (recent and upcoming working days) ---
|
||||
function Get-Workday([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
|
||||
}
|
||||
$today = Get-Date
|
||||
$dateTags = @(-4, -3, -2, -1, 0, 1) | ForEach-Object { (Get-Workday $today $_).ToString("M/d") }
|
||||
# --- 2. Date-batch tags (recent and upcoming working days; the worker keeps them current) ---
|
||||
$dateTags = Get-DemoDateTags
|
||||
[System.IO.File]::WriteAllText((Join-Path $dataDir "Demo-Dates.json"), (@{ Tags = $dateTags } | ConvertTo-Json), [System.Text.Encoding]::UTF8)
|
||||
|
||||
$tags = [ordered]@{}
|
||||
foreach ($t in $dateTags) { $tags[$t] = [System.Collections.Generic.List[string]]::new() }
|
||||
|
||||
@@ -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-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
|
||||
# and a destination's first 7 chars are a reversible scramble of its source's.
|
||||
|
||||
@@ -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
|
||||
`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.
|
||||
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)
|
||||
|
||||
|
||||
@@ -405,6 +405,7 @@ while ($true) {
|
||||
Update-WorkerState "Preparing..." 0 0 $globalLastDuration "Calculating..."
|
||||
|
||||
$session = Open-ProviderCycle
|
||||
if ($config.IsDemo) { Update-DemoDateTags $dataDir }
|
||||
|
||||
$completedList = @()
|
||||
if (Test-Path $completedPath) {
|
||||
|
||||
+10
-1
@@ -295,6 +295,7 @@
|
||||
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
|
||||
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 => {
|
||||
const n = pdf.internal.getNumberOfPages();
|
||||
const w = pdf.internal.pageSize.getWidth(), h = pdf.internal.pageSize.getHeight();
|
||||
@@ -382,8 +383,16 @@
|
||||
};
|
||||
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', () => {
|
||||
reindex();
|
||||
if (indexReady) reindex();
|
||||
if (firstLoad) {
|
||||
firstLoad = false;
|
||||
const t = titleFromPath();
|
||||
|
||||
+32
-12
@@ -8,7 +8,6 @@
|
||||
<link rel="icon" href="/favicon.png" type="image/png">
|
||||
|
||||
<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">
|
||||
<script src="/vendor/highlight/highlight.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();
|
||||
initMasterTables();
|
||||
|
||||
@@ -1812,9 +1826,9 @@
|
||||
document.getElementById('btn-edit-mode').innerHTML = EDIT_SVG;
|
||||
document.getElementById('btn-save-doc').innerHTML = `${SAVE_SVG} Save`;
|
||||
|
||||
await loadBlacklist();
|
||||
await loadDocsList();
|
||||
await loadAllTags();
|
||||
// Load side by side; the device data doesn't wait for the article list
|
||||
const docsReady = loadDocsList();
|
||||
await Promise.all([loadBlacklist(), loadAllTags()]);
|
||||
|
||||
const savedTech = localStorage.getItem('usmt_tech_id');
|
||||
if (savedTech) {
|
||||
@@ -1822,15 +1836,16 @@
|
||||
await loadTechRoster();
|
||||
}
|
||||
|
||||
startDashboard();
|
||||
|
||||
// Check if we were opened in a new tab specifically to view a document
|
||||
await docsReady;
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const targetDoc = urlParams.get('doc');
|
||||
if (targetDoc && availableDocs.includes(targetDoc)) {
|
||||
await openDoc(targetDoc);
|
||||
}
|
||||
|
||||
startDashboard();
|
||||
};
|
||||
}
|
||||
|
||||
// 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`
|
||||
@@ -2088,7 +2103,7 @@
|
||||
btn.disabled = true;
|
||||
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.innerHTML = btn.innerHTML.replace('Exporting...', 'PDF');
|
||||
});
|
||||
@@ -4307,7 +4322,8 @@
|
||||
|
||||
async function pullData() {
|
||||
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());
|
||||
if (!stateRes.ok) return;
|
||||
const state = await stateRes.json();
|
||||
@@ -4353,7 +4369,7 @@
|
||||
document.getElementById('syncBreakdown').innerHTML = DeviceInfo.syncBreakdownHtml(state);
|
||||
|
||||
// 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 trackData = null;
|
||||
if (trackRes && trackRes.ok) {
|
||||
@@ -4374,6 +4390,7 @@
|
||||
globalTrackedHash = currentTrackedHash;
|
||||
|
||||
// 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);
|
||||
if (dataRes && dataRes.ok) {
|
||||
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) {
|
||||
advancedDataCache = await advRes.json();
|
||||
}
|
||||
@@ -4496,6 +4513,8 @@
|
||||
renderSoftwareTable(); // <-- Forces the Software tab to update instantly!
|
||||
renderCustomTable();
|
||||
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
|
||||
fetch('Software-Cache.json?t=' + Date.now())
|
||||
@@ -4528,6 +4547,7 @@
|
||||
renderSoftwareTable();
|
||||
renderCustomTable();
|
||||
refreshOpenDialogs(true);
|
||||
if (window.Overview) Overview.refresh();
|
||||
}
|
||||
}
|
||||
}).catch(() => null);
|
||||
|
||||
@@ -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 */
|
||||
#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; } }
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
let state = { kind: null, arg: null, map: 'left' };
|
||||
let lastSig = '';
|
||||
let loaded = false; // set once the dashboard's first data load has finished
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// Data
|
||||
@@ -222,9 +223,16 @@
|
||||
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() {
|
||||
const root = document.getElementById('overview-root');
|
||||
if (!root) return;
|
||||
if (!loaded) { renderSkeleton(root); return; }
|
||||
const all = rows();
|
||||
hideTip();
|
||||
|
||||
@@ -663,7 +671,10 @@
|
||||
renderList: () => renderList()
|
||||
};
|
||||
|
||||
document.addEventListener('usmt:data', () => { loaded = true; });
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
maybeRender(true);
|
||||
setTimeout(() => { if (!loaded) { loaded = true; maybeRender(true); } }, 8000); // never stuck on the placeholder
|
||||
const orig = window.switchPage;
|
||||
if (typeof orig === 'function') {
|
||||
window.switchPage = function (id) {
|
||||
|
||||
Reference in new issue
Block a user