diff --git a/Documentation/Adobe Acrobat Pro.md b/Documentation/Adobe Acrobat Pro.md
index a353d2c..b213396 100644
--- a/Documentation/Adobe Acrobat Pro.md
+++ b/Documentation/Adobe Acrobat Pro.md
@@ -1,3 +1,4 @@
+
[[Blue:Licensed]] [[Gray:Named User]]
# Adobe Acrobat Pro
diff --git a/Documentation/Adobe Creative Cloud.md b/Documentation/Adobe Creative Cloud.md
index 83cc30d..40608fa 100644
--- a/Documentation/Adobe Creative Cloud.md
+++ b/Documentation/Adobe Creative Cloud.md
@@ -1,3 +1,4 @@
+
[[Blue:Licensed]] [[Yellow:Large Install]]
# Adobe Creative Cloud
@@ -10,6 +11,7 @@ Creative Cloud apps (Photoshop, Illustrator, InDesign, ...) are installed throug
2. Have the user sign in with their company account.
3. From the **Apps** tab install only what was on the old device. The **All Devices** page in USMT Dash lists them under *Missing Software*.
+> [!NOTE]
> Expect 5-20 GB per app. Check free space on the destination before starting.
## Fonts and Presets
diff --git a/Documentation/Create Bootable USB.md b/Documentation/Create Bootable USB.md
index 21635b6..93cc21f 100644
--- a/Documentation/Create Bootable USB.md
+++ b/Documentation/Create Bootable USB.md
@@ -1,3 +1,4 @@
+
[[Gray:Reference]]
# Create a Bootable Imaging USB
diff --git a/Documentation/Device Storage Remediation.md b/Documentation/Device Storage Remediation.md
index 8c440f0..c9fb6d0 100644
--- a/Documentation/Device Storage Remediation.md
+++ b/Documentation/Device Storage Remediation.md
@@ -1,3 +1,4 @@
+
[[Yellow:Remediation]]
# Device Storage Remediation
diff --git a/Documentation/Diskpart Steps.md b/Documentation/Diskpart Steps.md
index d1f24f1..fb247b3 100644
--- a/Documentation/Diskpart Steps.md
+++ b/Documentation/Diskpart Steps.md
@@ -1,7 +1,9 @@
+
[[Red:Destructive]]
# Diskpart: Wipe and Re-partition a Disk (UEFI/GPT)
+> [!CAUTION]
> **This erases the disk.** Only use it on a device whose backup is complete, or on new hardware.
1. Boot into WinPE or the task sequence wizard and press **F8** for a command prompt.
diff --git a/Documentation/Manually Start USMT Backup.md b/Documentation/Manually Start USMT Backup.md
index 5fd40a0..c36266d 100644
--- a/Documentation/Manually Start USMT Backup.md
+++ b/Documentation/Manually Start USMT Backup.md
@@ -1,3 +1,4 @@
+
[[Yellow:Remediation]]
# Manually Start a USMT Backup
diff --git a/Documentation/Microsoft Office.md b/Documentation/Microsoft Office.md
index 8366ec3..a286adb 100644
--- a/Documentation/Microsoft Office.md
+++ b/Documentation/Microsoft Office.md
@@ -1,3 +1,4 @@
+
[[Green:Baseline]]
# Microsoft Office (Microsoft 365 Apps)
diff --git a/Documentation/Microsoft Power BI.md b/Documentation/Microsoft Power BI.md
index a5fefd3..f7960c0 100644
--- a/Documentation/Microsoft Power BI.md
+++ b/Documentation/Microsoft Power BI.md
@@ -1,3 +1,4 @@
+
[[Gray:Optional]]
# Microsoft Power BI
diff --git a/Documentation/Microsoft Project.md b/Documentation/Microsoft Project.md
index 3a43fb7..86d5677 100644
--- a/Documentation/Microsoft Project.md
+++ b/Documentation/Microsoft Project.md
@@ -1,3 +1,4 @@
+
[[Blue:Licensed]]
# Microsoft Project
diff --git a/Documentation/Microsoft Teams.md b/Documentation/Microsoft Teams.md
index 3163968..45b6411 100644
--- a/Documentation/Microsoft Teams.md
+++ b/Documentation/Microsoft Teams.md
@@ -1,3 +1,4 @@
+
[[Green:Baseline]]
# Microsoft Teams
diff --git a/Documentation/Microsoft Visio.md b/Documentation/Microsoft Visio.md
index 03ef84b..a06082a 100644
--- a/Documentation/Microsoft Visio.md
+++ b/Documentation/Microsoft Visio.md
@@ -1,3 +1,4 @@
+
[[Blue:Licensed]]
# Microsoft Visio
diff --git a/Documentation/Notepad++.md b/Documentation/Notepad++.md
index b8a773d..772f392 100644
--- a/Documentation/Notepad++.md
+++ b/Documentation/Notepad++.md
@@ -1,3 +1,4 @@
+
[[Gray:Optional]]
# Notepad++
diff --git a/Documentation/QuickBooks.md b/Documentation/QuickBooks.md
index bab0413..c85008b 100644
--- a/Documentation/QuickBooks.md
+++ b/Documentation/QuickBooks.md
@@ -1,3 +1,4 @@
+
[[Blue:Licensed]] [[Red:Finance Approval]]
# QuickBooks
diff --git a/Documentation/Salesforce.md b/Documentation/Salesforce.md
index 7e00461..c1eb781 100644
--- a/Documentation/Salesforce.md
+++ b/Documentation/Salesforce.md
@@ -1,3 +1,4 @@
+
[[Blue:Licensed]]
# Salesforce
diff --git a/Documentation/Slack.md b/Documentation/Slack.md
index 75072f6..5471ae6 100644
--- a/Documentation/Slack.md
+++ b/Documentation/Slack.md
@@ -1,3 +1,4 @@
+
[[Gray:Optional]]
# Slack
diff --git a/Documentation/Tableau.md b/Documentation/Tableau.md
index 952f3a5..5152c7d 100644
--- a/Documentation/Tableau.md
+++ b/Documentation/Tableau.md
@@ -1,3 +1,4 @@
+
[[Blue:Licensed]]
# Tableau Desktop
diff --git a/Documentation/Using USMT Dash.md b/Documentation/Using USMT Dash.md
index 426babc..c964081 100644
--- a/Documentation/Using USMT Dash.md
+++ b/Documentation/Using USMT Dash.md
@@ -1,3 +1,4 @@
+
[[Blue:Start Here]]
# Using USMT Dash
diff --git a/Documentation/WMI Namespace Fix.md b/Documentation/WMI Namespace Fix.md
index 9c0eb6d..9d9b676 100644
--- a/Documentation/WMI Namespace Fix.md
+++ b/Documentation/WMI Namespace Fix.md
@@ -1,3 +1,4 @@
+
[[Yellow:Remediation]] [[Red:Admin Required]]
# WMI Namespace Fix
diff --git a/Documentation/Zoom.md b/Documentation/Zoom.md
index dd395c5..e90de40 100644
--- a/Documentation/Zoom.md
+++ b/Documentation/Zoom.md
@@ -1,3 +1,4 @@
+
[[Gray:Optional]]
# Zoom
diff --git a/USMT-Server.ps1 b/USMT-Server.ps1
index edeb39d..64f0fbd 100755
--- a/USMT-Server.ps1
+++ b/USMT-Server.ps1
@@ -472,6 +472,8 @@ while ($listener.IsListening) {
# --- STATIC FILES ---
if ([string]::IsNullOrWhiteSpace($localPath)) { $localPath = "index.html" }
+ # Article permalinks (/docs/Adobe Acrobat Pro) are pages of the dashboard itself
+ if ($localPath -eq "docs" -or ($localPath -like "docs/*" -and $localPath -notmatch '\.(md|png|jpe?g|gif|webp|svg|css|js)$')) { $localPath = "index.html" }
# Data/ state files, Documentation/ articles + images, and web/ assets for everything else
if ($publicDataFiles -contains $localPath) {
diff --git a/web/demo/demo.js b/web/demo/demo.js
index 783291b..82f224d 100644
--- a/web/demo/demo.js
+++ b/web/demo/demo.js
@@ -1010,7 +1010,7 @@
if (!cfg.Demo) return realFetch(input, init);
let url;
- try { url = new URL(typeof input === 'string' ? input : input.url, location.href); } catch (e) { return realFetch(input, init); }
+ try { url = new URL(typeof input === 'string' ? input : input.url, document.baseURI); } catch (e) { return realFetch(input, init); }
if (url.origin !== location.origin) return realFetch(input, init);
await Store.load();
diff --git a/web/docs.css b/web/docs.css
new file mode 100644
index 0000000..19e5e06
--- /dev/null
+++ b/web/docs.css
@@ -0,0 +1,66 @@
+/* ---------- Knowledge base ---------- */
+/* search + count */
+.kb-search { flex-shrink: 0; margin-bottom: 10px; }
+.kb-search input { width: 100%; box-sizing: border-box; padding: 8px 10px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--input-bg); color: var(--text-main); font: inherit; font-size: 0.92em; outline: none; transition: border-color 0.15s, box-shadow 0.15s; }
+.kb-search input:focus { border-color: #3498db; box-shadow: 0 0 0 3px rgba(52,152,219,0.25); }
+.kb-count { color: var(--text-muted); font-size: 0.8em; margin-top: 6px; }
+#docList { gap: 14px !important; }
+.kb-empty { color: var(--text-muted); font-size: 0.9em; }
+
+/* groups + items */
+.kb-group-title { font-size: 0.72em; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); margin: 2px 0 6px; display: flex; justify-content: space-between; }
+.kb-group-title span { font-weight: 600; }
+.kb-item { display: flex; align-items: flex-start; gap: 6px; padding: 6px 8px; margin: 0 -8px 2px; border-radius: 6px; border: 1px solid transparent; transition: background 0.15s, border-color 0.15s, box-shadow 0.15s; }
+.kb-item:hover { background: rgba(52,152,219,0.08); border-color: rgba(52,152,219,0.45); box-shadow: 0 0 10px rgba(52,152,219,0.35); }
+.kb-item.active { background: rgba(52,152,219,0.14); border-color: #3498db; }
+.kb-item-main { min-width: 0; flex: 1; }
+.kb-item a { color: #3498db; text-decoration: none; font-weight: 500; overflow-wrap: anywhere; }
+.kb-item.active a { font-weight: 700; }
+.kb-snip { font-size: 0.78em; color: var(--text-muted); margin-top: 2px; line-height: 1.35; overflow-wrap: anywhere; }
+.kb-snip mark { background: rgba(52,152,219,0.3); color: inherit; border-radius: 2px; padding: 0 1px; }
+.kb-copy { background: none; border: 0; padding: 2px; margin-top: 1px; color: var(--text-muted); opacity: 0.5; cursor: pointer; transition: opacity 0.15s, color 0.15s; }
+.kb-copy:hover { opacity: 1; color: #3498db; }
+.kb-copy svg { width: 14px; height: 14px; fill: currentColor; }
+
+/* toolbar extras */
+.kb-tool { padding: 6px 10px !important; border: 1px solid var(--border-color) !important; display: inline-flex !important; align-items: center; gap: 6px; }
+.kb-tool svg { width: 14px; height: 14px; fill: currentColor; }
+.kb-cat-select { padding: 6px 8px; border: 1px solid var(--border-color); border-radius: 4px; background: var(--input-bg); color: var(--text-main); font: inherit; font-size: 0.85em; }
+#docToolbar { flex-wrap: wrap; }
+
+/* inline code: a pill that glows blue on hover (click to copy) */
+#docContent :not(pre) > code {
+ font-family: Consolas, "Cascadia Mono", Menlo, monospace; font-size: 0.88em;
+ background: rgba(127,127,127,0.14); border: 1px solid var(--border-color); border-radius: 5px;
+ padding: 1px 6px; color: var(--text-main); cursor: copy; white-space: break-spaces; overflow-wrap: anywhere;
+ transition: border-color 0.15s, box-shadow 0.15s, color 0.15s, background 0.15s;
+}
+#docContent :not(pre) > code:hover {
+ border-color: #3498db; color: #3498db; background: rgba(52,152,219,0.1);
+ box-shadow: 0 0 0 1px rgba(52,152,219,0.35), 0 0 12px rgba(52,152,219,0.55);
+}
+[data-theme="dark"] #docContent :not(pre) > code:hover { color: #85c1e9; }
+
+/* blockquotes: normal block flow (multi-line quotes, lists and links no longer squeeze) */
+#docContent blockquote { display: block !important; padding: 10px 16px !important; min-height: 0 !important; line-height: 1.6; }
+#docContent blockquote p { display: block !important; margin: 0 0 8px !important; }
+#docContent blockquote > :last-child { margin-bottom: 0 !important; }
+#docContent blockquote ul, #docContent blockquote ol { margin: 4px 0; padding-left: 22px; }
+
+/* GitHub-style alerts: > [!NOTE] */
+#docContent blockquote.kb-alert { border-left-width: 4px; }
+.kb-alert-title { font-weight: 700; margin-bottom: 4px; display: flex; align-items: center; gap: 6px; }
+#docContent .kb-alert-note { border-left-color: #2a78d6 !important; } .kb-alert-note .kb-alert-title { color: #2a78d6; }
+#docContent .kb-alert-tip { border-left-color: #0ca30c !important; } .kb-alert-tip .kb-alert-title { color: #0a8a0a; }
+#docContent .kb-alert-important { border-left-color: #8250df !important; } .kb-alert-important .kb-alert-title { color: #8250df; }
+#docContent .kb-alert-warning { border-left-color: #d29922 !important; } .kb-alert-warning .kb-alert-title { color: #9a6700; }
+#docContent .kb-alert-caution { border-left-color: #d03b3b !important; } .kb-alert-caution .kb-alert-title { color: #d03b3b; }
+[data-theme="dark"] .kb-alert-tip .kb-alert-title { color: #3fb950; }
+[data-theme="dark"] .kb-alert-important .kb-alert-title { color: #a371f7; }
+[data-theme="dark"] .kb-alert-warning .kb-alert-title { color: #d29922; }
+[data-theme="dark"] .kb-alert-caution .kb-alert-title { color: #f85149; }
+[data-theme="dark"] .kb-alert-note .kb-alert-title { color: #58a6ff; }
+
+@media (max-width: 1100px) {
+ .kb-tool span { display: none; }
+}
diff --git a/web/docs.js b/web/docs.js
new file mode 100644
index 0000000..f1c2646
--- /dev/null
+++ b/web/docs.js
@@ -0,0 +1,411 @@
+/*
+ * USMT Dash - knowledge base upgrades
+ *
+ * - Every article has its own address: /docs/
(shareable; back/forward work).
+ * - The article list is searchable (titles and full text), shows how many articles there
+ * are, and is split into Applications and Troubleshooting & Guides. An article's group
+ * comes from a hidden first line, , set from the editor;
+ * articles without one are grouped by a heuristic (named after an app = Applications).
+ * - PDF export lays the article out at page width with real page breaks, a title block
+ * and page numbers. Print uses the browser's own (vector) print-to-PDF.
+ * - GitHub-style alerts in blockquotes: > [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], [!CAUTION].
+ */
+(function () {
+ 'use strict';
+
+ const CATS = ['Applications', 'Troubleshooting & Guides'];
+ const CAT_RE = /^\s*[ \t]*\r?\n?/i;
+ const index = {}; // title -> { text, lower, category }
+ let query = '';
+ let indexing = null;
+ let firstLoad = true;
+
+ const esc = s => String(s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
+ const docs = () => (typeof availableDocs !== 'undefined' && Array.isArray(availableDocs)) ? availableDocs : [];
+ const permalink = t => '/docs/' + encodeURIComponent(t);
+ const currentTitle = () => (typeof currentDocTitle !== 'undefined' ? currentDocTitle : '');
+
+ function categoryOf(title, text) {
+ const m = String(text || '').match(CAT_RE);
+ if (m && m[1]) return m[1].trim();
+ // Heuristic: named after software we've seen on devices, or written as an install guide
+ try {
+ if (typeof softwareCache !== 'undefined') {
+ const t = title.toLowerCase();
+ for (const list of Object.values(softwareCache)) {
+ if (Array.isArray(list) && list.some(a => a && a.Name && getAppDisplayName(a.Name).toLowerCase() === t)) return CATS[0];
+ }
+ }
+ } catch (e) {}
+ if (/^#{1,3}\s*(install|installation|deploy)/im.test(text || '')) return CATS[0];
+ return CATS[1];
+ }
+
+ async function buildIndex() {
+ const titles = docs();
+ await Promise.all(titles.map(async t => {
+ try {
+ const r = await fetch(`/Documentation/${encodeURIComponent(t)}.md?t=${Date.now()}`);
+ const text = r.ok ? await r.text() : '';
+ index[t] = { text, lower: text.toLowerCase(), category: categoryOf(t, text) };
+ } catch (e) { index[t] = { text: '', lower: '', category: categoryOf(t, '') }; }
+ }));
+ Object.keys(index).forEach(t => { if (!titles.includes(t)) delete index[t]; });
+ }
+ function reindex() {
+ indexing = buildIndex().then(() => { indexing = null; renderSidebar(); });
+ return indexing;
+ }
+
+ // ---------------------------------------------------------------------------------
+ // Sidebar: search, count, categories
+ // ---------------------------------------------------------------------------------
+ const LINK_SVG = '';
+
+ function ensureSearchUi() {
+ const list = document.getElementById('docList');
+ if (!list || document.getElementById('docSearch')) return;
+ const box = document.createElement('div');
+ box.className = 'kb-search';
+ box.innerHTML = `
+
+ `;
+ list.parentElement.insertBefore(box, list);
+ const input = box.querySelector('input');
+ input.addEventListener('input', () => { query = input.value.trim(); renderSidebar(); });
+ input.addEventListener('keydown', e => {
+ if (e.key === 'Enter') { const first = list.querySelector('.kb-item a'); if (first) first.click(); }
+ if (e.key === 'Escape') { input.value = ''; query = ''; renderSidebar(); }
+ });
+ }
+
+ function snippet(text, q) {
+ const body = text.replace(CAT_RE, '').replace(/\[\[(\w+):([^\]|]+)(\|[^\]]*)?\]\]/g, '$2').replace(/[#>*_`]/g, '').replace(/\s+/g, ' ');
+ const i = body.toLowerCase().indexOf(q.toLowerCase());
+ if (i < 0) return '';
+ const start = Math.max(0, i - 40), end = Math.min(body.length, i + q.length + 60);
+ return (start ? '...' : '') + esc(body.slice(start, i)) + '' + esc(body.slice(i, i + q.length)) + '' + esc(body.slice(i + q.length, end)) + (end < body.length ? '...' : '');
+ }
+
+ function renderSidebar() {
+ ensureSearchUi();
+ const container = document.getElementById('docList');
+ if (!container) return;
+ const all = docs();
+ const count = document.getElementById('docCount');
+ if (!all.length) {
+ container.innerHTML = "No articles yet. Use + Add to write the first one.";
+ if (count) count.textContent = '0 articles';
+ return;
+ }
+ const q = query.toLowerCase();
+ const hits = all.map(t => {
+ const e = index[t];
+ const inTitle = !q || t.toLowerCase().includes(q);
+ const inText = q && e && e.lower.includes(q);
+ if (!inTitle && !inText) return null;
+ return { t, cat: e ? e.category : null, snip: (!inTitle && inText) ? snippet(e.text, query) : '' };
+ }).filter(Boolean);
+
+ if (count) {
+ count.textContent = q ? `${hits.length} of ${all.length} article${all.length === 1 ? '' : 's'} match` : `${all.length} article${all.length === 1 ? '' : 's'}`;
+ if (q && indexing) count.textContent += ' (searching text...)';
+ }
+
+ const groups = {};
+ hits.forEach(h => { const c = h.cat || 'Articles'; (groups[c] = groups[c] || []).push(h); });
+ const order = Object.keys(groups).sort((a, b) => {
+ const ia = CATS.indexOf(a), ib = CATS.indexOf(b);
+ return (ia < 0 ? 99 : ia) - (ib < 0 ? 99 : ib) || a.localeCompare(b);
+ });
+ const active = currentTitle();
+ container.innerHTML = hits.length ? order.map(cat => `
+
+
${esc(cat)} ${groups[cat].length}
+ ${groups[cat].map(h => `
+
+
+
+
${esc(h.t)}
+ ${h.snip ? `
${h.snip}
` : ''}
+
+
`).join('')}
+
`).join('') : `No article mentions "${esc(query)}".`;
+ }
+
+ // Clicks in the list: open in place (middle / ctrl-click still open a new tab)
+ document.addEventListener('click', e => {
+ const a = e.target.closest('#docList a[data-doc]');
+ if (a && !e.ctrlKey && !e.metaKey && !e.shiftKey && e.button === 0) { e.preventDefault(); openDoc(a.dataset.doc); return; }
+ // Links between articles (/docs/Title or the older ?doc=Title) open in place
+ const link = e.target.closest('#docContent a[href]');
+ if (link && !e.ctrlKey && !e.metaKey && !e.shiftKey && e.button === 0 && link.target !== '_blank') {
+ let target = null;
+ try {
+ const u = new URL(link.getAttribute('href'), document.baseURI);
+ if (u.origin === location.origin) {
+ const m = u.pathname.match(/^\/docs\/(.+)$/);
+ target = m ? decodeURIComponent(m[1]) : u.searchParams.get('doc');
+ }
+ } catch (err) {}
+ const doc = findDoc(target);
+ if (doc) { e.preventDefault(); openDoc(doc); return; }
+ }
+ const c = e.target.closest('#docList .kb-copy');
+ if (c) { e.preventDefault(); copyDocLink(c.dataset.copyDoc); }
+ });
+
+ // ---------------------------------------------------------------------------------
+ // Permalinks
+ // ---------------------------------------------------------------------------------
+ function titleFromPath() {
+ const m = location.pathname.match(/^\/docs\/(.+)$/);
+ if (!m) return null;
+ try { return decodeURIComponent(m[1]).replace(/\/+$/, ''); } catch (e) { return null; }
+ }
+ function findDoc(name) {
+ if (!name) return null;
+ const all = docs();
+ return all.find(d => d === name) || all.find(d => d.toLowerCase() === name.toLowerCase()) || null;
+ }
+ function setUrl(path, push) {
+ if (location.pathname + location.search === path) return;
+ try { history[push ? 'pushState' : 'replaceState']({ doc: path }, '', path); } catch (e) {}
+ }
+
+ function copyPageLink() {
+ const t = currentTitle();
+ if (!t) return;
+ fallbackCopyTextToClipboard(location.origin + permalink(t));
+ showToast('Page link copied to clipboard!');
+ }
+
+ function addToolbarButtons() {
+ const bar = document.getElementById('docToolbar');
+ if (!bar || document.getElementById('btn-copy-page')) return;
+ const pdf = document.getElementById('btn-download-pdf');
+ const mk = (id, title, html, fn) => {
+ const b = document.createElement('button');
+ b.className = 'nav-btn kb-tool'; b.id = id; b.title = title; b.type = 'button'; b.innerHTML = html; b.onclick = fn;
+ bar.insertBefore(b, pdf);
+ return b;
+ };
+ mk('btn-copy-page', 'Copy a link to this page', `${LINK_SVG}Link`, copyPageLink);
+ mk('btn-print-doc', 'Print, or save as PDF with your browser', 'Print', printDoc);
+
+ // Category picker (edit mode only)
+ const sel = document.createElement('select');
+ sel.id = 'docCategory'; sel.className = 'kb-cat-select'; sel.title = 'Which group this article is listed under';
+ sel.innerHTML = CATS.map(c => ``).join('');
+ sel.style.display = 'none';
+ sel.onchange = () => {
+ const ed = document.getElementById('docEditor');
+ ed.value = `\n` + ed.value.replace(CAT_RE, '');
+ };
+ bar.insertBefore(sel, document.getElementById('btn-save-doc'));
+ }
+
+ // ---------------------------------------------------------------------------------
+ // PDF + print
+ // ---------------------------------------------------------------------------------
+ const PRINT_CSS = `
+ .kb-print { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; color: #1f2328; background: #fff; font-size: 12.5px; line-height: 1.55; }
+ .kb-print * { box-sizing: border-box; }
+ .kb-print .kb-print-head { border-bottom: 2px solid #2a78d6; padding-bottom: 8px; margin-bottom: 14px; }
+ .kb-print .kb-print-head h1 { font-size: 22px; margin: 0; border: 0; padding: 0; color: #1f2328; }
+ .kb-print .kb-print-head div { color: #59636e; font-size: 11px; margin-top: 3px; }
+ .kb-print h1, .kb-print h2 { border-bottom: 1px solid #d1d9e0; padding-bottom: 4px; color: #1f2328; }
+ .kb-print h1 { font-size: 19px; } .kb-print h2 { font-size: 16px; } .kb-print h3 { font-size: 14px; }
+ .kb-print h1, .kb-print h2, .kb-print h3, .kb-print h4 { break-after: avoid; page-break-after: avoid; margin: 14px 0 8px; }
+ .kb-print p, .kb-print li { orphans: 3; widows: 3; }
+ .kb-print pre, .kb-print table, .kb-print blockquote, .kb-print img, .kb-print .kb-code { break-inside: avoid; page-break-inside: avoid; }
+ .kb-print a { color: #0969da; text-decoration: none; font-weight: 600; }
+ .kb-print code { font-family: Consolas, "Cascadia Mono", Menlo, monospace; font-size: 0.88em; background: #eff2f5; border: 1px solid #d1d9e0; border-radius: 4px; padding: 1px 5px; color: #1f2328; }
+ .kb-print pre { background: #f6f8fa !important; border: 1px solid #d1d9e0; border-radius: 6px; padding: 10px 12px !important; white-space: pre-wrap; word-break: break-word; margin: 8px 0 !important; }
+ .kb-print pre code, .kb-print pre code * { background: none !important; border: 0; padding: 0; color: #1f2328 !important; }
+ .kb-print .kb-code-head, .kb-print .copy-code-btn, .kb-print button { display: none !important; }
+ .kb-print table { border-collapse: collapse; width: 100%; margin: 8px 0; font-size: 0.95em; }
+ .kb-print th, .kb-print td { border: 1px solid #d1d9e0; padding: 5px 8px; text-align: left; vertical-align: top; }
+ .kb-print th { background: #eef4fc !important; color: #1f2328 !important; font-weight: 700; }
+ .kb-print td { background: #fff !important; color: #1f2328 !important; }
+ .kb-print th, .kb-print td { white-space: normal !important; overflow: visible !important; text-overflow: clip !important; max-width: none !important; position: static !important; }
+ .kb-print p, .kb-print li, .kb-print h1, .kb-print h2, .kb-print h3, .kb-print h4, .kb-print strong, .kb-print em { color: #1f2328; }
+ .kb-print blockquote { display: block !important; margin: 8px 0; padding: 8px 12px; border-left: 4px solid #2a78d6; background: #f3f7fc; color: #1f2328; border-radius: 0 4px 4px 0; }
+ .kb-print blockquote p { display: block !important; margin: 0 0 6px; }
+ .kb-print blockquote p:last-child { margin-bottom: 0; }
+ .kb-print img { max-width: 100% !important; max-height: 420px; height: auto !important; display: block; margin: 8px 0; border: 1px solid #d1d9e0; border-radius: 4px; }
+ .kb-print .tag-badge { border: 1px solid currentColor; }
+ .kb-print mark { background: none; }`;
+
+ function printableClone() {
+ const content = document.getElementById('docContent');
+ const wrap = document.createElement('div');
+ wrap.className = 'kb-print';
+ const st = document.createElement('style'); st.textContent = PRINT_CSS; wrap.appendChild(st);
+ const head = document.createElement('div'); head.className = 'kb-print-head';
+ const date = document.getElementById('docEditDate');
+ const body = content.cloneNode(true);
+ // The article usually opens with its own H1; don't print the title twice
+ const firstH1 = body.querySelector('h1');
+ const dup = firstH1 && firstH1.textContent.trim().toLowerCase() === currentTitle().trim().toLowerCase();
+ head.innerHTML = `${dup ? '' : `${esc(currentTitle())}
`}${esc(date ? date.textContent : '')}${date ? ' · ' : ''}${esc(location.origin + '/docs/' + currentTitle())}
`;
+ wrap.appendChild(head);
+ body.removeAttribute('id'); body.removeAttribute('style');
+ // Undo on-screen code block chrome (language bar + copy button container)
+ body.querySelectorAll('pre').forEach(pre => { pre.removeAttribute('style'); });
+ body.querySelectorAll('[style]').forEach(n => { if (n.tagName !== 'SPAN' || !n.classList.contains('tag-badge')) n.style.removeProperty('background'); });
+ wrap.appendChild(body);
+ return wrap;
+ }
+
+ function waitForImages(root) {
+ return Promise.all(Array.from(root.querySelectorAll('img')).map(img => img.complete ? null : new Promise(r => { img.onload = img.onerror = r; })));
+ }
+
+ async function downloadPdf() {
+ if (!currentTitle()) return;
+ const btn = document.getElementById('btn-download-pdf');
+ const label = btn.innerHTML;
+ btn.disabled = true;
+ btn.innerHTML = label.replace('PDF', 'Exporting...');
+ // Lay the article out offscreen at the printable width of an A4/Letter page
+ // (images load while it's briefly attached and hidden, then html2pdf takes its own copy)
+ const host = document.createElement('div');
+ host.style.cssText = 'position: fixed; left: 0; top: 0; width: 688px; visibility: hidden; pointer-events: none;';
+ const page = printableClone();
+ page.style.width = '688px';
+ host.appendChild(page);
+ document.body.appendChild(host);
+ try {
+ await waitForImages(page);
+ host.removeChild(page);
+ const opt = {
+ margin: [14, 14, 16, 14],
+ filename: `${currentTitle()}.pdf`,
+ image: { type: 'jpeg', quality: 0.95 },
+ html2canvas: { scale: 2, useCORS: true, logging: false, backgroundColor: '#ffffff', scrollX: 0, scrollY: 0 },
+ jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
+ pagebreak: { mode: ['css', 'legacy'], avoid: ['pre', 'table', 'img', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'li', 'tr'] }
+ };
+ 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();
+ pdf.setFontSize(8); pdf.setTextColor(120);
+ for (let i = 1; i <= n; i++) {
+ pdf.setPage(i);
+ pdf.text(currentTitle(), 14, h - 7);
+ pdf.text(`Page ${i} of ${n}`, w - 14, h - 7, { align: 'right' });
+ }
+ }).save();
+ } catch (e) {
+ showToast('PDF export failed. Try Print > Save as PDF.');
+ } finally {
+ host.remove();
+ btn.disabled = false;
+ btn.innerHTML = label;
+ }
+ }
+
+ function printDoc() {
+ if (!currentTitle()) return;
+ const w = window.open('', '_blank');
+ if (!w) { showToast('Allow pop-ups to print this article.'); return; }
+ const page = printableClone();
+ w.document.write(`${esc(currentTitle())}
+ `);
+ w.document.body.appendChild(w.document.importNode(page, true));
+ w.document.close();
+ const go = () => { w.focus(); w.print(); };
+ Promise.all(Array.from(w.document.images).map(i => i.complete ? null : new Promise(r => { i.onload = i.onerror = r; }))).then(() => setTimeout(go, 150));
+ }
+
+ // ---------------------------------------------------------------------------------
+ // Rendering extras: GitHub-style alerts
+ // ---------------------------------------------------------------------------------
+ const ALERTS = { NOTE: 'Note', TIP: 'Tip', IMPORTANT: 'Important', WARNING: 'Warning', CAUTION: 'Caution' };
+ function decorate() {
+ document.querySelectorAll('#docContent blockquote').forEach(bq => {
+ if (bq.dataset.kb) return;
+ bq.dataset.kb = '1';
+ const p = bq.querySelector('p');
+ if (!p) return;
+ const m = p.innerHTML.match(/^\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*(
)?\s*/i);
+ if (!m) return;
+ const kind = m[1].toUpperCase();
+ p.innerHTML = p.innerHTML.slice(m[0].length);
+ if (!p.innerHTML.trim()) p.remove();
+ bq.classList.add('kb-alert', 'kb-alert-' + kind.toLowerCase());
+ const t = document.createElement('div');
+ t.className = 'kb-alert-title';
+ t.textContent = ALERTS[kind];
+ bq.insertBefore(t, bq.firstChild);
+ });
+ }
+
+ // ---------------------------------------------------------------------------------
+ // Wiring
+ // ---------------------------------------------------------------------------------
+ function wrap(name, after, before) {
+ const orig = window[name];
+ if (typeof orig !== 'function') return;
+ window[name] = function () {
+ if (before) before.apply(this, arguments);
+ const r = orig.apply(this, arguments);
+ if (r && typeof r.then === 'function') return r.then(v => { after && after.apply(this, arguments); return v; });
+ if (after) after.apply(this, arguments);
+ return r;
+ };
+ }
+
+ document.addEventListener('DOMContentLoaded', () => {
+ window.renderDocsSidebar = renderSidebar;
+ window.downloadDocAsPDF = downloadPdf;
+ // Markdown link for use inside other articles
+ window.copyDocLink = function (docName) {
+ fallbackCopyTextToClipboard(`[${docName}](${permalink(docName)})`);
+ showToast('Markdown link copied: paste it into another article.');
+ };
+ addToolbarButtons();
+
+ wrap('loadDocsList', () => {
+ reindex();
+ if (firstLoad) {
+ firstLoad = false;
+ const t = titleFromPath();
+ if (location.pathname.startsWith('/docs')) {
+ const doc = findDoc(t);
+ if (doc) openDoc(doc);
+ else {
+ switchPage('docs');
+ if (t) {
+ const c = document.getElementById('docContent');
+ if (c) c.innerHTML = `There's no article called "${esc(t)}". Pick one from the list, or search for it.`;
+ }
+ }
+ }
+ }
+ });
+ wrap('openDoc', () => { renderSidebar(); decorate(); }, name => { setUrl(permalink(name), true); });
+ wrap('formatCodeBlocks', decorate);
+ wrap('setDocMode', mode => {
+ const sel = document.getElementById('docCategory');
+ if (!sel) return;
+ sel.style.display = mode === 'edit' ? '' : 'none';
+ if (mode === 'edit') {
+ const ed = document.getElementById('docEditor');
+ const m = ed.value.match(CAT_RE);
+ sel.value = m && CATS.includes(m[1].trim()) ? m[1].trim() : categoryOf(currentTitle(), ed.value);
+ if (!m) ed.value = `\n` + ed.value;
+ }
+ });
+ wrap('switchPage', id => {
+ if (id !== 'docs' && location.pathname.startsWith('/docs')) setUrl('/', false);
+ if (id === 'docs' && currentTitle() && !location.pathname.startsWith('/docs/')) setUrl(permalink(currentTitle()), false);
+ });
+ window.addEventListener('popstate', () => {
+ const doc = findDoc(titleFromPath());
+ if (doc) openDoc(doc);
+ else if (location.pathname === '/' && document.getElementById('docs').style.display !== 'none') switchPage('overview');
+ });
+ });
+})();
diff --git a/web/index.html b/web/index.html
index bb3c57a..b5d4832 100755
--- a/web/index.html
+++ b/web/index.html
@@ -4,6 +4,7 @@
USMT Dash
+
@@ -353,9 +354,11 @@
+
+
@@ -1751,7 +1754,7 @@
const preview = document.getElementById('docContent');
if (preview && preview.style.display !== 'none') {
// Encode spaces ONLY in UNC/Local paths so marked.js doesn't fail to parse them
- let safeText = content.replace(/\[([^\]]+)\]\(((\\\\|[A-Za-z]:\\)[^\)]+)\)/g, (match, title, url) => {
+ let safeText = shieldCode(content).replace(/\[([^\]]+)\]\(((\\\\|[A-Za-z]:\\)[^\)]+)\)/g, (match, title, url) => {
return `[${title}](${url.replace(/ /g, '%20')})`;
});
@@ -1772,7 +1775,7 @@
return `${safeDisplay}`;
});
- preview.innerHTML = typeof marked !== 'undefined' ? marked.parse(safeText) : `${safeText}`;
+ preview.innerHTML = typeof marked !== 'undefined' ? marked.parse(unshieldCode(safeText)) : `${unshieldCode(safeText)}`;
formatCodeBlocks();
}
}
@@ -1826,6 +1829,17 @@
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`
+ const CODE_SHIELD = [];
+ function shieldCode(text) {
+ CODE_SHIELD.length = 0;
+ return String(text).replace(/(```[\s\S]*?```|~~~[\s\S]*?~~~|`[^`\n]+`)/g, m => `\u0000CODE${CODE_SHIELD.push(m) - 1}\u0000`);
+ }
+ function unshieldCode(text) {
+ return String(text).replace(/\u0000CODE(\d+)\u0000/g, (m, i) => CODE_SHIELD[+i]);
+ }
+
async function loadDocsList() {
try {
const res = await fetch('/get-docs-list?t=' + Date.now());
@@ -1890,7 +1904,7 @@
`;
// Encode spaces ONLY in UNC/Local paths so marked.js doesn't fail to parse them
- let safeText = text.replace(/\[([^\]]+)\]\(((\\\\|[A-Za-z]:\\)[^\)]+)\)/g, (match, title, url) => {
+ let safeText = shieldCode(text).replace(/\[([^\]]+)\]\(((\\\\|[A-Za-z]:\\)[^\)]+)\)/g, (match, title, url) => {
return `[${title}](${url.replace(/ /g, '%20')})`;
});
@@ -1911,7 +1925,7 @@
return `${safeDisplay}`;
});
- const parsedContent = typeof marked !== 'undefined' ? marked.parse(safeText) : `${safeText}`;
+ const parsedContent = typeof marked !== 'undefined' ? marked.parse(unshieldCode(safeText)) : `${unshieldCode(safeText)}`;
document.getElementById('docContent').innerHTML = parsedContent;
// Keep the actual spaces in the editor so the user doesn't see ugly %20 encoding!
@@ -1975,7 +1989,7 @@
const rawText = editor.value;
if (rawText) {
// Encode spaces ONLY in UNC/Local paths so marked.js doesn't fail to parse them
- let safeText = rawText.replace(/\[([^\]]+)\]\(((\\\\|[A-Za-z]:\\)[^\)]+)\)/g, (match, title, url) => {
+ let safeText = shieldCode(rawText).replace(/\[([^\]]+)\]\(((\\\\|[A-Za-z]:\\)[^\)]+)\)/g, (match, title, url) => {
return `[${title}](${url.replace(/ /g, '%20')})`;
});
@@ -1996,7 +2010,7 @@
return `${safeDisplay}`;
});
- preview.innerHTML = typeof marked !== 'undefined' ? marked.parse(safeText) : `${safeText}`;
+ preview.innerHTML = typeof marked !== 'undefined' ? marked.parse(unshieldCode(safeText)) : `${unshieldCode(safeText)}`;
formatCodeBlocks();
}
}
@@ -2688,7 +2702,7 @@
const nameText = s.Name || '--';
const linkMatch = availableDocs.find(d => d.toLowerCase() === nameText.toLowerCase());
const nameDisplay = linkMatch
- ? `${nameText}`
+ ? `${nameText}`
: `${nameText}`;
return `
@@ -2833,7 +2847,7 @@
let hyperlinkedApps = sApps.map(app => {
const linkMatch = availableDocs.find(d => d.toLowerCase() === app.toLowerCase());
return linkMatch
- ? `${app}`
+ ? `${app}`
: app;
});
specialtySoftware = hyperlinkedApps.length > 0 ? hyperlinkedApps.join("
") : "None";
@@ -3058,7 +3072,7 @@
let hyperlinkedMissing = missing.map(app => {
const linkMatch = availableDocs.find(d => d.toLowerCase() === app.toLowerCase());
return linkMatch
- ? `${app}`
+ ? `${app}`
: app;
});
missingSoftwareDisplay = hyperlinkedMissing.length > 0 ? hyperlinkedMissing.join(", ") : "None";
@@ -3171,7 +3185,7 @@
let hyperlinkedMissing = missing.map(app => {
const linkMatch = availableDocs.find(d => d.toLowerCase() === app.toLowerCase());
return linkMatch
- ? `${app}`
+ ? `${app}`
: app;
});
enriched["MissingSoftwareDisplay"] = hyperlinkedMissing.length > 0 ? hyperlinkedMissing.join(", ") : "None";