Pithos 0.3.19: open Pithos in a browser, from the app or the pithos.sia website
This commit is contained in:
parent
4ff75d312a
commit
dbef930d45
4 changed files with 140 additions and 7 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "pithos",
|
"id": "pithos",
|
||||||
"name": "Pithos",
|
"name": "Pithos",
|
||||||
"version": "0.3.18",
|
"version": "0.3.19",
|
||||||
"description": "Run s3d, the Sia S3 gateway, from the Theseus sidebar: connect it to a Sia indexer, create S3 users and access keys, browse and share buckets, and watch uploads reach Sia.",
|
"description": "Run s3d, the Sia S3 gateway, from the Theseus sidebar: connect it to a Sia indexer, create S3 users and access keys, browse and share buckets, and watch uploads reach Sia.",
|
||||||
"author": "Silent Mode",
|
"author": "Silent Mode",
|
||||||
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIwIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM2RkYzk3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMWU4ZjZhIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTIyIDZoMjB2NWMwIDItMiAzLTIgNSA4IDMgMTQgMTEgMTQgMjEgMCAxMi05IDIxLTIyIDIxUzEwIDQ5IDEwIDM3YzAtMTAgNi0xOCAxNC0yMSAwLTItMi0zLTItNXoiIGZpbGw9InVybCgjZykiLz48cGF0aCBkPSJNMTQgMzBoMzZNMTMgNDBoMzgiIHN0cm9rZT0iIzBiMWYxOCIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9Ii41NSIvPjwvc3ZnPgo=",
|
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIwIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM2RkYzk3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMWU4ZjZhIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTIyIDZoMjB2NWMwIDItMiAzLTIgNSA4IDMgMTQgMTEgMTQgMjEgMCAxMi05IDIxLTIyIDIxUzEwIDQ5IDEwIDM3YzAtMTAgNi0xOCAxNC0yMSAwLTItMi0zLTItNXoiIGZpbGw9InVybCgjZykiLz48cGF0aCBkPSJNMTQgMzBoMzZNMTMgNDBoMzgiIHN0cm9rZT0iIzBiMWYxOCIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9Ii41NSIvPjwvc3ZnPgo=",
|
||||||
|
|
@ -9,7 +9,8 @@
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
"sidebar-panel",
|
"sidebar-panel",
|
||||||
"vault-derive",
|
"vault-derive",
|
||||||
"site-route"
|
"site-route",
|
||||||
|
"approval-modal"
|
||||||
],
|
],
|
||||||
"updateURL": "https://navigate.st/bns/theseus.x/extensions/pithos/updates.json",
|
"updateURL": "https://navigate.st/bns/theseus.x/extensions/pithos/updates.json",
|
||||||
"siteRoutes": [
|
"siteRoutes": [
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,11 @@ export async function createPithos(opts = {}) {
|
||||||
allowedHosts = [], // extra Host header values (reverse proxy names)
|
allowedHosts = [], // extra Host header values (reverse proxy names)
|
||||||
hostName = 'web', // 'web' | 'desktop' | 'theseus' — UI tweaks only
|
hostName = 'web', // 'web' | 'desktop' | 'theseus' — UI tweaks only
|
||||||
openExternal, // (url) => void, for hosts that can open the system browser
|
openExternal, // (url) => void, for hosts that can open the system browser
|
||||||
|
openBrowser = openExternal, // (url) => void: the user's default browser (Theseus opens openExternal in its own tab)
|
||||||
|
// ({ path }) => Promise<boolean>: the host asks the user, in its own UI, whether
|
||||||
|
// a browser that came to the open-in-Pithos address may be signed in.
|
||||||
|
confirmBrowserOpen,
|
||||||
|
handoffPort = HANDOFF_PORT, // fixed loopback port the pithos.sia website links to; null = off
|
||||||
showPanel, // () => void, for hosts with a side panel to switch back to (Theseus)
|
showPanel, // () => void, for hosts with a side panel to switch back to (Theseus)
|
||||||
secrets, // hosted identity + encryption keys; the Theseus vault, else a local key file
|
secrets, // hosted identity + encryption keys; the Theseus vault, else a local key file
|
||||||
} = opts;
|
} = opts;
|
||||||
|
|
@ -67,6 +72,19 @@ export async function createPithos(opts = {}) {
|
||||||
});
|
});
|
||||||
const secret = crypto.randomBytes(24).toString('base64url');
|
const secret = crypto.randomBytes(24).toString('base64url');
|
||||||
const sessions = new Set();
|
const sessions = new Set();
|
||||||
|
// Sessions of a browser this host opened (or let in), not the host's own
|
||||||
|
// window: the UI leaves out the controls that only make sense in the host.
|
||||||
|
const externalSessions = new Set();
|
||||||
|
// One-time sign-in links handed to a browser: id -> expiry. Unlike the
|
||||||
|
// host's ?t= link they work once, for a minute.
|
||||||
|
const signins = new Map();
|
||||||
|
function signInUrl(route = '') {
|
||||||
|
for (const [k, exp] of signins) if (exp < Date.now()) signins.delete(k);
|
||||||
|
const id = crypto.randomBytes(24).toString('base64url');
|
||||||
|
signins.set(id, Date.now() + 60_000);
|
||||||
|
const frag = /^#\/[A-Za-z0-9._~%/=-]{0,300}$/.test(route) ? route : '';
|
||||||
|
return `${baseUrl}?s=${id}${frag}`;
|
||||||
|
}
|
||||||
// One-time download links: id -> { path, exp }. Lets a host hand a file
|
// One-time download links: id -> { path, exp }. Lets a host hand a file
|
||||||
// to its own download manager, which carries no session cookie.
|
// to its own download manager, which carries no session cookie.
|
||||||
const tickets = new Map();
|
const tickets = new Map();
|
||||||
|
|
@ -550,6 +568,15 @@ export async function createPithos(opts = {}) {
|
||||||
return body;
|
return body;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// "Open in browser": the same Pithos in the user's default browser, signed
|
||||||
|
// in with a one-time link, at the page the user is on.
|
||||||
|
route('POST', '/api/open-in-browser', async (req) => {
|
||||||
|
if (!openBrowser) throw new HttpError(400, 'this Pithos cannot open a browser');
|
||||||
|
const { route: r = '' } = await jsonBody(req);
|
||||||
|
openBrowser(signInUrl(String(r)));
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
|
||||||
route('GET', '/api/space', async () => {
|
route('GET', '/api/space', async () => {
|
||||||
// On Silent Mode this call is forwarded: the server's s3d answers for its own folder.
|
// On Silent Mode this call is forwarded: the server's s3d answers for its own folder.
|
||||||
if (Date.now() - spaceCache.at < 60_000 && spaceCache.value) return spaceCache.value;
|
if (Date.now() - spaceCache.at < 60_000 && spaceCache.value) return spaceCache.value;
|
||||||
|
|
@ -731,9 +758,10 @@ export async function createPithos(opts = {}) {
|
||||||
return m && sessions.has(m[1]) ? m[1] : null;
|
return m && sessions.has(m[1]) ? m[1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function issueSession(res) {
|
function issueSession(res, { external = false } = {}) {
|
||||||
const id = crypto.randomBytes(24).toString('base64url');
|
const id = crypto.randomBytes(24).toString('base64url');
|
||||||
sessions.add(id);
|
sessions.add(id);
|
||||||
|
if (external) externalSessions.add(id);
|
||||||
res.setHeader('set-cookie', `pithos=${id}; HttpOnly; SameSite=Strict; Path=/`);
|
res.setHeader('set-cookie', `pithos=${id}; HttpOnly; SameSite=Strict; Path=/`);
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
@ -773,9 +801,17 @@ export async function createPithos(opts = {}) {
|
||||||
res.writeHead(302, { location: url.pathname });
|
res.writeHead(302, { location: url.pathname });
|
||||||
return res.end();
|
return res.end();
|
||||||
}
|
}
|
||||||
|
const once = url.searchParams.get('s');
|
||||||
|
if (once && req.method === 'GET' && !url.pathname.startsWith('/api/')) {
|
||||||
|
const exp = signins.get(once);
|
||||||
|
signins.delete(once);
|
||||||
|
if (exp && exp > Date.now()) issueSession(res, { external: true });
|
||||||
|
res.writeHead(302, { location: url.pathname }); // the #route stays with the browser
|
||||||
|
return res.end();
|
||||||
|
}
|
||||||
|
|
||||||
if (url.pathname === '/api/session') {
|
if (url.pathname === '/api/session') {
|
||||||
if (req.method === 'GET') return sendJson(res, 200, { authenticated: !!cookieSession(req), passwordLogin: !!password, host: hostName });
|
if (req.method === 'GET') { const c = cookieSession(req); return sendJson(res, 200, { authenticated: !!c, external: !!c && externalSessions.has(c), passwordLogin: !!password, host: hostName, canOpenBrowser: !!openBrowser }); }
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
requireCsrf(req);
|
requireCsrf(req);
|
||||||
const body = await jsonBody(req);
|
const body = await jsonBody(req);
|
||||||
|
|
@ -866,6 +902,7 @@ export async function createPithos(opts = {}) {
|
||||||
autoFlushTimer.unref?.();
|
autoFlushTimer.unref?.();
|
||||||
const displayHost = host === '0.0.0.0' || host === '::' ? '127.0.0.1' : host;
|
const displayHost = host === '0.0.0.0' || host === '::' ? '127.0.0.1' : host;
|
||||||
const baseUrl = `http://${displayHost.includes(':') ? `[${displayHost}]` : displayHost}:${boundPort}/`;
|
const baseUrl = `http://${displayHost.includes(':') ? `[${displayHost}]` : displayHost}:${boundPort}/`;
|
||||||
|
const handoff = handoffPort && confirmBrowserOpen ? startHandoff(handoffPort, confirmBrowserOpen, signInUrl, (m) => daemon.pushLog('sys', m)) : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
server,
|
server,
|
||||||
|
|
@ -901,6 +938,7 @@ export async function createPithos(opts = {}) {
|
||||||
hosted.close();
|
hosted.close();
|
||||||
login.cancel();
|
login.cancel();
|
||||||
for (const s of subscribers) s.end();
|
for (const s of subscribers) s.end();
|
||||||
|
handoff?.close();
|
||||||
await daemon.stop();
|
await daemon.stop();
|
||||||
// Idle keep-alive sockets would hold close() for seconds; nobody is left to answer.
|
// Idle keep-alive sockets would hold close() for seconds; nobody is left to answer.
|
||||||
const closed = new Promise((r) => server.close(r));
|
const closed = new Promise((r) => server.close(r));
|
||||||
|
|
@ -910,6 +948,51 @@ export async function createPithos(opts = {}) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- open from the pithos.sia website ---------------------------------------
|
||||||
|
// The website links to http://127.0.0.1:47621/open?path=/<user>/<drive>. Any
|
||||||
|
// page or local program could follow that link, so nobody is signed in until
|
||||||
|
// the user says yes in the host's own window; the page that sent the browser
|
||||||
|
// here learns nothing either way. One question at a time.
|
||||||
|
export const HANDOFF_PORT = 47621;
|
||||||
|
|
||||||
|
export function handoffRoute(p) {
|
||||||
|
const segs = String(p || '').split('/').filter(Boolean).slice(0, 12);
|
||||||
|
if (!segs.length || segs.some((x) => !/^[A-Za-z0-9._~%=-]{1,120}$/.test(x))) return '#/overview';
|
||||||
|
const pages = ['overview', 'users', 'buckets', 'account', 'settings', 'logs'];
|
||||||
|
if (segs[0] === 'drives') return '#/buckets';
|
||||||
|
if (pages.includes(segs[0])) return `#/${segs.join('/')}`;
|
||||||
|
return `#/u/${segs.join('/')}`; // /<user>/<drive>/<folder>
|
||||||
|
}
|
||||||
|
|
||||||
|
function startHandoff(port, confirm, signInUrl, log) {
|
||||||
|
const page = (res, status, title, text) => {
|
||||||
|
res.writeHead(status, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'x-frame-options': 'DENY', 'content-security-policy': "default-src 'none'; style-src 'unsafe-inline'" });
|
||||||
|
res.end(`<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>${title}</title><body style="font:16px system-ui;background:#0f1214;color:#e6ebe8;display:grid;place-items:center;min-height:90vh;margin:0"><div style="max-width:460px;padding:24px"><h1 style="font-size:20px">${title}</h1><p style="color:#9aa5a0">${text}</p></div>`);
|
||||||
|
};
|
||||||
|
let pending = false;
|
||||||
|
const srv = http.createServer(async (req, res) => {
|
||||||
|
const url = new URL(req.url, 'http://127.0.0.1');
|
||||||
|
// DNS rebinding: only the loopback names, never a hostname that resolves here
|
||||||
|
if (!new RegExp(`^(127\\.0\\.0\\.1|localhost|\\[::1\\]):${port}$`).test(req.headers.host || '')) return page(res, 403, 'Not allowed', 'Use http://127.0.0.1 to open Pithos.');
|
||||||
|
if (req.method !== 'GET' || url.pathname !== '/open') return page(res, 404, 'Pithos', 'Nothing here. Open Pithos from the pithos.sia website.');
|
||||||
|
if (pending) return page(res, 429, 'Answer Pithos first', 'Pithos is already asking whether to open in a browser. Answer it there, then try again.');
|
||||||
|
pending = true;
|
||||||
|
try {
|
||||||
|
let timer;
|
||||||
|
const ok = await Promise.race([
|
||||||
|
Promise.resolve(confirm({ path: url.searchParams.get('path') || '/' })).catch(() => false),
|
||||||
|
new Promise((r) => { timer = setTimeout(() => r(false), 120_000); }),
|
||||||
|
]).finally(() => clearTimeout(timer));
|
||||||
|
if (!ok) return page(res, 403, 'Not opened', 'Pithos was not opened in this browser. If you meant to, go back and press Open my Pithos again, then choose Open in Pithos.');
|
||||||
|
res.writeHead(302, { location: signInUrl(handoffRoute(url.searchParams.get('path'))), 'cache-control': 'no-store' });
|
||||||
|
res.end();
|
||||||
|
} finally { pending = false; }
|
||||||
|
});
|
||||||
|
srv.on('error', (e) => log(e.code === 'EADDRINUSE' ? `another Pithos already answers the website on port ${port}` : `website link: ${e.message}`));
|
||||||
|
srv.listen(port, '127.0.0.1');
|
||||||
|
return { close: () => srv.close() };
|
||||||
|
}
|
||||||
|
|
||||||
// ---- helpers -------------------------------------------------------------
|
// ---- helpers -------------------------------------------------------------
|
||||||
|
|
||||||
function sendJson(res, status, body) {
|
function sendJson(res, status, body) {
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,25 @@ module.exports = {
|
||||||
hostName: "theseus",
|
hostName: "theseus",
|
||||||
binDir: path.join(dataDir, "bin"),
|
binDir: path.join(dataDir, "bin"),
|
||||||
openExternal: (url) => api.openTab(url),
|
openExternal: (url) => api.openTab(url),
|
||||||
|
// "Open in browser": the user's default browser, not a Theseus tab.
|
||||||
|
openBrowser: (url) => require("electron").shell.openExternal(url),
|
||||||
|
// "Open my Pithos" on the pithos.sia website in another browser:
|
||||||
|
// only with the vault unlocked, and only if the user says yes here.
|
||||||
|
confirmBrowserOpen: async () => {
|
||||||
|
if (typeof api.approvalModal !== "function") return false;
|
||||||
|
const st = await vaultStatus();
|
||||||
|
if (st.setup && st.prompt && !st.unlocked) {
|
||||||
|
const r = await vault.requestUnlock({ reason: "Open Pithos in another browser." });
|
||||||
|
if (!r.ok) return false;
|
||||||
|
}
|
||||||
|
const pick = await api.approvalModal({
|
||||||
|
title: "Open Pithos in another browser?",
|
||||||
|
origin: "pithos.sia website",
|
||||||
|
body: "A browser on this computer asked to open Pithos, usually from \"Open my Pithos\" on the pithos.sia website. Allow it only if you just clicked that. That browser then has the same access as Pithos here until Theseus restarts.",
|
||||||
|
actions: [{ id: "allow", label: "Open in browser", primary: true }],
|
||||||
|
});
|
||||||
|
return String(pick || "").split("+")[0] === "allow";
|
||||||
|
},
|
||||||
// "Back to sidebar" from the full-tab view.
|
// "Back to sidebar" from the full-tab view.
|
||||||
showPanel: () => api.revealSidebar("main"),
|
showPanel: () => api.revealSidebar("main"),
|
||||||
// "On Silent Mode": sign-in identity and file encryption keys come
|
// "On Silent Mode": sign-in identity and file encryption keys come
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,16 @@ const NAV_OF = { start: 'account', setup: 'account' };
|
||||||
// [page, ...parts] for the current address. On pithos.sia the first part is a
|
// [page, ...parts] for the current address. On pithos.sia the first part is a
|
||||||
// page name or an S3 user (whose drives the rest addresses).
|
// page name or an S3 user (whose drives the rest addresses).
|
||||||
function currentRoute() {
|
function currentRoute() {
|
||||||
if (!PATH_MODE) return location.hash.replace(/^#\/?/, '').split('/').filter((x, i) => i > 0 || x);
|
if (!PATH_MODE) {
|
||||||
|
const parts = location.hash.replace(/^#\/?/, '').split('/').filter((x, i) => i > 0 || x);
|
||||||
|
// #/u/<user>/<drive>/…: a drive of a given S3 user (links handed to a browser)
|
||||||
|
if (parts[0] === 'u' && parts[1]) {
|
||||||
|
const user = decodeURIComponent(parts[1]);
|
||||||
|
if (state.selectedUser !== user) { state.selectedUser = user; try { localStorage.setItem('pithos.user', user); } catch {} }
|
||||||
|
return ['buckets', ...parts.slice(2)];
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
const segs = location.pathname.split('/').filter(Boolean);
|
const segs = location.pathname.split('/').filter(Boolean);
|
||||||
if (!segs.length) return [];
|
if (!segs.length) return [];
|
||||||
const first = decodeURIComponent(segs[0]);
|
const first = decodeURIComponent(segs[0]);
|
||||||
|
|
@ -227,6 +236,25 @@ function hashToPath(hash) {
|
||||||
}
|
}
|
||||||
return `/${[name, ...rest].join('/')}`;
|
return `/${[name, ...rest].join('/')}`;
|
||||||
}
|
}
|
||||||
|
// The page the user is on as a '#/' route another browser can open
|
||||||
|
// (drives carry their S3 user, which that browser does not know yet).
|
||||||
|
function shareableRoute() {
|
||||||
|
const [name = 'overview', ...rest] = currentRoute();
|
||||||
|
if (name === 'buckets' && state.selectedUser) return `#/u/${encodeURIComponent(state.selectedUser)}${rest.length ? '/' + rest.join('/') : ''}`;
|
||||||
|
return `#/${[name, ...rest].join('/')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Open in browser": this Pithos in the default browser, at the same page.
|
||||||
|
function addOpenInBrowser() {
|
||||||
|
const run = async () => {
|
||||||
|
try { await api('POST', '/api/open-in-browser', { route: shareableRoute() }); toast('Opened in your browser'); } catch (e) { fail(e); }
|
||||||
|
};
|
||||||
|
const row = h('div', { class: 'host-controls' },
|
||||||
|
h('button', { class: 'btn small', title: 'Open this page of Pithos in your default browser', onclick: run }, '↗ Open in browser'));
|
||||||
|
$('.sidebar').insertBefore(row, $('#daemon-pill'));
|
||||||
|
addMenuAction('↗ Open in browser', run);
|
||||||
|
}
|
||||||
|
|
||||||
// Go to a page given as '#/page/parts', whichever kind of address this host uses.
|
// Go to a page given as '#/page/parts', whichever kind of address this host uses.
|
||||||
function navTo(hash) {
|
function navTo(hash) {
|
||||||
if (!PATH_MODE) { location.hash = hash; return; }
|
if (!PATH_MODE) { location.hash = hash; return; }
|
||||||
|
|
@ -2623,7 +2651,7 @@ function setupFooter() {
|
||||||
if (bridge) {
|
if (bridge) {
|
||||||
$('#brandlink').addEventListener('click', (e) => { e.preventDefault(); bridge.invoke('open-external', { url: 'https://pithos.sia/' }).catch(fail); });
|
$('#brandlink').addEventListener('click', (e) => { e.preventDefault(); bridge.invoke('open-external', { url: 'https://pithos.sia/' }).catch(fail); });
|
||||||
}
|
}
|
||||||
if (!bridge && state.status?.host === 'desktop' && current) return desktopUpdates(current, { ver, chip, check });
|
if (!bridge && state.status?.host === 'desktop' && current && !state.external) return desktopUpdates(current, { ver, chip, check });
|
||||||
if (!bridge || !current) return; // only the Theseus add-on updates itself
|
if (!bridge || !current) return; // only the Theseus add-on updates itself
|
||||||
|
|
||||||
let latest = null;
|
let latest = null;
|
||||||
|
|
@ -2722,7 +2750,9 @@ async function boot() {
|
||||||
if (bridge && !(await vaultGate())) return;
|
if (bridge && !(await vaultGate())) return;
|
||||||
$('#app').hidden = false;
|
$('#app').hidden = false;
|
||||||
$('#app').dataset.booted = '1';
|
$('#app').dataset.booted = '1';
|
||||||
|
state.external = !!s.external;
|
||||||
if (bridge) { addSidebarControls(); watchVaultLock(); }
|
if (bridge) { addSidebarControls(); watchVaultLock(); }
|
||||||
|
if (!state.external && s.canOpenBrowser && ['desktop', 'theseus'].includes(s.host)) addOpenInBrowser();
|
||||||
// Backfill logs the server already holds, then stream.
|
// Backfill logs the server already holds, then stream.
|
||||||
try {
|
try {
|
||||||
state.logs = await api('GET', '/api/logs');
|
state.logs = await api('GET', '/api/logs');
|
||||||
|
|
@ -2730,7 +2760,7 @@ async function boot() {
|
||||||
} catch {}
|
} catch {}
|
||||||
await refreshStatus();
|
await refreshStatus();
|
||||||
setupFooter();
|
setupFooter();
|
||||||
if (!bridge && state.status?.host === 'theseus') addBackToSidebar();
|
if (!bridge && state.status?.host === 'theseus' && !state.external) addBackToSidebar();
|
||||||
connectEvents();
|
connectEvents();
|
||||||
if (PATH_MODE) {
|
if (PATH_MODE) {
|
||||||
window.addEventListener('popstate', route);
|
window.addEventListener('popstate', route);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue