1// RDWRXSRV â the RoadDefender fleet board, as a Studio shell. 2// 3// WHAT THIS REPLACES. The board shipped as seven standalone .html pages under the FE docroot 4// (index/watch/notifications/map/jobs/events/admin.html), navigated by <a href> and styled by 5// its own global shell.css. It worked, and it is the board running on cloud.smartcone.io â but 6// as a PAGE it sits beside the Studio rather than inside it: it cannot be opened in a window 7// next to anything else, it is absent from the app tray and the apps catalog, and it had to 8// hunt at runtime for the platform's own ws-dba client because it could not know where it had 9// been mounted. This is that board as a shell type, which is the platform's unit of an app. 10// 11// WHAT WAS NOT REWRITTEN. The panels. Every one of them already talked ws-dba through the 12// platform client and already rendered correctly; rewriting ~150KB of proven rendering to 13// change how an app is packaged would be the actual regression. They moved under panels/ 14// with their transport import repointed, and gained a stop() to match their start(). 15// 16// THE FOUR THINGS A SHELL NEEDS THAT A PAGE DID NOT: 17// 1. Scoped CSS. A page owns the document; `body{background:...}` here would repaint the 18// whole Studio. See styles.js. 19// 2. Tabs that swap the DOM instead of navigating. An <a href> would tear the Studio out 20// of the window. 21// 3. Teardown. A page navigation stopped every poll for free. Here nothing does, so each 22// tab switch must stop what the outgoing tab started or the intervals compound. 23// 4. One tab's markup live at a time â Fleet and Events share element IDs. See tabs.js. 24// 25// Raw ES module, no bundler, per the nextgen FE convention. 26 27import { ensureStyles } from "./styles.js"; 28import { TABS, DEFAULT_TAB } from "./tabs.js"; 29 30/** 31 * mount(el, body, opts) â the shell-type contract, same as telematics/server-manager. 32 * 33 * @param {HTMLElement} el the shell's content area 34 * @param {HTMLElement} body a full-size child of `el` to render into 35 * @param {object} opts {shell} â the window shell, when there is one 36 */ 37export function mount(el, body, opts = {}) { 38 ensureStyles(); 39 40 body.classList.add("shell-rdx"); 41 body.innerHTML = ""; 42 43 const main = document.createElement("div"); 44 main.className = "rdx-main"; 45 46 // ââ header ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 47 const header = document.createElement("header"); 48 header.className = "top"; 49 const h1 = document.createElement("h1"); 50 const rdx = document.createElement("span"); 51 rdx.className = "rdx"; 52 rdx.textContent = "RDWRXSRV"; 53 h1.append(rdx, document.createTextNode(" â Road30 Fleet")); 54 const badge = document.createElement("span"); 55 badge.className = "badge"; 56 badge.id = "badge"; 57 badge.hidden = true; 58 header.append(h1, badge); 59 60 // ââ tab bar âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 61 // Buttons, not links: a tab switch is a DOM swap, and an <a href> would both lie about 62 // that and, if followed, navigate the entire Studio away. 63 const nav = document.createElement("nav"); 64 nav.className = "tabs"; 65 nav.setAttribute("role", "tablist"); 66 67 const note = document.createElement("span"); 68 note.className = "navnote"; 69 70 const panel = document.createElement("div"); 71 panel.className = "rdx-tabpanel"; 72 panel.setAttribute("role", "tabpanel"); 73 74 /** The tab currently mounted, so switching can stop it before the markup goes away. */ 75 let current = null; 76 let generation = 0; // guards against a slow start() landing after the user moved on 77 78 async function show(tab) { 79 if (current && current.id === tab.id) return; 80 81 // Stop FIRST, while the outgoing tab's elements still exist. A panel's stop() only has 82 // to clear timers and document-level listeners, but doing it after the innerHTML wipe 83 // would leave a refresh mid-flight writing into detached no
83des. 84 if (current) { 85 try { current.stop(); } catch (err) { console.error(`[RDWRXSRV] ${current.id} stop failed:`, err); } 86 } 87 current = tab; 88 const mine = ++generation; 89 90 for (const b of nav.querySelectorAll("button")) { 91 b.setAttribute("aria-selected", String(b.dataset.tab === tab.id)); 92 } 93 note.textContent = tab.note || ""; 94 95 panel.innerHTML = tab.markup; 96 if (tab.badge) { badge.hidden = false; badge.textContent = tab.badge; badge.removeAttribute("style"); } 97 else { badge.hidden = true; } 98 99 try { 100 await tab.start(); 101 } catch (err) { 102 // A tab that failed to load says so in place. Silence would render as an empty fleet, 103 // which is the one thing an operations board must never do. 104 console.error(`[RDWRXSRV] ${tab.id} failed to start:`, err); 105 if (mine !== generation) return; // the user has already switched away 106 const box = document.createElement("div"); 107 box.className = "rdx-taberr"; 108 const h = document.createElement("h3"); 109 h.textContent = `The ${tab.label} board did not load`; 110 const p = document.createElement("p"); 111 p.textContent = "Nothing is being shown rather than something stale or wrong. " 112 + "The other tabs are unaffected."; 113 const pre = document.createElement("pre"); 114 pre.textContent = String((err && err.message) || err); 115 box.append(h, p, pre); 116 panel.prepend(box); 117 } 118 } 119 120 for (const tab of TABS) { 121 const b = document.createElement("button"); 122 b.type = "button"; 123 b.dataset.tab = tab.id; 124 b.textContent = tab.label; 125 b.setAttribute("role", "tab"); 126 b.setAttribute("aria-selected", "false"); 127 b.addEventListener("click", () => show(tab)); 128 nav.appendChild(b); 129 } 130 nav.appendChild(note); 131 132 main.append(header, nav, panel); 133 body.appendChild(main); 134 135 // ââ teardown ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 136 // Closing the window must stop the live tab's polling. Without this a closed shell keeps 137 // querying ws-dba for the life of the session, and the operator has no way to see it. 138 // 139 // â THERE IS NO CONTENT-LEVEL CLOSE HOOK TO USE. `createShellWindow` takes an `onClose` 140 // as a CONSTRUCTOR option (windowShell.js:25), owned by whoever built the window â not a 141 // method a mounted module can register against. appTray only untracks its own Map entry 142 // on close (appTray.js:807-830). So the shell watches for its own detachment, which is 143 // the same technique the tray uses and is correct however the window goes away â close 144 // button, programmatic removal, or a re-mount replacing the content area. 145 let observer = null; 146 const dispose = () => { 147 if (observer) { observer.disconnect(); observer = null; } 148 if (!current) return; 149 try { current.stop(); } catch (err) { console.error("[RDWRXSRV] dispose failed:", err); } 150 current = null; 151 }; 152 153 observer = new MutationObserver(() => { 154 if (!document.contains(body)) dispose(); 155 }); 156 observer.observe(document.documentElement, { childList: true, subtree: true }); 157 158 // Explicit teardown for a caller that has one; the observer is the backstop, not the plan. 159 el.addEventListener("rdx-dispose", dispose); 160 161 show(TABS.find((t) => t.id === DEFAULT_TAB) || TABS[0]); 162 163 return { dispose }; 164} 165 166export default mount;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.