locadot 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +17 -1
- package/dist/cli/commands/hosts.js +1 -1
- package/dist/cli/commands/index.js +2 -1
- package/dist/cli/commands/protect.js +76 -0
- package/dist/cli/commands/tunnel.js +12 -1
- package/dist/cli/commands/ui.js +6 -3
- package/dist/constants/index.js +1 -0
- package/dist/dashboard/api.js +11 -1
- package/dist/dashboard/index.js +2 -0
- package/dist/dashboard/page.js +20 -4285
- package/dist/dashboard/ui/core.js +803 -0
- package/dist/dashboard/ui/hosts.js +1522 -0
- package/dist/dashboard/ui/remote.js +958 -0
- package/dist/dashboard/ui/shell.js +509 -0
- package/dist/dashboard/ui/system.js +764 -0
- package/dist/dashboard/ui/theme.js +1710 -0
- package/dist/index.js +11 -0
- package/dist/lib/host-auth.js +123 -0
- package/dist/lib/hosts.js +53 -0
- package/dist/proxy/cloudflare-logins.js +197 -0
- package/dist/proxy/protect.js +215 -0
- package/dist/proxy/router.js +5 -0
- package/dist/proxy/tunnel.js +16 -11
- package/package.json +1 -1
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/** The app shell: sidebar, top bar, routing, preferences, and the shared confirm/prompt dialogs and toasts. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.toastMarkup = exports.dialogsMarkup = exports.shellBottom = exports.shellTop = exports.jsRouting = exports.jsPrefs = exports.jsDialogs = exports.jsHeader = void 0;
|
|
5
|
+
exports.jsHeader = ` // ---------- header + system tiles ----------
|
|
6
|
+
|
|
7
|
+
function hideHostsSkeleton() {
|
|
8
|
+
if (!hostsBooted) {
|
|
9
|
+
hostsBooted = true;
|
|
10
|
+
hostsSkeletonEl.hidden = true;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function showLoadError(err) {
|
|
15
|
+
isDown = true;
|
|
16
|
+
var reason = (err && err.message) ? err.message : "connection failed";
|
|
17
|
+
loadErrorText.textContent = "Can't reach the locadot proxy (" + reason + "). Start it with locadot start; this page retries every 5 s.";
|
|
18
|
+
loadErrorBanner.hidden = false;
|
|
19
|
+
if (!lastStatus) hostsErrorEl.hidden = false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function hideLoadError() {
|
|
23
|
+
isDown = false;
|
|
24
|
+
loadErrorBanner.hidden = true;
|
|
25
|
+
hostsErrorEl.hidden = true;
|
|
26
|
+
liveLabel.textContent = "live";
|
|
27
|
+
document.body.classList.remove("offline");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
loadErrorRetry.addEventListener("click", function () {
|
|
31
|
+
loadErrorRetry.disabled = true;
|
|
32
|
+
loadErrorRetry.classList.add("busy");
|
|
33
|
+
refreshAll().then(function () {
|
|
34
|
+
loadErrorRetry.disabled = false;
|
|
35
|
+
loadErrorRetry.classList.remove("busy");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function renderHeader(status) {
|
|
40
|
+
document.body.classList.remove("boot");
|
|
41
|
+
versionEl.textContent = "v" + status.proxy.version;
|
|
42
|
+
uptimeEl.textContent = "uptime " + fmtUptime(status.uptimeSec);
|
|
43
|
+
pidEl.textContent = "pid " + status.proxy.pid;
|
|
44
|
+
updatedEl.textContent = "Updated " + new Date().toLocaleTimeString();
|
|
45
|
+
liveDot.classList.remove("down");
|
|
46
|
+
liveDot.classList.add("up");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function markDown() {
|
|
50
|
+
liveDot.classList.remove("up");
|
|
51
|
+
liveDot.classList.add("down");
|
|
52
|
+
liveLabel.textContent = "offline";
|
|
53
|
+
document.body.classList.add("offline");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function renderSystemTiles(status) {
|
|
57
|
+
var sys = status.system;
|
|
58
|
+
var proxy = status.proxy;
|
|
59
|
+
|
|
60
|
+
rootValueEl.textContent = sys.isRoot ? "Elevated" : "User";
|
|
61
|
+
rootValueEl.className = "tile-value " + (sys.isRoot ? "ok" : "");
|
|
62
|
+
portsValueEl.textContent = "Ports <1024: " + (sys.canBindPrivileged ? "allowed" : "blocked");
|
|
63
|
+
clear(portsHintEl);
|
|
64
|
+
if (!sys.canBindPrivileged && sys.platform === "linux") {
|
|
65
|
+
var hintText = "sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80";
|
|
66
|
+
var code = document.createElement("code");
|
|
67
|
+
code.className = "mono";
|
|
68
|
+
code.textContent = hintText;
|
|
69
|
+
portsHintEl.appendChild(code);
|
|
70
|
+
portsHintEl.appendChild(makeCopyButton(function () { return hintText; }));
|
|
71
|
+
portsHintEl.hidden = false;
|
|
72
|
+
} else {
|
|
73
|
+
portsHintEl.hidden = true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
var caTrusted = sys.caTrusted;
|
|
77
|
+
caValueEl.textContent = caTrusted === true ? "Trusted" : caTrusted === false ? "Not trusted" : "Unknown";
|
|
78
|
+
caValueEl.className = "tile-value " + (caTrusted === true ? "ok" : caTrusted === false ? "warn" : "");
|
|
79
|
+
if (!caBusy) {
|
|
80
|
+
caToggle.setAttribute("aria-checked", String(caTrusted === true));
|
|
81
|
+
caToggle.classList.toggle("on", caTrusted === true);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
startupValueEl.textContent = sys.startup.enabled ? "Enabled" : "Disabled";
|
|
85
|
+
startupValueEl.className = "tile-value " + (sys.startup.enabled ? "ok" : "");
|
|
86
|
+
startupMethodEl.textContent = sys.startup.method ? ("via " + sys.startup.method) : "";
|
|
87
|
+
if (!startupBusy) {
|
|
88
|
+
startupToggle.setAttribute("aria-checked", String(sys.startup.enabled));
|
|
89
|
+
startupToggle.classList.toggle("on", sys.startup.enabled);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
proxyPortsEl.textContent = "http :" + proxy.httpPort + " \\u00b7 https :" + proxy.httpsPort;
|
|
93
|
+
proxyBindEl.textContent = "bind " + proxy.bind.join(", ");
|
|
94
|
+
proxyStateDirEl.textContent = proxy.stateDir;
|
|
95
|
+
proxyPlatformEl.textContent = sys.platform;
|
|
96
|
+
|
|
97
|
+
var cf = sys.cloudflared || { installed: false };
|
|
98
|
+
cloudflaredInstalled = !!cf.installed;
|
|
99
|
+
cloudflareLoggedIn = !!cf.loggedIn;
|
|
100
|
+
cloudflareZones = Array.isArray(cf.zones) ? cf.zones : [];
|
|
101
|
+
if (cf.installed) {
|
|
102
|
+
cfValueEl.textContent = "Installed";
|
|
103
|
+
cfValueEl.className = "tile-value ok";
|
|
104
|
+
cfSubEl.textContent = [cf.version, cf.path].filter(Boolean).join(" \\u00b7 ");
|
|
105
|
+
if (!cfInstallBusy) cfInstallBtn.hidden = true;
|
|
106
|
+
} else {
|
|
107
|
+
cfValueEl.textContent = "Not installed";
|
|
108
|
+
cfValueEl.className = "tile-value warn";
|
|
109
|
+
cfSubEl.textContent = "";
|
|
110
|
+
if (!cfInstallBusy) cfInstallBtn.hidden = false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function renderCli(status) {
|
|
115
|
+
var stateDir = status.proxy.stateDir;
|
|
116
|
+
var port = status.proxy.httpPort;
|
|
117
|
+
var urlBase = "http://localhost" + (port !== 80 ? ":" + port : "");
|
|
118
|
+
var curl = "curl -X POST " + urlBase + "/api/hosts" +
|
|
119
|
+
' -H "X-Locadot-Token: $(cat ' + stateDir + '/.locadot-token)"' +
|
|
120
|
+
" -H 'Content-Type: application/json'" +
|
|
121
|
+
' -d \\'{"host":"app.localhost","target":"3000"}\\'';
|
|
122
|
+
cliCurlPre.textContent = curl;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------- status pill (details + quick actions) ----------
|
|
126
|
+
|
|
127
|
+
var statusBtn = document.getElementById("status-btn");
|
|
128
|
+
function stripPrefix(text, prefix) {
|
|
129
|
+
text = text || "";
|
|
130
|
+
return text.indexOf(prefix) === 0 ? text.slice(prefix.length) : text;
|
|
131
|
+
}
|
|
132
|
+
statusBtn.addEventListener("click", function () {
|
|
133
|
+
var items = [
|
|
134
|
+
{ info: true, label: "Proxy", hint: isDown ? "offline" : "live" },
|
|
135
|
+
{ info: true, label: "Version", hint: versionEl.textContent || "\u2014" },
|
|
136
|
+
{ info: true, label: "Uptime", hint: stripPrefix(uptimeEl.textContent, "uptime ") || "\u2014" },
|
|
137
|
+
{ info: true, label: "PID", hint: stripPrefix(pidEl.textContent, "pid ") || "\u2014" },
|
|
138
|
+
{ info: true, label: "Last update", hint: stripPrefix(updatedEl.textContent, "Updated ") || "\u2014" },
|
|
139
|
+
"-",
|
|
140
|
+
{ label: "Refresh now", icon: ICONS.refresh, onSelect: function () { refreshAll().then(tickViews); refreshHubAndRemotes(); } },
|
|
141
|
+
{ label: "View logs", icon: ICONS.logs, onSelect: function () { location.hash = "#/logs"; } },
|
|
142
|
+
{ label: "Settings", icon: ICONS.settings, onSelect: function () { location.hash = "#/settings"; } }
|
|
143
|
+
];
|
|
144
|
+
var signOut = document.getElementById("sb-signout");
|
|
145
|
+
if (signOut && !signOut.hidden) items.push("-", { label: "Sign out", icon: ICONS.signOut, danger: true, onSelect: function () { signOut.click(); } });
|
|
146
|
+
openMenu(statusBtn, items, { label: "Proxy status", title: "Proxy status", align: "end" });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
`;
|
|
150
|
+
exports.jsDialogs = ` // ---------- confirm / prompt dialogs ----------
|
|
151
|
+
|
|
152
|
+
var confirmDialogEl = document.getElementById("confirm-dialog");
|
|
153
|
+
var confirmTitleEl = document.getElementById("confirm-title");
|
|
154
|
+
var confirmMessageEl = document.getElementById("confirm-message");
|
|
155
|
+
var confirmCancelBtn = document.getElementById("confirm-cancel");
|
|
156
|
+
var confirmOkBtn = document.getElementById("confirm-ok");
|
|
157
|
+
var confirmCloseBtn = document.getElementById("confirm-close");
|
|
158
|
+
var confirmResolve = null;
|
|
159
|
+
|
|
160
|
+
function closeConfirmDialog(result) {
|
|
161
|
+
var resolve = confirmResolve;
|
|
162
|
+
confirmResolve = null;
|
|
163
|
+
confirmDialogEl.close();
|
|
164
|
+
if (resolve) resolve(result);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function confirmDialog(opts) {
|
|
168
|
+
opts = opts || {};
|
|
169
|
+
return new Promise(function (resolve) {
|
|
170
|
+
confirmResolve = resolve;
|
|
171
|
+
confirmTitleEl.textContent = opts.title || "Are you sure?";
|
|
172
|
+
confirmMessageEl.textContent = opts.message || "";
|
|
173
|
+
confirmOkBtn.textContent = opts.confirmLabel || "OK";
|
|
174
|
+
confirmOkBtn.className = "btn " + (opts.danger ? "btn-danger" : "btn-primary");
|
|
175
|
+
confirmDialogEl.showModal();
|
|
176
|
+
setTimeout(function () { confirmOkBtn.focus(); }, 0);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
confirmCancelBtn.addEventListener("click", function () { closeConfirmDialog(false); });
|
|
181
|
+
confirmCloseBtn.addEventListener("click", function () { closeConfirmDialog(false); });
|
|
182
|
+
confirmOkBtn.addEventListener("click", function () { closeConfirmDialog(true); });
|
|
183
|
+
confirmDialogEl.addEventListener("click", function (e) {
|
|
184
|
+
if (e.target === confirmDialogEl) closeConfirmDialog(false);
|
|
185
|
+
});
|
|
186
|
+
confirmDialogEl.addEventListener("close", function () {
|
|
187
|
+
if (confirmResolve) { var resolve = confirmResolve; confirmResolve = null; resolve(false); }
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
var promptDialogEl = document.getElementById("prompt-dialog");
|
|
191
|
+
var promptTitleEl = document.getElementById("prompt-title");
|
|
192
|
+
var promptMessageEl = document.getElementById("prompt-message");
|
|
193
|
+
var promptLabelEl = document.getElementById("prompt-label");
|
|
194
|
+
var promptInput = document.getElementById("prompt-input");
|
|
195
|
+
var promptErrorEl = document.getElementById("prompt-error");
|
|
196
|
+
var promptCancelBtn = document.getElementById("prompt-cancel");
|
|
197
|
+
var promptConfirmBtn = document.getElementById("prompt-confirm");
|
|
198
|
+
var promptCloseBtn = document.getElementById("prompt-close");
|
|
199
|
+
var promptResolve = null;
|
|
200
|
+
var promptValidate = null;
|
|
201
|
+
|
|
202
|
+
function closePromptDialog(result) {
|
|
203
|
+
var resolve = promptResolve;
|
|
204
|
+
promptResolve = null;
|
|
205
|
+
promptValidate = null;
|
|
206
|
+
promptDialogEl.close();
|
|
207
|
+
if (resolve) resolve(result);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function promptDialog(opts) {
|
|
211
|
+
opts = opts || {};
|
|
212
|
+
return new Promise(function (resolve) {
|
|
213
|
+
promptResolve = resolve;
|
|
214
|
+
promptValidate = typeof opts.validate === "function" ? opts.validate : null;
|
|
215
|
+
promptTitleEl.textContent = opts.title || "";
|
|
216
|
+
promptMessageEl.textContent = opts.message || "";
|
|
217
|
+
promptMessageEl.hidden = !opts.message;
|
|
218
|
+
promptLabelEl.textContent = opts.label || "";
|
|
219
|
+
promptLabelEl.hidden = !opts.label;
|
|
220
|
+
promptInput.placeholder = opts.placeholder || "";
|
|
221
|
+
promptInput.value = opts.value || "";
|
|
222
|
+
promptConfirmBtn.textContent = opts.confirmLabel || "OK";
|
|
223
|
+
promptErrorEl.hidden = true;
|
|
224
|
+
promptErrorEl.textContent = "";
|
|
225
|
+
promptDialogEl.showModal();
|
|
226
|
+
setTimeout(function () { promptInput.focus(); promptInput.select(); }, 0);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function submitPromptDialog() {
|
|
231
|
+
var value = promptInput.value;
|
|
232
|
+
if (promptValidate) {
|
|
233
|
+
var err = promptValidate(value);
|
|
234
|
+
if (err) {
|
|
235
|
+
promptErrorEl.textContent = err;
|
|
236
|
+
promptErrorEl.hidden = false;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
closePromptDialog(value);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
promptCancelBtn.addEventListener("click", function () { closePromptDialog(null); });
|
|
244
|
+
promptCloseBtn.addEventListener("click", function () { closePromptDialog(null); });
|
|
245
|
+
promptConfirmBtn.addEventListener("click", submitPromptDialog);
|
|
246
|
+
promptInput.addEventListener("keydown", function (e) {
|
|
247
|
+
if (e.key === "Enter") { e.preventDefault(); submitPromptDialog(); }
|
|
248
|
+
});
|
|
249
|
+
promptDialogEl.addEventListener("click", function (e) {
|
|
250
|
+
if (e.target === promptDialogEl) closePromptDialog(null);
|
|
251
|
+
});
|
|
252
|
+
promptDialogEl.addEventListener("close", function () {
|
|
253
|
+
if (promptResolve) { var resolve = promptResolve; promptResolve = null; resolve(null); }
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
`;
|
|
257
|
+
exports.jsPrefs = ` // ---------- preferences (per browser) ----------
|
|
258
|
+
|
|
259
|
+
var PREF_DEFAULTS = { refresh: 5000, start: "overview", confirm: true, insecure: false, cors: false };
|
|
260
|
+
var prefs = (function () {
|
|
261
|
+
var out = {};
|
|
262
|
+
var saved = {};
|
|
263
|
+
try { saved = JSON.parse(localStorage.getItem("locadot.prefs") || "{}") || {}; } catch (e) {}
|
|
264
|
+
Object.keys(PREF_DEFAULTS).forEach(function (k) { out[k] = saved[k] !== undefined ? saved[k] : PREF_DEFAULTS[k]; });
|
|
265
|
+
return out;
|
|
266
|
+
})();
|
|
267
|
+
function savePrefs() {
|
|
268
|
+
try { localStorage.setItem("locadot.prefs", JSON.stringify(prefs)); } catch (e) {}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function applyHostDefaults() {
|
|
272
|
+
addInsecureInput.checked = !!prefs.insecure;
|
|
273
|
+
addCorsInput.checked = !!prefs.cors;
|
|
274
|
+
}
|
|
275
|
+
applyHostDefaults();
|
|
276
|
+
|
|
277
|
+
function setSwitch(el, on) {
|
|
278
|
+
el.setAttribute("aria-checked", String(on));
|
|
279
|
+
el.classList.toggle("on", on);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
`;
|
|
283
|
+
exports.jsRouting = ` // ---------- sidebar + routing ----------
|
|
284
|
+
// Routes are #/<view> or #/<view>/<tab>. A view with a tab group registered via
|
|
285
|
+
// initTabs() (core) gets the tab selected from the second segment; an unknown or
|
|
286
|
+
// missing tab falls back to the group's first tab. Legacy routes redirect.
|
|
287
|
+
|
|
288
|
+
var VIEWS = ["overview", "hosts", "sharing", "remote", "machines", "logs", "settings"];
|
|
289
|
+
var ROUTE_REDIRECTS = { sharing: ["hosts", "shared"], machines: ["remote", "machines"] };
|
|
290
|
+
var TITLES = { overview: "Overview", hosts: "Hosts", sharing: "Hosts", remote: "Remote", machines: "Remote", logs: "Logs", settings: "Settings" };
|
|
291
|
+
var rootEl = document.documentElement;
|
|
292
|
+
var sidebarEl = document.getElementById("sidebar");
|
|
293
|
+
var collapseBtn = document.getElementById("sb-collapse");
|
|
294
|
+
var menuBtn = document.getElementById("sb-menu");
|
|
295
|
+
var backdropEl = document.getElementById("sb-backdrop");
|
|
296
|
+
var tbTitleEl = document.getElementById("tb-title");
|
|
297
|
+
var navLinks = Array.prototype.slice.call(sidebarEl.querySelectorAll("a.sb-link"));
|
|
298
|
+
var viewEls = {};
|
|
299
|
+
Array.prototype.forEach.call(document.querySelectorAll("section.view"), function (el) { viewEls[el.getAttribute("data-view")] = el; });
|
|
300
|
+
var drawerMq = window.matchMedia("(max-width: 640px)");
|
|
301
|
+
var currentView = null;
|
|
302
|
+
|
|
303
|
+
function isCollapsed() { return rootEl.getAttribute("data-sidebar") === "collapsed"; }
|
|
304
|
+
function setCollapsed(collapsed, persist) {
|
|
305
|
+
if (collapsed) rootEl.setAttribute("data-sidebar", "collapsed");
|
|
306
|
+
else rootEl.removeAttribute("data-sidebar");
|
|
307
|
+
if (persist) { try { localStorage.setItem("locadot.sidebar", collapsed ? "collapsed" : "expanded"); } catch (e) {} }
|
|
308
|
+
var label = collapsed ? "Expand sidebar" : "Collapse sidebar";
|
|
309
|
+
collapseBtn.setAttribute("aria-expanded", String(!collapsed));
|
|
310
|
+
collapseBtn.setAttribute("aria-label", label);
|
|
311
|
+
collapseBtn.setAttribute("data-tip", label);
|
|
312
|
+
collapseBtn.querySelector(".sb-label").textContent = collapsed ? "Expand" : "Collapse";
|
|
313
|
+
setSwitch(prefSidebar, collapsed);
|
|
314
|
+
hideTooltip();
|
|
315
|
+
}
|
|
316
|
+
collapseBtn.addEventListener("click", function () { setCollapsed(!isCollapsed(), true); });
|
|
317
|
+
|
|
318
|
+
// Legacy slide-in drawer; the menu button is hidden now that phones get a bottom tab bar.
|
|
319
|
+
function setDrawer(open) {
|
|
320
|
+
document.body.classList.toggle("sb-open", open);
|
|
321
|
+
backdropEl.hidden = !open;
|
|
322
|
+
menuBtn.setAttribute("aria-expanded", String(open));
|
|
323
|
+
menuBtn.setAttribute("aria-label", open ? "Close navigation" : "Open navigation");
|
|
324
|
+
if (open && navLinks[0]) navLinks[0].focus();
|
|
325
|
+
}
|
|
326
|
+
menuBtn.addEventListener("click", function () { setDrawer(!document.body.classList.contains("sb-open")); });
|
|
327
|
+
backdropEl.addEventListener("click", function () { setDrawer(false); });
|
|
328
|
+
document.addEventListener("keydown", function (e) {
|
|
329
|
+
if (e.key === "Escape" && document.body.classList.contains("sb-open")) { setDrawer(false); menuBtn.focus(); }
|
|
330
|
+
});
|
|
331
|
+
if (drawerMq.addEventListener) drawerMq.addEventListener("change", function () { if (!drawerMq.matches) setDrawer(false); });
|
|
332
|
+
|
|
333
|
+
// Sidebar tooltips only make sense on the icon rail.
|
|
334
|
+
tooltipGuards.push(function (el) {
|
|
335
|
+
if (!sidebarEl.contains(el)) return true;
|
|
336
|
+
return isCollapsed() && !drawerMq.matches;
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
function resolveRoute(view, tab) {
|
|
340
|
+
var redirected = false;
|
|
341
|
+
if (ROUTE_REDIRECTS[view]) { tab = tab || ROUTE_REDIRECTS[view][1]; view = ROUTE_REDIRECTS[view][0]; redirected = true; }
|
|
342
|
+
if (VIEWS.indexOf(view) === -1) return null;
|
|
343
|
+
return { view: view, tab: tab || null, redirected: redirected };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function routeFromHash() {
|
|
347
|
+
var parts = (location.hash || "").replace(/^#\\/?/, "").split("/");
|
|
348
|
+
return resolveRoute(decodeURIComponent(parts[0] || ""), parts[1] ? decodeURIComponent(parts[1]) : null);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function viewFromHash() {
|
|
352
|
+
var r = routeFromHash();
|
|
353
|
+
return r ? r.view : null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function showView(name, tab) {
|
|
357
|
+
var r = resolveRoute(name, tab) || { view: "overview", tab: null };
|
|
358
|
+
name = r.view;
|
|
359
|
+
tab = r.tab;
|
|
360
|
+
var changed = currentView !== name;
|
|
361
|
+
currentView = name;
|
|
362
|
+
closeMenu(false);
|
|
363
|
+
VIEWS.forEach(function (v) { if (viewEls[v]) viewEls[v].hidden = v !== name; });
|
|
364
|
+
navLinks.forEach(function (a) {
|
|
365
|
+
if (a.getAttribute("data-view") === name) a.setAttribute("aria-current", "page");
|
|
366
|
+
else a.removeAttribute("aria-current");
|
|
367
|
+
});
|
|
368
|
+
document.title = TITLES[name] + " · locadot";
|
|
369
|
+
if (tbTitleEl) tbTitleEl.textContent = TITLES[name];
|
|
370
|
+
try { localStorage.setItem("locadot.lastView", name); } catch (e) {}
|
|
371
|
+
if (drawerMq.matches) setDrawer(false);
|
|
372
|
+
var group = tabGroups[name];
|
|
373
|
+
if (group) {
|
|
374
|
+
var picked = group.select(tab, { silent: changed });
|
|
375
|
+
if (tab && picked !== tab) replaceHash(name, picked);
|
|
376
|
+
}
|
|
377
|
+
if (r.redirected) replaceHash(name, group ? group.current() : tab);
|
|
378
|
+
if (changed) {
|
|
379
|
+
window.scrollTo(0, 0);
|
|
380
|
+
onViewEnter(name);
|
|
381
|
+
if (group && group.opts.onChange) group.opts.onChange(group.current(), null);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function replaceHash(view, tab) {
|
|
386
|
+
var h = "#/" + view + (tab ? "/" + encodeURIComponent(tab) : "");
|
|
387
|
+
if (location.hash !== h) { try { history.replaceState(null, "", h); } catch (e) {} }
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function showRoute() {
|
|
391
|
+
var r = routeFromHash();
|
|
392
|
+
if (r && r.redirected) replaceHash(r.view, r.tab);
|
|
393
|
+
if (r) showView(r.view, r.tab);
|
|
394
|
+
else showView("overview");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function onViewEnter(name) {
|
|
398
|
+
if (name === "logs") loadLogs();
|
|
399
|
+
if (name === "overview") { renderOverview(); loadOverviewLogs(); }
|
|
400
|
+
if (name === "hosts" || name === "sharing") renderShareList();
|
|
401
|
+
if (name === "settings") loadSettings();
|
|
402
|
+
if (name === "remote" || name === "machines") refreshHubAndRemotes();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
window.addEventListener("hashchange", showRoute);
|
|
406
|
+
|
|
407
|
+
`;
|
|
408
|
+
exports.shellTop = `<body class="boot">
|
|
409
|
+
<nav id="sidebar" class="sidebar" aria-label="Main">
|
|
410
|
+
<div class="sb-head">
|
|
411
|
+
<a class="sb-brand" href="#/overview" aria-label="locadot overview"><span class="logo" aria-hidden="true"></span><h1 class="wordmark sb-label">locadot</h1></a>
|
|
412
|
+
<span id="version" class="sb-version sb-label"></span>
|
|
413
|
+
</div>
|
|
414
|
+
<div class="sb-links">
|
|
415
|
+
<div class="sb-group">
|
|
416
|
+
<a class="sb-link" href="#/overview" data-view="overview" aria-label="Overview" data-tip="Overview" data-tip-pos="right"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="3" width="7" height="9" rx="1.5"/><rect x="14" y="3" width="7" height="5" rx="1.5"/><rect x="14" y="12" width="7" height="9" rx="1.5"/><rect x="3" y="16" width="7" height="5" rx="1.5"/></svg><span class="sb-label">Overview</span></a>
|
|
417
|
+
<a class="sb-link" href="#/hosts" data-view="hosts" aria-label="Hosts" data-tip="Hosts" data-tip-pos="right"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="4" width="18" height="6" rx="1.5"/><rect x="3" y="14" width="18" height="6" rx="1.5"/><path d="M7 7h.01M7 17h.01"/></svg><span class="sb-label">Hosts</span><span class="sb-badges"><span id="sb-shares-count" class="sb-count sb-count-accent" title="Shared publicly" hidden></span><span id="sb-hosts-count" class="sb-count" hidden></span></span></a>
|
|
418
|
+
<a class="sb-link" href="#/remote" data-view="remote" aria-label="Remote" data-tip="Remote" data-tip-pos="right"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5a10 10 0 0 1 14 0M8.5 16a5 5 0 0 1 7 0M12 19.5h.01"/></svg><span class="sb-label">Remote</span><span class="sb-badges"><span id="sb-remotes-count" class="sb-count" title="Connected machines" hidden></span><span id="sb-hub-dot" class="dot sb-dot" hidden></span></span></a>
|
|
419
|
+
</div>
|
|
420
|
+
<div class="sb-section sb-label" role="presentation">System</div>
|
|
421
|
+
<div class="sb-group">
|
|
422
|
+
<a class="sb-link" href="#/logs" data-view="logs" aria-label="Logs" data-tip="Logs" data-tip-pos="right"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h10"/></svg><span class="sb-label">Logs</span></a>
|
|
423
|
+
<a class="sb-link" href="#/settings" data-view="settings" aria-label="Settings" data-tip="Settings" data-tip-pos="right"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg><span class="sb-label">Settings</span></a>
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
<div class="sb-foot">
|
|
427
|
+
<button type="button" id="sb-signout" class="sb-link sb-signout" aria-label="Sign out" data-tip="Sign out" data-tip-pos="right" hidden><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/></svg><span class="sb-label">Sign out</span></button>
|
|
428
|
+
<button type="button" id="sb-collapse" class="sb-link sb-collapse" aria-controls="sidebar" aria-expanded="true" aria-label="Collapse sidebar" data-tip="Expand sidebar" data-tip-pos="right"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M9 4v16M15 10l-2 2 2 2"/></svg><span class="sb-label">Collapse</span></button>
|
|
429
|
+
</div>
|
|
430
|
+
</nav>
|
|
431
|
+
<div id="sb-backdrop" class="sb-backdrop" hidden></div>
|
|
432
|
+
<div class="shell">
|
|
433
|
+
<header>
|
|
434
|
+
<div class="topbar">
|
|
435
|
+
<div class="brand">
|
|
436
|
+
<button type="button" id="sb-menu" class="btn btn-ghost btn-icon sb-menu" aria-controls="sidebar" aria-expanded="false" aria-label="Open navigation"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h16"/></svg></button>
|
|
437
|
+
<a class="brand-mobile logo" href="#/overview" aria-label="locadot overview"></a>
|
|
438
|
+
<span class="wordmark brand-mobile">locadot</span>
|
|
439
|
+
<div class="crumbs"><span class="crumb-root">locadot</span><span class="crumb-sep" aria-hidden="true">/</span><span id="tb-title" class="crumb-current">Overview</span></div>
|
|
440
|
+
</div>
|
|
441
|
+
<div class="header-meta">
|
|
442
|
+
<span id="updated"></span>
|
|
443
|
+
<button type="button" id="status-btn" class="status-pill" aria-haspopup="menu" aria-expanded="false" aria-label="Proxy status and details">
|
|
444
|
+
<span id="live-dot" class="live-dot"></span><span id="live-label" class="live-label">live</span>
|
|
445
|
+
<span class="status-extra"><span class="status-sep" aria-hidden="true"></span><span id="uptime"></span><span class="status-sep status-pid-sep" aria-hidden="true"></span><span id="pid"></span></span>
|
|
446
|
+
<svg class="status-caret" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
|
|
447
|
+
</button>
|
|
448
|
+
<button type="button" id="theme-toggle" class="btn btn-ghost theme-toggle" data-mode="system" title="Theme: system" aria-label="Theme: system (click to change)">
|
|
449
|
+
<svg class="i-system" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none" viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="4" width="18" height="12" rx="2"/><path d="M8 20h8M12 16v4"/></svg>
|
|
450
|
+
<svg class="i-light" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
|
|
451
|
+
<svg class="i-dark" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none" viewBox="0 0 24 24" aria-hidden="true"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
|
|
452
|
+
</button>
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
455
|
+
</header>
|
|
456
|
+
<main>
|
|
457
|
+
<div id="stopped-banner" class="banner" role="status" hidden></div>
|
|
458
|
+
<div id="load-error-banner" class="banner" role="alert" hidden>
|
|
459
|
+
<div class="banner-row">
|
|
460
|
+
<span id="load-error-text"></span>
|
|
461
|
+
<button type="button" id="load-error-retry" class="btn btn-sm">Retry</button>
|
|
462
|
+
</div>
|
|
463
|
+
</div>
|
|
464
|
+
|
|
465
|
+
`;
|
|
466
|
+
exports.shellBottom = `</main>
|
|
467
|
+
<footer>
|
|
468
|
+
<a href="https://github.com/avinashid/locadot" target="_blank" rel="noopener noreferrer">github.com/avinashid/locadot</a>
|
|
469
|
+
</footer>
|
|
470
|
+
</div>
|
|
471
|
+
`;
|
|
472
|
+
exports.dialogsMarkup = `<dialog id="confirm-dialog">
|
|
473
|
+
<div class="modal-head">
|
|
474
|
+
<div>
|
|
475
|
+
<h3 class="modal-title" id="confirm-title"></h3>
|
|
476
|
+
</div>
|
|
477
|
+
<button type="button" id="confirm-close" class="btn btn-ghost modal-close" aria-label="Close"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
|
|
478
|
+
</div>
|
|
479
|
+
<div class="modal-body">
|
|
480
|
+
<p class="modal-desc" id="confirm-message"></p>
|
|
481
|
+
</div>
|
|
482
|
+
<div class="modal-foot">
|
|
483
|
+
<button type="button" id="confirm-cancel" class="btn">Cancel</button>
|
|
484
|
+
<button type="button" id="confirm-ok" class="btn btn-primary">OK</button>
|
|
485
|
+
</div>
|
|
486
|
+
</dialog>
|
|
487
|
+
<dialog id="prompt-dialog">
|
|
488
|
+
<div class="modal-head">
|
|
489
|
+
<div>
|
|
490
|
+
<h3 class="modal-title" id="prompt-title"></h3>
|
|
491
|
+
</div>
|
|
492
|
+
<button type="button" id="prompt-close" class="btn btn-ghost modal-close" aria-label="Close"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
|
|
493
|
+
</div>
|
|
494
|
+
<div class="modal-body">
|
|
495
|
+
<p class="modal-desc" id="prompt-message"></p>
|
|
496
|
+
<div class="field">
|
|
497
|
+
<label id="prompt-label" for="prompt-input"></label>
|
|
498
|
+
<input type="text" id="prompt-input" class="input" autocomplete="off">
|
|
499
|
+
</div>
|
|
500
|
+
<div id="prompt-error" class="field-error" role="alert" hidden></div>
|
|
501
|
+
</div>
|
|
502
|
+
<div class="modal-foot">
|
|
503
|
+
<button type="button" id="prompt-cancel" class="btn">Cancel</button>
|
|
504
|
+
<button type="button" id="prompt-confirm" class="btn btn-primary">OK</button>
|
|
505
|
+
</div>
|
|
506
|
+
</dialog>
|
|
507
|
+
`;
|
|
508
|
+
exports.toastMarkup = `<div id="toast-container" class="toast-container" aria-live="polite"></div>
|
|
509
|
+
`;
|