PageSourceSearch

https://smartcone.ai/src/shell/types/rdwrxsrv/index.js

js smartcone.ai collected 2026-10-05 15:06:34 UTC 7,721 bytes, 166 lines download raw bytes

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.