locadot 2.4.0 → 2.6.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 +21 -0
- package/README.md +5 -1
- package/dist/cli/commands/hosts.js +20 -1
- package/dist/cli/commands/proxy.js +15 -4
- package/dist/dashboard/api.js +39 -28
- package/dist/dashboard/index.js +1 -0
- package/dist/dashboard/page.js +20 -4724
- package/dist/dashboard/ui/core.js +803 -0
- package/dist/dashboard/ui/hosts.js +1543 -0
- package/dist/dashboard/ui/remote.js +958 -0
- package/dist/dashboard/ui/shell.js +509 -0
- package/dist/dashboard/ui/system.js +781 -0
- package/dist/dashboard/ui/theme.js +1710 -0
- package/dist/index.js +5 -0
- package/dist/lib/config.js +46 -0
- package/dist/lib/hosts.js +13 -1
- package/dist/proxy/https-redirect.js +40 -0
- package/dist/proxy/router.js +5 -0
- package/dist/server/index.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/** Overview, logs and settings. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.css = exports.settingsView = exports.logsView = exports.overviewView = exports.jsUiPassword = exports.jsSettings = exports.jsLogsPage = exports.jsOverview = exports.jsLogs = void 0;
|
|
5
|
+
exports.jsLogs = ` // ---------- logs ----------
|
|
6
|
+
|
|
7
|
+
var LEVELS = { error: 0, warn: 1, info: 2 };
|
|
8
|
+
|
|
9
|
+
function lineLevel(line) {
|
|
10
|
+
var m = /\\[(error|warn|info|debug|verbose|silly)\\]/i.exec(line);
|
|
11
|
+
return m ? m[1].toLowerCase() : "info";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Renders lines as individual, level-coloured rows instead of one big <pre>
|
|
15
|
+
// blob, so long lines wrap nicely and errors/warnings stand out.
|
|
16
|
+
function renderLogLines(container, lines, emptyText) {
|
|
17
|
+
clear(container);
|
|
18
|
+
if (!lines.length) {
|
|
19
|
+
var empty = document.createElement("div");
|
|
20
|
+
empty.className = "log-line log-line-empty";
|
|
21
|
+
empty.textContent = emptyText;
|
|
22
|
+
container.appendChild(empty);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
lines.forEach(function (line) {
|
|
26
|
+
var row = document.createElement("div");
|
|
27
|
+
row.className = "log-line log-line-" + lineLevel(line);
|
|
28
|
+
row.textContent = line;
|
|
29
|
+
container.appendChild(row);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
logsRefreshBtn.addEventListener("click", loadLogs);
|
|
34
|
+
logsClearBtn.addEventListener("click", function () {
|
|
35
|
+
confirmDialog({ title: "Clear logs?", message: "This can't be undone.", confirmLabel: "Clear logs", danger: true }).then(function (ok) {
|
|
36
|
+
if (!ok) return;
|
|
37
|
+
apiFetch("/api/logs/clear", { method: "POST" })
|
|
38
|
+
.then(function () {
|
|
39
|
+
showToast("Logs cleared", "success");
|
|
40
|
+
return loadLogs();
|
|
41
|
+
})
|
|
42
|
+
.catch(function (err) { apiError(err, "Couldn't clear logs"); });
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
cliAddCopy.addEventListener("click", function () { copyText(cliAddPre.textContent, cliAddCopy); });
|
|
47
|
+
cliCurlCopy.addEventListener("click", function () { copyText(cliCurlPre.textContent, cliCurlCopy); });
|
|
48
|
+
cliTunnelCopy.addEventListener("click", function () { copyText(cliTunnelPre.textContent, cliTunnelCopy); });
|
|
49
|
+
|
|
50
|
+
`;
|
|
51
|
+
exports.jsOverview = ` // ---------- overview ----------
|
|
52
|
+
|
|
53
|
+
var ovHostsEl = document.getElementById("ov-hosts");
|
|
54
|
+
var ovHostsSubEl = document.getElementById("ov-hosts-sub");
|
|
55
|
+
var ovDownEl = document.getElementById("ov-down");
|
|
56
|
+
var ovHitsEl = document.getElementById("ov-hits");
|
|
57
|
+
var ovHitsSubEl = document.getElementById("ov-hits-sub");
|
|
58
|
+
var ovSharesEl = document.getElementById("ov-shares");
|
|
59
|
+
var ovSharesSubEl = document.getElementById("ov-shares-sub");
|
|
60
|
+
var ovHubEl = document.getElementById("ov-hub");
|
|
61
|
+
var ovHubSubEl = document.getElementById("ov-hub-sub");
|
|
62
|
+
var ovRemotesEl = document.getElementById("ov-remotes");
|
|
63
|
+
var ovRemotesSubEl = document.getElementById("ov-remotes-sub");
|
|
64
|
+
var ovHostList = document.getElementById("ov-host-list");
|
|
65
|
+
var ovHostEmpty = document.getElementById("ov-host-empty");
|
|
66
|
+
var ovChecks = document.getElementById("ov-checks");
|
|
67
|
+
var ovLogs = document.getElementById("ov-logs");
|
|
68
|
+
var sbSharesCountEl = document.getElementById("sb-shares-count");
|
|
69
|
+
var sbHubDot = document.getElementById("sb-hub-dot");
|
|
70
|
+
|
|
71
|
+
// Plays once on load; the tiles themselves are static, so later polls just
|
|
72
|
+
// update their text and don't need to re-animate.
|
|
73
|
+
Array.prototype.forEach.call(document.querySelectorAll('[data-view="overview"] .tile'), function (el, i) { animateIn(el, i); });
|
|
74
|
+
|
|
75
|
+
function isShared(row) {
|
|
76
|
+
var t = tunnelInfo(row);
|
|
77
|
+
return t.enabled || t.status === "up" || t.status === "starting";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function ovRow(title, sub, side, dotClass, href) {
|
|
81
|
+
var row = document.createElement(href ? "a" : "div");
|
|
82
|
+
row.className = "ov-row";
|
|
83
|
+
if (href) { row.href = href; row.style.color = "inherit"; row.style.textDecoration = "none"; }
|
|
84
|
+
var dot = document.createElement("span");
|
|
85
|
+
dot.className = "dot" + (dotClass ? " " + dotClass : "");
|
|
86
|
+
row.appendChild(dot);
|
|
87
|
+
var main = document.createElement("div");
|
|
88
|
+
main.className = "ov-main";
|
|
89
|
+
var t = document.createElement("span");
|
|
90
|
+
t.className = "ov-title";
|
|
91
|
+
t.textContent = title;
|
|
92
|
+
main.appendChild(t);
|
|
93
|
+
if (sub) {
|
|
94
|
+
var s = document.createElement("span");
|
|
95
|
+
s.className = "ov-sub";
|
|
96
|
+
s.textContent = sub;
|
|
97
|
+
main.appendChild(s);
|
|
98
|
+
}
|
|
99
|
+
row.appendChild(main);
|
|
100
|
+
var sideEl = document.createElement("div");
|
|
101
|
+
sideEl.className = "ov-side";
|
|
102
|
+
if (typeof side === "string") sideEl.textContent = side;
|
|
103
|
+
else if (side) sideEl.appendChild(side);
|
|
104
|
+
row.appendChild(sideEl);
|
|
105
|
+
return row;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function fmtNum(n) { return n >= 10000 ? Math.round(n / 1000) + "k" : String(n); }
|
|
109
|
+
|
|
110
|
+
function renderOverview() {
|
|
111
|
+
var hosts = lastHosts || [];
|
|
112
|
+
var up = 0, hits = 0, errors = 0, shared = 0, remoteCount = 0;
|
|
113
|
+
hosts.forEach(function (h) {
|
|
114
|
+
if (h.probe && h.probe.up) up++;
|
|
115
|
+
if (h.stats) { hits += h.stats.hits || 0; errors += h.stats.errors || 0; }
|
|
116
|
+
if (isShared(h)) shared++;
|
|
117
|
+
if (h.remote) remoteCount++;
|
|
118
|
+
});
|
|
119
|
+
var down = hosts.length - up;
|
|
120
|
+
if (lastStatus) {
|
|
121
|
+
ovHostsEl.textContent = String(hosts.length);
|
|
122
|
+
ovHostsSubEl.textContent = up + " up" + (remoteCount ? " · " + remoteCount + " from other machines" : "");
|
|
123
|
+
ovDownEl.textContent = String(down);
|
|
124
|
+
ovDownEl.className = "tile-big" + (down ? " bad" : " ok");
|
|
125
|
+
ovHitsEl.textContent = fmtNum(hits);
|
|
126
|
+
ovHitsSubEl.textContent = errors ? errors + " errors since start" : "since the proxy started";
|
|
127
|
+
ovSharesEl.textContent = String(shared);
|
|
128
|
+
ovSharesSubEl.textContent = cloudflaredInstalled ? (shared ? "public right now" : "nothing public") : "cloudflared not installed";
|
|
129
|
+
}
|
|
130
|
+
sbSharesCountEl.textContent = String(shared);
|
|
131
|
+
sbSharesCountEl.hidden = shared === 0;
|
|
132
|
+
|
|
133
|
+
var hub = lastHub;
|
|
134
|
+
var hubLabels = { off: "Off", starting: "Starting", login: "Login needed", up: "On", error: "Error" };
|
|
135
|
+
if (hub) {
|
|
136
|
+
ovHubEl.textContent = hubLabels[hub.status] || hub.status;
|
|
137
|
+
ovHubEl.className = "tile-big tile-word" + (hub.status === "up" ? " ok" : hub.status === "error" ? " bad" : hub.status === "off" ? "" : " warn");
|
|
138
|
+
ovHubSubEl.textContent = hub.url ? hub.url.replace(/^https?:\\/\\//, "") : (hub.status === "off" ? "not shared" : "");
|
|
139
|
+
sbHubDot.hidden = hub.status === "off";
|
|
140
|
+
sbHubDot.className = "dot sb-dot " + (hub.status === "up" ? "up" : hub.status === "error" ? "down" : "warn");
|
|
141
|
+
}
|
|
142
|
+
var remotes = lastRemotes || [];
|
|
143
|
+
ovRemotesEl.textContent = String(remotes.length);
|
|
144
|
+
ovRemotesSubEl.textContent = remotes.length ? remotes.map(function (r) { return r.name; }).join(", ") : "none";
|
|
145
|
+
|
|
146
|
+
clear(ovHostList);
|
|
147
|
+
ovHostEmpty.hidden = hosts.length !== 0 || !lastStatus;
|
|
148
|
+
var sorted = hosts.slice().sort(function (a, b) {
|
|
149
|
+
var au = a.probe && a.probe.up ? 1 : 0, bu = b.probe && b.probe.up ? 1 : 0;
|
|
150
|
+
return au - bu || a.host.localeCompare(b.host);
|
|
151
|
+
});
|
|
152
|
+
sorted.slice(0, 6).forEach(function (h, i) {
|
|
153
|
+
var p = h.probe || {};
|
|
154
|
+
var side = p.up ? ((p.status || "") + (p.ms !== undefined ? " · " + p.ms + "ms" : "")) : (p.error || "down");
|
|
155
|
+
var sub = h.remote ? "via " + h.remote.name : h.target;
|
|
156
|
+
var row = ovRow(h.host, sub, side, p.up ? "up" : "down", "#/hosts");
|
|
157
|
+
ovHostList.appendChild(row);
|
|
158
|
+
animateIn(row, i);
|
|
159
|
+
});
|
|
160
|
+
if (sorted.length > 6) {
|
|
161
|
+
var more = document.createElement("a");
|
|
162
|
+
more.className = "ov-more";
|
|
163
|
+
more.href = "#/hosts";
|
|
164
|
+
more.textContent = "View all " + sorted.length + " hosts";
|
|
165
|
+
ovHostList.appendChild(more);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
clear(ovChecks);
|
|
169
|
+
if (lastStatus) {
|
|
170
|
+
var sys = lastStatus.system, proxy = lastStatus.proxy;
|
|
171
|
+
var checkRows = [
|
|
172
|
+
ovRow("Running", "pid " + proxy.pid + " · uptime " + fmtUptime(lastStatus.uptimeSec), "http :" + proxy.httpPort + " · https :" + proxy.httpsPort, "up"),
|
|
173
|
+
ovRow("HTTPS certificates", sys.caTrusted === true ? "local CA is trusted" : "browsers will warn until the CA is trusted", sys.caTrusted === true ? "Trusted" : "Trust CA", sys.caTrusted === true ? "up" : "warn", "#/settings/security"),
|
|
174
|
+
ovRow("Start at boot", sys.startup.method ? "via " + sys.startup.method : "", sys.startup.enabled ? "Enabled" : "Enable", sys.startup.enabled ? "up" : "", "#/settings/general")
|
|
175
|
+
];
|
|
176
|
+
var cf = sys.cloudflared || {};
|
|
177
|
+
checkRows.push(ovRow("cloudflared", cf.installed ? (cf.version || cf.path || "") : "needed for public sharing and remote access", cf.installed ? "Installed" : "Install", cf.installed ? "up" : "warn", "#/sharing"));
|
|
178
|
+
checkRows.forEach(function (row, i) { ovChecks.appendChild(row); animateIn(row, i); });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function loadOverviewLogs() {
|
|
183
|
+
return fetch("/api/logs?lines=8").then(parseJsonOrThrow).then(function (data) {
|
|
184
|
+
var lines = (data && data.lines) || [];
|
|
185
|
+
renderLogLines(ovLogs, lines, "No log lines yet.");
|
|
186
|
+
ovLogs.scrollTop = ovLogs.scrollHeight;
|
|
187
|
+
}).catch(function () {});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
`;
|
|
191
|
+
exports.jsLogsPage = ` // ---------- logs page ----------
|
|
192
|
+
|
|
193
|
+
var logsFilter = document.getElementById("logs-filter");
|
|
194
|
+
var logsLevel = document.getElementById("logs-level");
|
|
195
|
+
var logsLines = document.getElementById("logs-lines");
|
|
196
|
+
var logsAuto = document.getElementById("logs-auto");
|
|
197
|
+
var logsCopy = document.getElementById("logs-copy");
|
|
198
|
+
var logsDownload = document.getElementById("logs-download");
|
|
199
|
+
var logsMeta = document.getElementById("logs-meta");
|
|
200
|
+
var lastLogLines = [];
|
|
201
|
+
|
|
202
|
+
function filteredLogLines() {
|
|
203
|
+
var q = (logsFilter.value || "").trim().toLowerCase();
|
|
204
|
+
var lvl = logsLevel.value;
|
|
205
|
+
return lastLogLines.filter(function (line) {
|
|
206
|
+
if (q && line.toLowerCase().indexOf(q) === -1) return false;
|
|
207
|
+
if (lvl) {
|
|
208
|
+
var l = LEVELS[lineLevel(line)];
|
|
209
|
+
if (l === undefined || l > LEVELS[lvl]) return false;
|
|
210
|
+
}
|
|
211
|
+
return true;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function renderLogs() {
|
|
216
|
+
var shown = filteredLogLines();
|
|
217
|
+
var atBottom = logsBox.scrollHeight - logsBox.scrollTop - logsBox.clientHeight < 40;
|
|
218
|
+
renderLogLines(logsBox, shown, lastLogLines.length ? "No lines match." : "No log lines yet.");
|
|
219
|
+
if (atBottom) logsBox.scrollTop = logsBox.scrollHeight;
|
|
220
|
+
logsMeta.textContent = (shown.length === lastLogLines.length ? lastLogLines.length + " lines" : shown.length + " of " + lastLogLines.length + " lines") + " · updated " + new Date().toLocaleTimeString();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function loadLogs() {
|
|
224
|
+
return fetch("/api/logs?lines=" + encodeURIComponent(logsLines.value)).then(parseJsonOrThrow).then(function (data) {
|
|
225
|
+
lastLogLines = (data && data.lines) || [];
|
|
226
|
+
renderLogs();
|
|
227
|
+
}).catch(function (err) { apiError(err, "Couldn't load logs"); });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
logsFilter.addEventListener("input", renderLogs);
|
|
231
|
+
logsLevel.addEventListener("change", renderLogs);
|
|
232
|
+
logsLines.addEventListener("change", loadLogs);
|
|
233
|
+
logsCopy.addEventListener("click", function () { copyText(filteredLogLines().join("\\n"), logsCopy); });
|
|
234
|
+
logsDownload.addEventListener("click", function () {
|
|
235
|
+
var blob = new Blob([lastLogLines.join("\\n") + "\\n"], { type: "text/plain" });
|
|
236
|
+
var a = document.createElement("a");
|
|
237
|
+
a.href = URL.createObjectURL(blob);
|
|
238
|
+
a.download = "locadot-" + new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-") + ".log";
|
|
239
|
+
document.body.appendChild(a);
|
|
240
|
+
a.click();
|
|
241
|
+
setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 1000);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
`;
|
|
245
|
+
exports.jsSettings = ` // ---------- settings page ----------
|
|
246
|
+
|
|
247
|
+
initTabs(document.getElementById("settings-tabs"));
|
|
248
|
+
|
|
249
|
+
var portsForm = document.getElementById("ports-form");
|
|
250
|
+
var httpPortInput = document.getElementById("set-http-port");
|
|
251
|
+
var httpsPortInput = document.getElementById("set-https-port");
|
|
252
|
+
var portsNote = document.getElementById("ports-note");
|
|
253
|
+
var portsError = document.getElementById("ports-error");
|
|
254
|
+
var portsSave = document.getElementById("ports-save");
|
|
255
|
+
var restartBtn = document.getElementById("restart-proxy-btn");
|
|
256
|
+
var restartNote = document.getElementById("restart-note");
|
|
257
|
+
var setHttpsRedirect = document.getElementById("set-https-redirect");
|
|
258
|
+
var setBind = document.getElementById("set-bind");
|
|
259
|
+
var setBindNote = document.getElementById("set-bind-note");
|
|
260
|
+
var setHome = document.getElementById("set-home");
|
|
261
|
+
var setLogLevel = document.getElementById("set-loglevel");
|
|
262
|
+
var prefTheme = document.getElementById("pref-theme");
|
|
263
|
+
var prefRefresh = document.getElementById("pref-refresh");
|
|
264
|
+
var prefStart = document.getElementById("pref-start");
|
|
265
|
+
var prefSidebar = document.getElementById("pref-sidebar");
|
|
266
|
+
var prefConfirm = document.getElementById("pref-confirm");
|
|
267
|
+
var prefInsecure = document.getElementById("pref-insecure");
|
|
268
|
+
var prefCors = document.getElementById("pref-cors");
|
|
269
|
+
var tokenValue = document.getElementById("token-value");
|
|
270
|
+
var tokenReveal = document.getElementById("token-reveal");
|
|
271
|
+
var tokenCopy = document.getElementById("token-copy");
|
|
272
|
+
var dangerClearLogs = document.getElementById("danger-clear-logs");
|
|
273
|
+
var settingsLoaded = null;
|
|
274
|
+
|
|
275
|
+
function renderSettings(s) {
|
|
276
|
+
settingsLoaded = s;
|
|
277
|
+
if (s.uiAuth) renderUiAuth(s.uiAuth.enabled);
|
|
278
|
+
if (document.activeElement !== httpPortInput) httpPortInput.value = String((s.saved && s.saved.httpPort) || s.httpPort);
|
|
279
|
+
if (document.activeElement !== httpsPortInput) httpsPortInput.value = String((s.saved && s.saved.httpsPort) || s.httpsPort);
|
|
280
|
+
var notes = ["Running on http :" + s.httpPort + " and https :" + s.httpsPort + "."];
|
|
281
|
+
if (s.env && s.env.httpPort) notes.push("LOCADOT_HTTP_PORT is set and overrides the saved HTTP port.");
|
|
282
|
+
if (s.env && s.env.httpsPort) notes.push("LOCADOT_HTTPS_PORT is set and overrides the saved HTTPS port.");
|
|
283
|
+
portsNote.textContent = notes.join(" ");
|
|
284
|
+
restartNote.hidden = !s.restartRequired;
|
|
285
|
+
setSwitch(setHttpsRedirect, !!s.httpsRedirect);
|
|
286
|
+
setBind.textContent = (s.bind || []).join(", ");
|
|
287
|
+
setBindNote.textContent = s.env && s.env.bind ? "Set by LOCADOT_BIND." : "Loopback only. Set LOCADOT_BIND to change it.";
|
|
288
|
+
setHome.textContent = s.stateDir || "";
|
|
289
|
+
setLogLevel.textContent = s.logLevel || "info";
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function loadSettings() {
|
|
293
|
+
return fetch("/api/settings").then(parseJsonOrThrow).then(renderSettings).catch(function (err) { apiError(err, "Couldn't load settings"); });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
portsForm.addEventListener("submit", function (e) {
|
|
297
|
+
e.preventDefault();
|
|
298
|
+
portsError.hidden = true;
|
|
299
|
+
var h = parseInt(httpPortInput.value, 10), hs = parseInt(httpsPortInput.value, 10);
|
|
300
|
+
var bad = function (p) { return !(p >= 1 && p <= 65535); };
|
|
301
|
+
if (bad(h) || bad(hs) || h === hs) {
|
|
302
|
+
portsError.textContent = h === hs ? "HTTP and HTTPS need different ports." : "Ports must be between 1 and 65535.";
|
|
303
|
+
portsError.hidden = false;
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
portsSave.disabled = true;
|
|
307
|
+
portsSave.classList.add("busy");
|
|
308
|
+
apiFetch("/api/settings", { method: "PUT", body: JSON.stringify({ httpPort: h, httpsPort: hs }) })
|
|
309
|
+
.then(function (s) {
|
|
310
|
+
renderSettings(s);
|
|
311
|
+
showToast(s.restartRequired ? "Ports saved. Restart to apply." : "Ports saved", s.warning ? "warn" : "success", s.warning);
|
|
312
|
+
})
|
|
313
|
+
.catch(function (err) { portsError.textContent = err.message; portsError.hidden = false; })
|
|
314
|
+
.then(function () { portsSave.disabled = false; portsSave.classList.remove("busy"); });
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
setHttpsRedirect.addEventListener("click", function () {
|
|
318
|
+
var next = !(settingsLoaded && settingsLoaded.httpsRedirect);
|
|
319
|
+
setHttpsRedirect.disabled = true;
|
|
320
|
+
apiFetch("/api/settings", { method: "PUT", body: JSON.stringify({ httpsRedirect: next }) })
|
|
321
|
+
.then(function (s) {
|
|
322
|
+
renderSettings(s);
|
|
323
|
+
showToast(next ? "Plain http now redirects to https" : "https redirect off", "success");
|
|
324
|
+
loadHosts();
|
|
325
|
+
})
|
|
326
|
+
.catch(function (err) { apiError(err, "Couldn't change the redirect"); })
|
|
327
|
+
.then(function () { setHttpsRedirect.disabled = false; });
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
restartBtn.addEventListener("click", function () {
|
|
331
|
+
confirmDialog({ title: "Restart the proxy?", message: "Hosts are unavailable for a few seconds.", confirmLabel: "Restart", danger: true }).then(function (ok) {
|
|
332
|
+
if (!ok) return;
|
|
333
|
+
var s = settingsLoaded;
|
|
334
|
+
var nextPort = s ? ((s.env && s.env.httpPort) ? s.httpPort : ((s.saved && s.saved.httpPort) || s.httpPort)) : null;
|
|
335
|
+
restartBtn.disabled = true;
|
|
336
|
+
restartBtn.classList.add("busy");
|
|
337
|
+
apiFetch("/api/proxy/restart", { method: "POST" })
|
|
338
|
+
.then(function () {
|
|
339
|
+
showToast("Restarting proxy…", "success");
|
|
340
|
+
var base = location.protocol + "//" + location.hostname;
|
|
341
|
+
var samePort = !nextPort || String(nextPort) === (location.port || "80");
|
|
342
|
+
var tries = 0;
|
|
343
|
+
var wait = setInterval(function () {
|
|
344
|
+
tries++;
|
|
345
|
+
if (!samePort) {
|
|
346
|
+
var go = function () { clearInterval(wait); setTimeout(function () { location.href = base + ":" + nextPort + "/#/settings"; }, 1500); };
|
|
347
|
+
if (tries > 20) return go();
|
|
348
|
+
fetch("/healthz", { cache: "no-store" }).catch(go);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (tries < 3) return;
|
|
352
|
+
fetch("/healthz", { cache: "no-store" }).then(function (r) {
|
|
353
|
+
if (!r.ok) return;
|
|
354
|
+
clearInterval(wait);
|
|
355
|
+
location.reload();
|
|
356
|
+
}).catch(function () {});
|
|
357
|
+
if (tries > 40) { clearInterval(wait); restartBtn.disabled = false; restartBtn.classList.remove("busy"); }
|
|
358
|
+
}, 500);
|
|
359
|
+
})
|
|
360
|
+
.catch(function (err) {
|
|
361
|
+
apiError(err, "Couldn't restart the proxy");
|
|
362
|
+
restartBtn.disabled = false;
|
|
363
|
+
restartBtn.classList.remove("busy");
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
document.getElementById("set-home-copy").addEventListener("click", function (e) { copyText(setHome.textContent, e.currentTarget); });
|
|
369
|
+
|
|
370
|
+
function syncThemeSeg(mode) {
|
|
371
|
+
Array.prototype.forEach.call(prefTheme.querySelectorAll("button"), function (b) {
|
|
372
|
+
b.setAttribute("aria-checked", String(b.getAttribute("data-value") === mode));
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
prefTheme.addEventListener("click", function (e) {
|
|
376
|
+
var b = e.target.closest("button[data-value]");
|
|
377
|
+
if (b) setTheme(b.getAttribute("data-value"));
|
|
378
|
+
});
|
|
379
|
+
syncThemeSeg(document.documentElement.getAttribute("data-theme") || "system");
|
|
380
|
+
document.getElementById("theme-toggle").addEventListener("click", function () {
|
|
381
|
+
syncThemeSeg(document.documentElement.getAttribute("data-theme") || "system");
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
prefRefresh.value = String(prefs.refresh);
|
|
385
|
+
if (prefRefresh.value !== String(prefs.refresh)) prefRefresh.value = "5000";
|
|
386
|
+
prefRefresh.addEventListener("change", function () {
|
|
387
|
+
prefs.refresh = parseInt(prefRefresh.value, 10) || 0;
|
|
388
|
+
savePrefs();
|
|
389
|
+
schedulePoll();
|
|
390
|
+
showToast(prefs.refresh ? "Refreshing every " + prefs.refresh / 1000 + "s" : "Auto-refresh paused", "success");
|
|
391
|
+
});
|
|
392
|
+
prefStart.value = prefs.start;
|
|
393
|
+
prefStart.addEventListener("change", function () { prefs.start = prefStart.value; savePrefs(); });
|
|
394
|
+
prefSidebar.addEventListener("click", function () { setCollapsed(!isCollapsed(), true); });
|
|
395
|
+
|
|
396
|
+
function bindPrefSwitch(el, key, after) {
|
|
397
|
+
setSwitch(el, !!prefs[key]);
|
|
398
|
+
el.addEventListener("click", function () {
|
|
399
|
+
prefs[key] = !prefs[key];
|
|
400
|
+
savePrefs();
|
|
401
|
+
setSwitch(el, prefs[key]);
|
|
402
|
+
if (after) after();
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
bindPrefSwitch(prefConfirm, "confirm");
|
|
406
|
+
bindPrefSwitch(prefInsecure, "insecure", applyHostDefaults);
|
|
407
|
+
bindPrefSwitch(prefCors, "cors", applyHostDefaults);
|
|
408
|
+
|
|
409
|
+
var tokenShown = false;
|
|
410
|
+
tokenReveal.addEventListener("click", function () {
|
|
411
|
+
tokenShown = !tokenShown;
|
|
412
|
+
tokenValue.textContent = tokenShown ? TOKEN : "••••••••••••••••";
|
|
413
|
+
tokenReveal.textContent = tokenShown ? "Hide" : "Show";
|
|
414
|
+
});
|
|
415
|
+
tokenCopy.addEventListener("click", function () { copyText(TOKEN, tokenCopy); });
|
|
416
|
+
dangerClearLogs.addEventListener("click", function () { logsClearBtn.click(); });
|
|
417
|
+
|
|
418
|
+
`;
|
|
419
|
+
exports.jsUiPassword = ` // ---------- dashboard password ----------
|
|
420
|
+
|
|
421
|
+
var uiAuthForm = document.getElementById("ui-auth-form");
|
|
422
|
+
var uiAuthBadge = document.getElementById("ui-auth-badge");
|
|
423
|
+
var uiAuthCurrentField = document.getElementById("ui-auth-current-field");
|
|
424
|
+
var uiAuthCurrent = document.getElementById("ui-auth-current");
|
|
425
|
+
var uiAuthNew = document.getElementById("ui-auth-new");
|
|
426
|
+
var uiAuthRepeat = document.getElementById("ui-auth-repeat");
|
|
427
|
+
var uiAuthError = document.getElementById("ui-auth-error");
|
|
428
|
+
var uiAuthSave = document.getElementById("ui-auth-save");
|
|
429
|
+
var uiAuthRemove = document.getElementById("ui-auth-remove");
|
|
430
|
+
var signOutBtn = document.getElementById("sb-signout");
|
|
431
|
+
|
|
432
|
+
function renderUiAuth(enabled) {
|
|
433
|
+
UI_AUTH = enabled;
|
|
434
|
+
uiAuthBadge.textContent = enabled ? "On" : "Off";
|
|
435
|
+
uiAuthBadge.className = "badge " + (enabled ? "badge-up" : "badge-count");
|
|
436
|
+
uiAuthCurrentField.hidden = !enabled;
|
|
437
|
+
uiAuthSave.textContent = enabled ? "Change password" : "Set password";
|
|
438
|
+
uiAuthRemove.hidden = !enabled;
|
|
439
|
+
signOutBtn.hidden = !enabled;
|
|
440
|
+
renderHubPanel(enabled && hubPanelSwitch.getAttribute("aria-checked") === "true");
|
|
441
|
+
}
|
|
442
|
+
renderUiAuth(UI_AUTH);
|
|
443
|
+
|
|
444
|
+
function uiAuthFail(message) {
|
|
445
|
+
uiAuthError.textContent = message;
|
|
446
|
+
uiAuthError.hidden = false;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function sendUiAuth(body, done) {
|
|
450
|
+
uiAuthError.hidden = true;
|
|
451
|
+
uiAuthSave.disabled = uiAuthRemove.disabled = true;
|
|
452
|
+
apiFetch("/api/settings/ui-password", { method: "PUT", body: JSON.stringify(body) })
|
|
453
|
+
.then(function (res) {
|
|
454
|
+
uiAuthCurrent.value = uiAuthNew.value = uiAuthRepeat.value = "";
|
|
455
|
+
renderUiAuth(res.uiAuth.enabled);
|
|
456
|
+
showToast(done, "success");
|
|
457
|
+
})
|
|
458
|
+
.catch(function (err) { uiAuthFail(err.message + (err.hint ? " " + err.hint : "")); })
|
|
459
|
+
.then(function () { uiAuthSave.disabled = uiAuthRemove.disabled = false; });
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
uiAuthForm.addEventListener("submit", function (e) {
|
|
463
|
+
e.preventDefault();
|
|
464
|
+
if (uiAuthNew.value.length < 8) return uiAuthFail("The password must be at least 8 characters.");
|
|
465
|
+
if (uiAuthNew.value !== uiAuthRepeat.value) return uiAuthFail("The passwords don't match.");
|
|
466
|
+
if (UI_AUTH && !uiAuthCurrent.value) return uiAuthFail("Enter the current password.");
|
|
467
|
+
var wasOn = UI_AUTH;
|
|
468
|
+
sendUiAuth({ password: uiAuthNew.value, current: uiAuthCurrent.value }, wasOn ? "Password changed. Other sessions are signed out." : "The dashboard now asks for this password");
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
uiAuthRemove.addEventListener("click", function () {
|
|
472
|
+
if (!uiAuthCurrent.value) return uiAuthFail("Enter the current password to remove it.");
|
|
473
|
+
sendUiAuth({ enabled: false, current: uiAuthCurrent.value }, "Password removed");
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
signOutBtn.addEventListener("click", function () {
|
|
477
|
+
fetch("/logout", { method: "POST" }).then(function () { location.reload(); });
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
`;
|
|
481
|
+
exports.overviewView = ` <section class="view" data-view="overview" aria-label="Overview">
|
|
482
|
+
<div class="page-head">
|
|
483
|
+
<h2 class="page-title">Overview</h2>
|
|
484
|
+
<p class="page-sub">Everything locadot is doing on this machine at a glance.</p>
|
|
485
|
+
</div>
|
|
486
|
+
<div class="tiles">
|
|
487
|
+
<a class="tile" href="#/hosts"><span class="label">Hosts</span><span id="ov-hosts" class="tile-big">—</span><span id="ov-hosts-sub" class="tile-sub"></span></a>
|
|
488
|
+
<a class="tile" href="#/hosts"><span class="label">Down</span><span id="ov-down" class="tile-big">—</span><span class="tile-sub">targets not answering</span></a>
|
|
489
|
+
<a class="tile" href="#/hosts"><span class="label">Requests</span><span id="ov-hits" class="tile-big">—</span><span id="ov-hits-sub" class="tile-sub">since the proxy started</span></a>
|
|
490
|
+
<a class="tile" href="#/hosts/shared"><span class="label">Public shares</span><span id="ov-shares" class="tile-big">—</span><span id="ov-shares-sub" class="tile-sub"></span></a>
|
|
491
|
+
<a class="tile" href="#/remote"><span class="label">Remote access</span><span id="ov-hub" class="tile-big tile-word">—</span><span id="ov-hub-sub" class="tile-sub"></span></a>
|
|
492
|
+
<a class="tile" href="#/remote/machines"><span class="label">Connected machines</span><span id="ov-remotes" class="tile-big">—</span><span id="ov-remotes-sub" class="tile-sub"></span></a>
|
|
493
|
+
</div>
|
|
494
|
+
<div class="grid-2">
|
|
495
|
+
<section class="card" aria-label="Host health">
|
|
496
|
+
<div class="card-head">
|
|
497
|
+
<div class="card-title-wrap"><h3 class="card-title">Host health</h3></div>
|
|
498
|
+
<div class="card-tools"><a class="btn btn-sm" href="#/hosts">Manage hosts</a></div>
|
|
499
|
+
</div>
|
|
500
|
+
<div id="ov-host-list" class="ov-list"></div>
|
|
501
|
+
<div id="ov-host-empty" class="empty" hidden>
|
|
502
|
+
<span class="empty-icon" aria-hidden="true">+</span>
|
|
503
|
+
<p>No hosts yet.</p>
|
|
504
|
+
<a class="btn btn-sm btn-primary" href="#/hosts">Add your first host</a>
|
|
505
|
+
</div>
|
|
506
|
+
</section>
|
|
507
|
+
<section class="card" aria-label="Proxy status">
|
|
508
|
+
<div class="card-head">
|
|
509
|
+
<div class="card-title-wrap"><h3 class="card-title">Proxy</h3></div>
|
|
510
|
+
<div class="card-tools"><a class="btn btn-sm" href="#/settings">Settings</a></div>
|
|
511
|
+
</div>
|
|
512
|
+
<div id="ov-checks" class="ov-list"></div>
|
|
513
|
+
</section>
|
|
514
|
+
</div>
|
|
515
|
+
<section class="card" aria-label="Recent activity">
|
|
516
|
+
<div class="card-head">
|
|
517
|
+
<div class="card-title-wrap"><h3 class="card-title">Recent activity</h3></div>
|
|
518
|
+
<div class="card-tools"><a class="btn btn-sm" href="#/logs">All logs</a></div>
|
|
519
|
+
</div>
|
|
520
|
+
<div id="ov-logs" class="logs-box logs-short" aria-live="off"></div>
|
|
521
|
+
</section>
|
|
522
|
+
</section>
|
|
523
|
+
|
|
524
|
+
`;
|
|
525
|
+
exports.logsView = ` <section class="view" data-view="logs" aria-label="Logs" hidden>
|
|
526
|
+
<div class="page-head">
|
|
527
|
+
<h2 class="page-title">Logs</h2>
|
|
528
|
+
<p class="page-sub">The proxy's log file.</p>
|
|
529
|
+
</div>
|
|
530
|
+
<section id="logs-panel" class="card" aria-label="Logs">
|
|
531
|
+
<div class="card-head logs-head">
|
|
532
|
+
<div class="card-tools logs-tools">
|
|
533
|
+
<input type="search" id="logs-filter" class="input input-sm input-search" placeholder="Filter lines" aria-label="Filter log lines" autocomplete="off">
|
|
534
|
+
<select id="logs-level" class="input input-sm" aria-label="Level">
|
|
535
|
+
<option value="">All levels</option>
|
|
536
|
+
<option value="error">Errors</option>
|
|
537
|
+
<option value="warn">Warnings+</option>
|
|
538
|
+
<option value="info">Info+</option>
|
|
539
|
+
</select>
|
|
540
|
+
<select id="logs-lines" class="input input-sm" aria-label="Lines">
|
|
541
|
+
<option value="200">Last 200</option>
|
|
542
|
+
<option value="500">Last 500</option>
|
|
543
|
+
<option value="1000">Last 1000</option>
|
|
544
|
+
</select>
|
|
545
|
+
<label class="check-inline"><input type="checkbox" id="logs-auto" checked> Follow</label>
|
|
546
|
+
</div>
|
|
547
|
+
<div class="card-tools">
|
|
548
|
+
<button type="button" id="logs-refresh" class="btn btn-sm">Refresh</button>
|
|
549
|
+
<button type="button" id="logs-copy" class="btn btn-sm">Copy</button>
|
|
550
|
+
<button type="button" id="logs-download" class="btn btn-sm">Download</button>
|
|
551
|
+
<button type="button" id="logs-clear" class="btn btn-sm btn-danger">Clear</button>
|
|
552
|
+
</div>
|
|
553
|
+
</div>
|
|
554
|
+
<div id="logs-box" class="logs-box logs-full" aria-live="off"></div>
|
|
555
|
+
<div id="logs-meta" class="logs-meta dim"></div>
|
|
556
|
+
</section>
|
|
557
|
+
</section>
|
|
558
|
+
|
|
559
|
+
`;
|
|
560
|
+
exports.settingsView = ` <section class="view" data-view="settings" aria-label="Settings" hidden>
|
|
561
|
+
<div class="page-head">
|
|
562
|
+
<h2 class="page-title">Settings</h2>
|
|
563
|
+
<p class="page-sub">Proxy, security and dashboard preferences.</p>
|
|
564
|
+
</div>
|
|
565
|
+
<div id="settings-tabs" class="tabs" role="tablist" data-tabs="settings" aria-label="Settings sections">
|
|
566
|
+
<button type="button" role="tab" data-tab="general">General</button>
|
|
567
|
+
<button type="button" role="tab" data-tab="security">Security</button>
|
|
568
|
+
<button type="button" role="tab" data-tab="preferences">Preferences</button>
|
|
569
|
+
<button type="button" role="tab" data-tab="developer">Developer</button>
|
|
570
|
+
</div>
|
|
571
|
+
<div class="tab-panels">
|
|
572
|
+
<div data-tab-panel="general">
|
|
573
|
+
<div class="layout">
|
|
574
|
+
<div class="col-primary">
|
|
575
|
+
<section id="proxy-settings-card" class="card" aria-label="Proxy settings">
|
|
576
|
+
<div class="card-head"><div class="card-title-wrap"><h3 class="card-title">Proxy</h3></div></div>
|
|
577
|
+
<form id="ports-form" class="card-body" novalidate>
|
|
578
|
+
<div class="form-row-2">
|
|
579
|
+
<div class="field"><label for="set-http-port">HTTP port</label><input type="number" id="set-http-port" class="input" min="1" max="65535" required></div>
|
|
580
|
+
<div class="field"><label for="set-https-port">HTTPS port</label><input type="number" id="set-https-port" class="input" min="1" max="65535" required></div>
|
|
581
|
+
</div>
|
|
582
|
+
<div id="ports-note" class="tile-sub"></div>
|
|
583
|
+
<div id="ports-error" class="field-error" role="alert" hidden></div>
|
|
584
|
+
<div class="form-actions">
|
|
585
|
+
<button type="submit" id="ports-save" class="btn btn-sm btn-primary">Save ports</button>
|
|
586
|
+
<button type="button" id="restart-proxy-btn" class="btn btn-sm">Restart proxy</button>
|
|
587
|
+
</div>
|
|
588
|
+
<div id="restart-note" class="hub-warn" hidden>Saved. Restart the proxy to use the new ports.</div>
|
|
589
|
+
</form>
|
|
590
|
+
<div class="stat-list">
|
|
591
|
+
<div class="stat-row"><div class="stat-main"><div class="label">Redirect http to https</div><div class="tile-sub">Plain http on this machine goes to https, for every host and the dashboard. A host's own setting wins.</div></div>
|
|
592
|
+
<div class="stat-side"><button type="button" id="set-https-redirect" class="switch" role="switch" aria-checked="false" aria-label="Redirect http to https"><span class="switch-knob"></span></button></div></div>
|
|
593
|
+
<div class="stat-row"><div class="stat-main"><div class="label">Listening on</div><div id="set-bind" class="tile-value mono">—</div><div id="set-bind-note" class="tile-sub"></div></div></div>
|
|
594
|
+
<div class="stat-row"><div class="stat-main"><div class="label">State directory</div><div id="set-home" class="tile-value mono">—</div></div><div class="stat-side"><button type="button" id="set-home-copy" class="copy-btn">Copy</button></div></div>
|
|
595
|
+
<div class="stat-row"><div class="stat-main"><div class="label">Log level</div><div id="set-loglevel" class="tile-value mono">—</div><div class="tile-sub">Set LOCADOT_LOG_LEVEL before starting to change it.</div></div></div>
|
|
596
|
+
</div>
|
|
597
|
+
</section>
|
|
598
|
+
|
|
599
|
+
<section class="card danger-card" aria-label="Danger zone">
|
|
600
|
+
<div class="card-head"><div class="card-title-wrap"><h3 class="card-title">Danger zone</h3></div></div>
|
|
601
|
+
<div class="stat-list">
|
|
602
|
+
<div class="stat-row"><div class="stat-main"><div class="label">Clear logs</div><div class="tile-sub">Empty the log file.</div></div><div class="stat-side"><button type="button" id="danger-clear-logs" class="btn btn-sm btn-danger">Clear</button></div></div>
|
|
603
|
+
<div class="stat-row"><div class="stat-main"><div class="label">Stop proxy</div><div class="tile-sub">All hosts stop working until <code class="mono">locadot start</code>.</div></div><div class="stat-side"><button type="button" id="stop-proxy-btn" class="btn btn-sm btn-danger">Stop</button></div></div>
|
|
604
|
+
</div>
|
|
605
|
+
</section>
|
|
606
|
+
</div>
|
|
607
|
+
<aside class="col-secondary">
|
|
608
|
+
<section id="system-card" class="card" aria-label="System and access">
|
|
609
|
+
<div class="card-head"><div class="card-title-wrap"><h3 class="card-title">System & access</h3></div></div>
|
|
610
|
+
<div class="stat-list">
|
|
611
|
+
<div class="stat-row" hidden>
|
|
612
|
+
<div class="stat-main">
|
|
613
|
+
<div class="label">Proxy</div>
|
|
614
|
+
<div id="tile-proxy-ports" class="tile-value mono">—</div>
|
|
615
|
+
<div id="tile-proxy-bind" class="tile-sub mono"></div>
|
|
616
|
+
<div id="tile-proxy-statedir" class="tile-sub mono"></div>
|
|
617
|
+
<div id="tile-proxy-platform" class="tile-sub"></div>
|
|
618
|
+
</div>
|
|
619
|
+
</div>
|
|
620
|
+
<div class="stat-row">
|
|
621
|
+
<div class="stat-main">
|
|
622
|
+
<div class="label">Access</div>
|
|
623
|
+
<div id="tile-root-value" class="tile-value">—</div>
|
|
624
|
+
<div id="tile-ports-value" class="tile-sub"></div>
|
|
625
|
+
<div id="tile-ports-hint" class="tile-hint" hidden></div>
|
|
626
|
+
</div>
|
|
627
|
+
</div>
|
|
628
|
+
<div class="stat-row">
|
|
629
|
+
<div class="stat-main">
|
|
630
|
+
<div class="label">Start at boot</div>
|
|
631
|
+
<div id="tile-startup-value" class="tile-value">—</div>
|
|
632
|
+
<div id="tile-startup-method" class="tile-sub"></div>
|
|
633
|
+
</div>
|
|
634
|
+
<div class="stat-side">
|
|
635
|
+
<button type="button" id="startup-toggle" class="switch" role="switch" aria-checked="false" aria-label="Toggle start at boot">
|
|
636
|
+
<span class="switch-knob"></span>
|
|
637
|
+
</button>
|
|
638
|
+
</div>
|
|
639
|
+
</div>
|
|
640
|
+
</div>
|
|
641
|
+
</section>
|
|
642
|
+
</aside>
|
|
643
|
+
</div>
|
|
644
|
+
</div>
|
|
645
|
+
|
|
646
|
+
<div data-tab-panel="security">
|
|
647
|
+
<div class="layout">
|
|
648
|
+
<div class="col-primary">
|
|
649
|
+
<section class="card" aria-label="HTTPS certificate">
|
|
650
|
+
<div class="card-head"><div class="card-title-wrap"><h3 class="card-title">HTTPS certificate</h3></div></div>
|
|
651
|
+
<div class="stat-list">
|
|
652
|
+
<div class="stat-row">
|
|
653
|
+
<div class="stat-main">
|
|
654
|
+
<div class="label">CA trust</div>
|
|
655
|
+
<div id="tile-ca-value" class="tile-value">—</div>
|
|
656
|
+
<div class="tile-sub">Trust the local CA so browsers stop warning on https hosts.</div>
|
|
657
|
+
</div>
|
|
658
|
+
<div class="stat-side">
|
|
659
|
+
<button type="button" id="ca-toggle" class="switch" role="switch" aria-checked="false" aria-label="Toggle local CA trust">
|
|
660
|
+
<span class="switch-knob"></span>
|
|
661
|
+
</button>
|
|
662
|
+
</div>
|
|
663
|
+
</div>
|
|
664
|
+
</div>
|
|
665
|
+
</section>
|
|
666
|
+
|
|
667
|
+
<section id="ui-auth-card" class="card" aria-label="Dashboard password">
|
|
668
|
+
<div class="card-head"><div class="card-title-wrap"><h3 class="card-title">Dashboard password</h3><span id="ui-auth-badge" class="badge">Off</span></div></div>
|
|
669
|
+
<form id="ui-auth-form" class="card-body token-body" novalidate>
|
|
670
|
+
<div id="ui-auth-note" class="tile-sub">Ask for a password before the dashboard opens. Only the dashboard is protected: your hosts and scripts using the API token work as before. Forgot it? Run <code class="mono">locadot ui:password</code> in a terminal.</div>
|
|
671
|
+
<div id="ui-auth-current-field" class="field" hidden><label for="ui-auth-current">Current password</label><input type="password" id="ui-auth-current" class="input" autocomplete="current-password"></div>
|
|
672
|
+
<div class="form-row-2">
|
|
673
|
+
<div class="field"><label for="ui-auth-new">New password</label><input type="password" id="ui-auth-new" class="input" autocomplete="new-password" minlength="8" placeholder="8+ characters"></div>
|
|
674
|
+
<div class="field"><label for="ui-auth-repeat">Repeat it</label><input type="password" id="ui-auth-repeat" class="input" autocomplete="new-password"></div>
|
|
675
|
+
</div>
|
|
676
|
+
<div id="ui-auth-error" class="field-error" role="alert" hidden></div>
|
|
677
|
+
<div class="form-actions">
|
|
678
|
+
<button type="submit" id="ui-auth-save" class="btn btn-sm btn-primary">Set password</button>
|
|
679
|
+
<button type="button" id="ui-auth-remove" class="btn btn-sm btn-danger" hidden>Remove password</button>
|
|
680
|
+
</div>
|
|
681
|
+
</form>
|
|
682
|
+
</section>
|
|
683
|
+
</div>
|
|
684
|
+
<aside class="col-secondary">
|
|
685
|
+
<section class="card" aria-label="API token">
|
|
686
|
+
<div class="card-head"><div class="card-title-wrap"><h3 class="card-title">API token</h3></div></div>
|
|
687
|
+
<div class="card-body token-body">
|
|
688
|
+
<div class="tile-sub">Scripts send it as <code class="mono">X-Locadot-Token</code>. It changes every time the proxy starts.</div>
|
|
689
|
+
<div class="token-row"><code id="token-value" class="mono token-value">••••••••••••••••</code>
|
|
690
|
+
<button type="button" id="token-reveal" class="copy-btn">Show</button>
|
|
691
|
+
<button type="button" id="token-copy" class="copy-btn">Copy</button></div>
|
|
692
|
+
</div>
|
|
693
|
+
</section>
|
|
694
|
+
</aside>
|
|
695
|
+
</div>
|
|
696
|
+
</div>
|
|
697
|
+
|
|
698
|
+
<div data-tab-panel="preferences">
|
|
699
|
+
<section class="card" aria-label="Dashboard preferences">
|
|
700
|
+
<div class="card-head"><div class="card-title-wrap"><h3 class="card-title">Dashboard</h3></div></div>
|
|
701
|
+
<div class="stat-list">
|
|
702
|
+
<div class="stat-row"><div class="stat-main"><div class="label">Theme</div><div class="tile-sub">Follow the system, or pick one.</div></div>
|
|
703
|
+
<div class="stat-side"><div id="pref-theme" class="segmented" role="radiogroup" aria-label="Theme">
|
|
704
|
+
<button type="button" role="radio" data-value="system">System</button><button type="button" role="radio" data-value="light">Light</button><button type="button" role="radio" data-value="dark">Dark</button>
|
|
705
|
+
</div></div></div>
|
|
706
|
+
<div class="stat-row"><div class="stat-main"><div class="label">Refresh every</div><div class="tile-sub">How often hosts and status are polled.</div></div>
|
|
707
|
+
<div class="stat-side"><select id="pref-refresh" class="input input-sm" aria-label="Refresh interval">
|
|
708
|
+
<option value="2000">2 seconds</option><option value="5000">5 seconds</option><option value="10000">10 seconds</option><option value="30000">30 seconds</option><option value="0">Paused</option>
|
|
709
|
+
</select></div></div>
|
|
710
|
+
<div class="stat-row"><div class="stat-main"><div class="label">Open on</div><div class="tile-sub">The page shown when the dashboard opens.</div></div>
|
|
711
|
+
<div class="stat-side"><select id="pref-start" class="input input-sm" aria-label="Start page">
|
|
712
|
+
<option value="overview">Overview</option><option value="hosts">Hosts</option><option value="last">Last visited</option>
|
|
713
|
+
</select></div></div>
|
|
714
|
+
<div class="stat-row"><div class="stat-main"><div class="label">Collapsed sidebar</div><div class="tile-sub">Show only icons in the sidebar.</div></div>
|
|
715
|
+
<div class="stat-side"><button type="button" id="pref-sidebar" class="switch" role="switch" aria-checked="false" aria-label="Collapsed sidebar"><span class="switch-knob"></span></button></div></div>
|
|
716
|
+
<div class="stat-row"><div class="stat-main"><div class="label">Ask before removing</div><div class="tile-sub">Confirm before a host is removed.</div></div>
|
|
717
|
+
<div class="stat-side"><button type="button" id="pref-confirm" class="switch" role="switch" aria-checked="true" aria-label="Ask before removing"><span class="switch-knob"></span></button></div></div>
|
|
718
|
+
</div>
|
|
719
|
+
</section>
|
|
720
|
+
|
|
721
|
+
<section class="card" aria-label="New host defaults">
|
|
722
|
+
<div class="card-head"><div class="card-title-wrap"><h3 class="card-title">New host defaults</h3></div></div>
|
|
723
|
+
<div class="stat-list">
|
|
724
|
+
<div class="stat-row"><div class="stat-main"><div class="label">Insecure TLS</div><div class="tile-sub">Accept self-signed certificates on https targets.</div></div>
|
|
725
|
+
<div class="stat-side"><button type="button" id="pref-insecure" class="switch" role="switch" aria-checked="false" aria-label="Insecure TLS by default"><span class="switch-knob"></span></button></div></div>
|
|
726
|
+
<div class="stat-row"><div class="stat-main"><div class="label">Bypass CORS</div><div class="tile-sub">Send the target's own Origin and allow any origin.</div></div>
|
|
727
|
+
<div class="stat-side"><button type="button" id="pref-cors" class="switch" role="switch" aria-checked="false" aria-label="Bypass CORS by default"><span class="switch-knob"></span></button></div></div>
|
|
728
|
+
</div>
|
|
729
|
+
</section>
|
|
730
|
+
</div>
|
|
731
|
+
|
|
732
|
+
<div data-tab-panel="developer">
|
|
733
|
+
<section id="cli-card" class="card" aria-label="Use from CLI or scripts">
|
|
734
|
+
<div class="card-head">
|
|
735
|
+
<div class="card-title-wrap"><h3 class="card-title">CLI & API</h3></div>
|
|
736
|
+
</div>
|
|
737
|
+
<div class="card-body cli-list">
|
|
738
|
+
<div class="cli-item">
|
|
739
|
+
<div class="cli-item-head"><span class="label">Add a host</span><button type="button" id="cli-add-copy" class="copy-btn">Copy</button></div>
|
|
740
|
+
<pre id="cli-add-pre" class="code-block mono">locadot add --host app.localhost --port 3000</pre>
|
|
741
|
+
</div>
|
|
742
|
+
<div class="cli-item">
|
|
743
|
+
<div class="cli-item-head"><span class="label">HTTP API (scripts, AI agents)</span><button type="button" id="cli-curl-copy" class="copy-btn">Copy</button></div>
|
|
744
|
+
<pre id="cli-curl-pre" class="code-block mono">curl -X POST http://localhost/api/hosts -H "X-Locadot-Token: $(locadot token)" -H 'Content-Type: application/json' -d '{"host":"app.localhost","target":"3000"}'</pre>
|
|
745
|
+
</div>
|
|
746
|
+
<div class="cli-item">
|
|
747
|
+
<div class="cli-item-head"><span class="label">Share publicly</span><button type="button" id="cli-tunnel-copy" class="copy-btn">Copy</button></div>
|
|
748
|
+
<pre id="cli-tunnel-pre" class="code-block mono">locadot tunnel --host app.localhost</pre>
|
|
749
|
+
</div>
|
|
750
|
+
</div>
|
|
751
|
+
</section>
|
|
752
|
+
</div>
|
|
753
|
+
</div>
|
|
754
|
+
</section>
|
|
755
|
+
`;
|
|
756
|
+
/** Styles for these pages, appended after theme.ts. */
|
|
757
|
+
exports.css = `
|
|
758
|
+
/* Keep card headers on one line on the overview page: short
|
|
759
|
+
title + a single action button shouldn't wrap onto its own row. */
|
|
760
|
+
[data-view="overview"] .card-head { flex-wrap: nowrap; }
|
|
761
|
+
[data-view="overview"] .card-title-wrap { flex: 1 1 auto; min-width: 0; overflow: hidden; }
|
|
762
|
+
[data-view="overview"] .card-title-wrap .card-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
763
|
+
[data-view="overview"] .card-head .card-tools { flex: 0 0 auto; width: auto; }
|
|
764
|
+
|
|
765
|
+
/* Level-coloured log lines (replaces one big <pre> blob). */
|
|
766
|
+
.log-line { font-family: var(--mono); font-size: 12px; line-height: 1.55; white-space: pre-wrap; word-break: break-all; }
|
|
767
|
+
.log-line + .log-line { margin-top: 1px; }
|
|
768
|
+
.log-line-error { color: var(--down); }
|
|
769
|
+
.log-line-warn { color: var(--warn); }
|
|
770
|
+
.log-line-info { color: var(--fg-2); }
|
|
771
|
+
.log-line-debug, .log-line-verbose, .log-line-silly { color: var(--muted); }
|
|
772
|
+
.log-line-empty { color: var(--muted); }
|
|
773
|
+
|
|
774
|
+
/* The logs toolbar stays visible while the log list scrolls underneath it. */
|
|
775
|
+
#logs-panel .logs-head {
|
|
776
|
+
position: sticky;
|
|
777
|
+
top: var(--topbar-h);
|
|
778
|
+
z-index: 3;
|
|
779
|
+
background: var(--surface);
|
|
780
|
+
}
|
|
781
|
+
`;
|