Demo: migrations follow the schedule, worked by a simulated team
Each device now migrates on the day of its date tag instead of on a looping 24-hour clock: earlier days are finished, today's devices go through backup and restore during working hours, later days haven't started. The few that don't finish have a cause the dashboard shows (broken client, broken WMI, low disk, a USMT error, unreachable PC). A simulated team keeps two weeks of working days tagged. During working hours it adds devices, reschedules or drops one, tags stuck PCs with their problem, then fixes them and moves them to a new day; each morning finished days are closed out and a new day is scheduled. Actions print in the worker terminal as [Team] lines. Settings live in Demo.Team. The visitor sandbox now keeps per-tag edits laid over the live roster, so the team's changes keep showing around a visitor's own edits. Overview calendar: today is a filled badge on the date number, so it no longer looks like the selected day (outline plus tint).
This commit is contained in:
7 files changed
+563
-155
No files matched your search
+59
-46
@@ -253,9 +253,11 @@
|
||||
state: null,
|
||||
empty() {
|
||||
return {
|
||||
version: 1,
|
||||
tracked: null, // visitor's copy of Tracked-Devices.json (null = untouched)
|
||||
appliedTracked: null, // roster the (simulated) worker last swept
|
||||
version: 2,
|
||||
// The visitor's roster edits, per tag, laid over the live roster (which the demo's
|
||||
// simulated team keeps changing): tag -> { devices, skipUSMT, created } or { deleted: true }
|
||||
overrides: {},
|
||||
applied: null, // the overrides the (simulated) worker last swept (null = none yet)
|
||||
rev: 0, // bumps whenever simulated data changes
|
||||
docs: {}, // title -> { content, modified }
|
||||
deletedDocs: [], // server docs the visitor archived
|
||||
@@ -282,15 +284,12 @@
|
||||
const tx = this.db.transaction('kv', 'readonly').objectStore('kv').get('state');
|
||||
tx.onsuccess = () => res(tx.result); tx.onerror = () => res(null);
|
||||
});
|
||||
if (saved && saved.version === 1) this.state = Object.assign(this.empty(), saved);
|
||||
if (saved && saved.version === 2) this.state = Object.assign(this.empty(), saved);
|
||||
// version 1 kept a full copy of the roster: keep its docs and images, drop the copy
|
||||
else if (saved && saved.version === 1) this.state = Object.assign(this.empty(), { docs: saved.docs || {}, deletedDocs: saved.deletedDocs || [],
|
||||
images: saved.images || {}, renamedImages: saved.renamedImages || {}, deletedImages: saved.deletedImages || [] });
|
||||
} catch (e) { this.db = null; } // private browsing etc. - keep changes in memory only
|
||||
|
||||
// The demo container reseeds its fleet on restart; a roster copied from an older
|
||||
// seed would reference tags that no longer exist, so drop it (docs/images are kept).
|
||||
if (this.state.tracked) {
|
||||
const base = await serverGet('/Tracked-Devices.json', true);
|
||||
if (base && hash(base) !== this.state.baseHash) { this.state.tracked = null; this.state.appliedTracked = null; this.save(); }
|
||||
}
|
||||
return this.state;
|
||||
},
|
||||
save() {
|
||||
@@ -303,7 +302,7 @@
|
||||
},
|
||||
hasChanges() {
|
||||
const s = this.state;
|
||||
return !!(s && (s.tracked || Object.keys(s.docs).length || s.deletedDocs.length || Object.keys(s.images).length || Object.keys(s.renamedImages).length || s.deletedImages.length));
|
||||
return !!(s && (Object.keys(s.overrides).length || Object.keys(s.docs).length || s.deletedDocs.length || Object.keys(s.images).length || Object.keys(s.renamedImages).length || s.deletedImages.length));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -355,11 +354,18 @@
|
||||
|
||||
// A device the visitor typed that the demo fleet doesn't know: borrow the live status of
|
||||
// a fleet device with the same prefix, so it keeps moving through the stages like the rest.
|
||||
function pickTemplate(name, rows) {
|
||||
// Prefers a device scheduled on the same day (a date tag such as 10/14), so a PC added to a
|
||||
// future day looks not started and one added to a past day looks done.
|
||||
const DATE_TAG = /^\d{1,2}\/\d{1,2}$/;
|
||||
function pickTemplate(name, rows, tags) {
|
||||
const prefix = name.split('-')[0];
|
||||
const pool = rows.filter(r => r["Source Device"].startsWith(prefix + '-') && r["Destination Device"] !== "N/A"
|
||||
const usable = rows.filter(r => r["Destination Device"] !== "N/A"
|
||||
&& !["Skipped", "N/A"].includes(r["Backup Status"]) && r["USMT Status"] !== "N/A")
|
||||
.sort((a, b) => a["Source Device"].localeCompare(b["Source Device"]));
|
||||
const days = (tags || []).filter(t => DATE_TAG.test(t));
|
||||
const sameDay = days.length ? usable.filter(r => String(r.Tags || '').split(', ').some(t => days.includes(t))) : [];
|
||||
const healthy = sameDay.filter(r => !/error/i.test((r["Source Scanstate Line"] || '') + (r["Destination Loadstate Line"] || '')));
|
||||
const pool = healthy.length ? healthy : sameDay.length ? sameDay : usable.filter(r => r["Source Device"].startsWith(prefix + '-'));
|
||||
return pool.length ? pool[hash(name) % pool.length] : null;
|
||||
}
|
||||
const synthDest = name => name.split('-')[0] + '-' + serial(name + '|dest');
|
||||
@@ -427,7 +433,7 @@
|
||||
|
||||
if (srow && !["Skipped", "N/A"].includes(srow["Backup Status"])) { out.push(Object.assign(clone(srow), { Tags: tags })); continue; }
|
||||
|
||||
const t = SCHEME.test(dev) ? pickTemplate(dev, serverRows) : null;
|
||||
const t = SCHEME.test(dev) ? pickTemplate(dev, serverRows, tagMap[dev]) : null;
|
||||
if (!t) {
|
||||
// Not in the fleet and not a valid name: what the worker reports for a typo
|
||||
const r = blankRow(dev, "N/A", tags, "None");
|
||||
@@ -456,22 +462,26 @@
|
||||
return { data: data || {}, rows, adv: adv || {} };
|
||||
}
|
||||
|
||||
async function visitorTracked() {
|
||||
const s = Store.state;
|
||||
if (s.tracked) return s.tracked;
|
||||
return (await serverGet('/Tracked-Devices.json')) || {};
|
||||
}
|
||||
|
||||
async function ensureOwnRoster() {
|
||||
const s = Store.state;
|
||||
if (!s.tracked) {
|
||||
const text = (await serverGet('/Tracked-Devices.json', true)) || '{}';
|
||||
const base = JSON.parse(text);
|
||||
s.baseHash = hash(text);
|
||||
s.tracked = clone(base);
|
||||
s.appliedTracked = clone(base);
|
||||
// The live roster with the visitor's per-tag edits on top. An edit to a tag the team has
|
||||
// since closed out is dropped; a tag the visitor created stays until they delete it.
|
||||
function mergeRoster(base, overrides) {
|
||||
const out = {};
|
||||
for (const [tag, entry] of Object.entries(base || {})) {
|
||||
const o = overrides[tag];
|
||||
if (!o) out[tag] = entry;
|
||||
else if (!o.deleted) out[tag] = { devices: o.devices, skipUSMT: o.skipUSMT };
|
||||
}
|
||||
return s.tracked;
|
||||
for (const [tag, o] of Object.entries(overrides)) {
|
||||
if (!(tag in (base || {})) && o.created && !o.deleted) out[tag] = { devices: o.devices, skipUSMT: o.skipUSMT };
|
||||
}
|
||||
return out;
|
||||
}
|
||||
async function visitorTracked() {
|
||||
return mergeRoster((await serverGet('/Tracked-Devices.json')) || {}, Store.state.overrides);
|
||||
}
|
||||
async function appliedTracked() {
|
||||
const s = Store.state;
|
||||
return s.applied ? mergeRoster((await serverGet('/Tracked-Devices.json')) || {}, s.applied) : null;
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
@@ -564,7 +574,7 @@
|
||||
try {
|
||||
const s = Store.state;
|
||||
const tracked = await visitorTracked();
|
||||
const applied = s.appliedTracked || tracked;
|
||||
const applied = (await appliedTracked()) || tracked;
|
||||
const snap = await serverSnapshot();
|
||||
if (!snap.rows.length) { // container just started: nothing to sweep against yet
|
||||
w([['[Waiting for the first worker sweep to finish...]', 'DarkGray']]);
|
||||
@@ -610,8 +620,7 @@
|
||||
await sleep(delay);
|
||||
if (fresh.length && i === fresh.length && i < ordered.length) {
|
||||
w([[` -> [PRIORITY] ${fresh.length} new device(s) swept. Publishing early...`, 'Green']]);
|
||||
if (!s.tracked) await ensureOwnRoster();
|
||||
s.appliedTracked = clone(tracked);
|
||||
s.applied = clone(s.overrides);
|
||||
s.rev++;
|
||||
Store.save();
|
||||
}
|
||||
@@ -629,8 +638,7 @@
|
||||
w([[`Next Cycle Scheduled For: ${nextText}`, 'DarkGray']]);
|
||||
w([]); w([]);
|
||||
|
||||
if (!s.tracked) await ensureOwnRoster();
|
||||
s.appliedTracked = clone(tracked);
|
||||
s.applied = clone(s.overrides);
|
||||
s.rev++;
|
||||
Store.save();
|
||||
} finally {
|
||||
@@ -692,7 +700,7 @@
|
||||
// ---- tracking roster ----
|
||||
'GET Tracked-Devices.json': async () => {
|
||||
const s = Store.state;
|
||||
const text = s.tracked ? JSON.stringify(s.tracked, null, 2) : ((await serverGet('/Tracked-Devices.json', true)) || '{}');
|
||||
const text = Object.keys(s.overrides).length ? JSON.stringify(await visitorTracked(), null, 2) : ((await serverGet('/Tracked-Devices.json', true)) || '{}');
|
||||
// Trailing whitespace makes the text differ after a simulated cycle, which is how
|
||||
// the dashboard notices it should reload Data.json.
|
||||
return textResponse(text + ' '.repeat(s.rev), 200, { 'Content-Type': 'application/json' });
|
||||
@@ -709,11 +717,16 @@
|
||||
},
|
||||
'POST update-tracked': async (url, init) => {
|
||||
const body = await readBody(init);
|
||||
const tracked = await ensureOwnRoster();
|
||||
const s = Store.state;
|
||||
const base = (await serverGet('/Tracked-Devices.json')) || {};
|
||||
const tech = body.tech;
|
||||
const skip = Object.prototype.hasOwnProperty.call(body, 'skipUSMT') ? !!body.skipUSMT : false;
|
||||
if (!body.devices || body.devices.length === 0) delete tracked[tech];
|
||||
else tracked[tech] = { devices: body.devices, skipUSMT: skip };
|
||||
const prev = s.overrides[tech];
|
||||
const created = prev && !prev.deleted ? !!prev.created : !(tech in base); // a tag the visitor made
|
||||
if (!body.devices || body.devices.length === 0) {
|
||||
if (tech in base) s.overrides[tech] = { deleted: true };
|
||||
else delete s.overrides[tech];
|
||||
} else s.overrides[tech] = { devices: body.devices, skipUSMT: skip, created };
|
||||
Store.save();
|
||||
srv(`Updated tracking roster for Tech/Tag: ${tech}`, 'Green');
|
||||
scheduleCycle('roster', 2500); // the worker picks the change up on its next sweep
|
||||
@@ -724,7 +737,7 @@
|
||||
'GET Worker-State.json': async () => {
|
||||
const state = (await serverGet('/Worker-State.json')) || {};
|
||||
// the visitor's own (simulated) changes count as new data too
|
||||
if (state.DataVersion && Store.state.appliedTracked) state.DataVersion += '#' + Store.state.rev;
|
||||
if (state.DataVersion && Store.state.applied) state.DataVersion += '#' + Store.state.rev;
|
||||
if (Sim.worker) return jsonResponse(Object.assign({}, state, Sim.worker));
|
||||
return jsonResponse(state);
|
||||
},
|
||||
@@ -733,24 +746,24 @@
|
||||
const snap = await serverSnapshot();
|
||||
// Until the worker's first sweep there are no rows to merge with: pass through so
|
||||
// the dashboard shows its "Waiting for Worker..." placeholders
|
||||
if (!s.appliedTracked || !snap.rows.length) return jsonResponse(snap.data);
|
||||
const { rows } = buildRows(s.appliedTracked, snap.rows, snap.adv);
|
||||
if (!s.applied || !snap.rows.length) return jsonResponse(snap.data);
|
||||
const { rows } = buildRows(await appliedTracked(), snap.rows, snap.adv);
|
||||
return jsonResponse({ LastUpdated: snap.data.LastUpdated || psStamp(), DataVersion: (snap.data.DataVersion || '') + '#' + s.rev, Devices: rows });
|
||||
},
|
||||
'GET Advanced-Cache.json': async () => {
|
||||
const s = Store.state;
|
||||
const snap = await serverSnapshot();
|
||||
if (!s.appliedTracked || !snap.rows.length) return jsonResponse(snap.adv);
|
||||
const { adv } = buildRows(s.appliedTracked, snap.rows, snap.adv);
|
||||
if (!s.applied || !snap.rows.length) return jsonResponse(snap.adv);
|
||||
const { adv } = buildRows(await appliedTracked(), snap.rows, snap.adv);
|
||||
for (const [k, val] of Object.entries(adv)) if (val) snap.adv[k] = val;
|
||||
return jsonResponse(snap.adv);
|
||||
},
|
||||
'GET Software-Cache.json': async () => {
|
||||
const s = Store.state;
|
||||
const cache = (await serverGet('/Software-Cache.json')) || {};
|
||||
if (s.appliedTracked) {
|
||||
if (s.applied) {
|
||||
const snap = await serverSnapshot();
|
||||
const { software } = buildRows(s.appliedTracked, snap.rows, snap.adv);
|
||||
const { software } = buildRows(await appliedTracked(), snap.rows, snap.adv);
|
||||
for (const [name, template] of Object.entries(software)) {
|
||||
if (cache[name]) continue;
|
||||
cache[name] = (template && cache[template]) || { Timestamp: new Date().toISOString(), Data: [] };
|
||||
@@ -949,7 +962,7 @@
|
||||
services would print. No one else sees your changes, and they're never sent to the server.
|
||||
Run it yourself without <code>-Demo</code> and every action works against the real backend.</p>
|
||||
<p class="usmt-demo-try"><strong>Things to try:</strong> open <em>Terminals</em> at the bottom to watch the worker sweep the
|
||||
fleet · add a device such as <code>PRVDT-7QK4M2XA</code> under <em>Tracked Devices</em> ·
|
||||
fleet and a simulated team work the schedule · add a device such as <code>PRVDT-7QK4M2XA</code> under <em>Tracked Devices</em> ·
|
||||
click a device for its details · <em>All Devices</em> shows apps missing from replacement machines ·
|
||||
edit an article under <em>Documentation</em>.</p>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user