Theseus: bundle Pithos 0.3.13
This commit is contained in:
parent
0ce2ab95dd
commit
f2913691aa
6 changed files with 129 additions and 16 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "pithos",
|
"id": "pithos",
|
||||||
"name": "Pithos",
|
"name": "Pithos",
|
||||||
"version": "0.3.12",
|
"version": "0.3.13",
|
||||||
"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=",
|
||||||
|
|
|
||||||
|
|
@ -62,12 +62,24 @@ export class Daemon extends EventEmitter {
|
||||||
|
|
||||||
logsSince(seq = 0) { return this.logs.filter((l) => l.seq > seq); }
|
logsSince(seq = 0) { return this.logs.filter((l) => l.seq > seq); }
|
||||||
|
|
||||||
|
// s3d migrates its database when it starts, and so does every one-shot
|
||||||
|
// command (users list, keys…) that opens it. Two of them at once failed
|
||||||
|
// with "table users already exists", so a start waits for running
|
||||||
|
// commands to finish, and commands wait while s3d is starting.
|
||||||
start() {
|
start() {
|
||||||
if (!this.binary) throw new Error('s3d binary not found');
|
if (!this.binary) throw new Error('s3d binary not found');
|
||||||
if (this.child) return this.status();
|
if (this.child || this.pendingStart) return this.status();
|
||||||
this.exitCode = null;
|
this.exitCode = null;
|
||||||
this.needsLogin = false;
|
this.needsLogin = false;
|
||||||
this.setState('starting');
|
this.setState('starting');
|
||||||
|
if (this.cliRuns > 0) {
|
||||||
|
this.pendingStart = true;
|
||||||
|
this.once('cli-idle', () => { this.pendingStart = false; if (this.state === 'starting' && !this.child) this.spawnServer(); });
|
||||||
|
} else this.spawnServer();
|
||||||
|
return this.status();
|
||||||
|
}
|
||||||
|
|
||||||
|
spawnServer() {
|
||||||
const child = spawn(this.binary, [], {
|
const child = spawn(this.binary, [], {
|
||||||
cwd: this.cwd(), env: this.env(), stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true,
|
cwd: this.cwd(), env: this.env(), stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true,
|
||||||
});
|
});
|
||||||
|
|
@ -91,6 +103,7 @@ export class Daemon extends EventEmitter {
|
||||||
|
|
||||||
async stop() {
|
async stop() {
|
||||||
const child = this.child;
|
const child = this.child;
|
||||||
|
if (!child && this.pendingStart) { this.pendingStart = false; this.setState('stopped'); return this.status(); }
|
||||||
if (!child) return this.status();
|
if (!child) return this.status();
|
||||||
this.setState('stopping');
|
this.setState('stopping');
|
||||||
const exited = new Promise((r) => child.once('exit', r));
|
const exited = new Promise((r) => child.once('exit', r));
|
||||||
|
|
@ -103,15 +116,32 @@ export class Daemon extends EventEmitter {
|
||||||
|
|
||||||
// One-shot CLI run. Resolves { code, stdout, stderr }; never rejects on a
|
// One-shot CLI run. Resolves { code, stdout, stderr }; never rejects on a
|
||||||
// non-zero exit so callers can surface s3d's own error text.
|
// non-zero exit so callers can surface s3d's own error text.
|
||||||
run(args, { timeout = 30_000 } = {}) {
|
async run(args, { timeout = 30_000 } = {}) {
|
||||||
if (!this.binary) return Promise.reject(new Error('s3d binary not found'));
|
if (!this.binary) throw new Error('s3d binary not found');
|
||||||
return new Promise((resolve, reject) => {
|
if (this.state === 'starting') await this.settled(20_000);
|
||||||
execFile(this.binary, args, {
|
this.cliRuns = (this.cliRuns || 0) + 1;
|
||||||
cwd: this.cwd(), env: this.env(), timeout, windowsHide: true, maxBuffer: 16 << 20,
|
try {
|
||||||
}, (err, stdout, stderr) => {
|
return await new Promise((resolve, reject) => {
|
||||||
if (err && typeof err.code !== 'number') return reject(err);
|
execFile(this.binary, args, {
|
||||||
resolve({ code: err ? err.code : 0, stdout: stdout.replace(ANSI, ''), stderr: stderr.replace(ANSI, '') });
|
cwd: this.cwd(), env: this.env(), timeout, windowsHide: true, maxBuffer: 16 << 20,
|
||||||
|
}, (err, stdout, stderr) => {
|
||||||
|
if (err && typeof err.code !== 'number') return reject(err);
|
||||||
|
resolve({ code: err ? err.code : 0, stdout: stdout.replace(ANSI, ''), stderr: stderr.replace(ANSI, '') });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
if (--this.cliRuns === 0) this.emit('cli-idle');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolves once s3d is no longer starting (or after ms).
|
||||||
|
settled(ms) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (this.state !== 'starting') return resolve();
|
||||||
|
const done = () => { clearTimeout(t); this.off('status', on); resolve(); };
|
||||||
|
const on = (st) => { if (st.state !== 'starting') done(); };
|
||||||
|
const t = setTimeout(done, ms);
|
||||||
|
this.on('status', on);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -340,7 +340,9 @@ export async function createPithos(opts = {}) {
|
||||||
const autoFlush = createAutoFlush({
|
const autoFlush = createAutoFlush({
|
||||||
settings: () => {
|
settings: () => {
|
||||||
const p = readPrefs(configFile);
|
const p = readPrefs(configFile);
|
||||||
return { enabled: p.autoFlush === true, minutes: p.autoFlushMinutes ?? DEFAULT_MINUTES };
|
// On unless the user turned it off: files left in the buffer otherwise
|
||||||
|
// never reach Sia, and nobody expects to have to push them.
|
||||||
|
return { enabled: p.autoFlush !== false, minutes: p.autoFlushMinutes ?? DEFAULT_MINUTES };
|
||||||
},
|
},
|
||||||
// Only an s3d that is up can be asked; a stopped one just means no flush.
|
// Only an s3d that is up can be asked; a stopped one just means no flush.
|
||||||
stats: () => (daemon.state === 'starting' || daemon.state === 'stopping'
|
stats: () => (daemon.state === 'starting' || daemon.state === 'stopping'
|
||||||
|
|
|
||||||
|
|
@ -421,3 +421,24 @@ details.more-opts > summary:hover { color: var(--text); }
|
||||||
.space { margin-top: 6px; max-width: 420px; }
|
.space { margin-top: 6px; max-width: 420px; }
|
||||||
.space-bar { margin-top: 4px; height: 6px; }
|
.space-bar { margin-top: 4px; height: 6px; }
|
||||||
.space-bar.full > i { background: var(--warn); }
|
.space-bar.full > i { background: var(--warn); }
|
||||||
|
|
||||||
|
/* Navigation: the Accounts card first, then pages, then the s3d group */
|
||||||
|
nav a.nav-account { display: flex; flex-direction: column; gap: 2px; padding: 9px 10px; margin-bottom: 6px;
|
||||||
|
border: 1px solid var(--border); border-radius: 10px; background: var(--surface-2); min-width: 0; }
|
||||||
|
nav a.nav-account:hover { border-color: var(--accent); background: var(--surface-2); }
|
||||||
|
nav a.nav-account.active { border-color: var(--accent); background: var(--accent-soft); color: var(--text); font-weight: 400; }
|
||||||
|
.na-label { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }
|
||||||
|
nav a.nav-account.active .na-label { color: var(--accent); }
|
||||||
|
.na-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.na-space { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--muted); margin-top: 2px; }
|
||||||
|
.na-space .bar { height: 4px; }
|
||||||
|
.na-space .bar.full > i { background: var(--warn); }
|
||||||
|
.nav-group { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); padding: 12px 10px 4px; }
|
||||||
|
|
||||||
|
/* Full page / Back to sidebar as an icon beside the menu button (compact bar) */
|
||||||
|
.bar-btn { display: none; }
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.bar-btn { display: inline-grid; place-items: center; width: 34px; height: 34px; border: 1px solid var(--border); border-radius: 8px;
|
||||||
|
background: var(--surface); color: var(--text); font: inherit; font-size: 16px; cursor: pointer; }
|
||||||
|
.bar-btn:hover { border-color: var(--accent); color: var(--accent); }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,8 @@ function route() {
|
||||||
for (const a of document.querySelectorAll('#nav a')) a.classList.toggle('active', a.dataset.view === navView);
|
for (const a of document.querySelectorAll('#nav a')) a.classList.toggle('active', a.dataset.view === navView);
|
||||||
// Compact bar (narrow hosts): name the page, fold the menu away.
|
// Compact bar (narrow hosts): name the page, fold the menu away.
|
||||||
const here = document.querySelector(`#nav a[data-view="${navView}"]`);
|
const here = document.querySelector(`#nav a[data-view="${navView}"]`);
|
||||||
$('#here').textContent = here ? here.textContent : '';
|
$('#here').textContent = here ? (here.dataset.title || here.textContent) : '';
|
||||||
|
refreshNavAccount();
|
||||||
setMenu(false);
|
setMenu(false);
|
||||||
const main = $('#main');
|
const main = $('#main');
|
||||||
put(main);
|
put(main);
|
||||||
|
|
@ -254,6 +255,7 @@ function overview(main) {
|
||||||
main.append(
|
main.append(
|
||||||
h('div', { class: 'page-head' }, h('div', {}, h('h1', {}, 'Overview'), h('p', { class: 'muted' }, 'Your s3d gateway and its upload pipeline to Sia.'))),
|
h('div', { class: 'page-head' }, h('div', {}, h('h1', {}, 'Overview'), h('p', { class: 'muted' }, 'Your s3d gateway and its upload pipeline to Sia.'))),
|
||||||
banners,
|
banners,
|
||||||
|
pendingBanner(),
|
||||||
h('div', { class: 'grid two' }, daemonCard, statsCard),
|
h('div', { class: 'grid two' }, daemonCard, statsCard),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -729,6 +731,7 @@ function otherAccounts(after) {
|
||||||
toast(`Switched to ${name}`);
|
toast(`Switched to ${name}`);
|
||||||
await refreshStatus();
|
await refreshStatus();
|
||||||
await loadAccount();
|
await loadAccount();
|
||||||
|
refreshNavAccount(true);
|
||||||
after();
|
after();
|
||||||
} catch (e) { fail(e); }
|
} catch (e) { fail(e); }
|
||||||
}
|
}
|
||||||
|
|
@ -767,7 +770,7 @@ function setup(main) {
|
||||||
h('p', {}, l.indexerUrl ? ['This s3d uses a Sia account on ', h('code', {}, l.indexerUrl), '. All S3 users, drives and files on it are stored, and paid for, through that account.'] : 'This s3d is registered with an indexer.'),
|
h('p', {}, l.indexerUrl ? ['This s3d uses a Sia account on ', h('code', {}, l.indexerUrl), '. All S3 users, drives and files on it are stored, and paid for, through that account.'] : 'This s3d is registered with an indexer.'),
|
||||||
h('label', { class: 'field', style: 'max-width:420px' }, h('span', {}, 'Account label'),
|
h('label', { class: 'field', style: 'max-width:420px' }, h('span', {}, 'Account label'),
|
||||||
h('div', { class: 'row', style: 'flex-wrap:nowrap' }, label,
|
h('div', { class: 'row', style: 'flex-wrap:nowrap' }, label,
|
||||||
h('button', { class: 'btn', onclick: async () => { try { await api('PUT', '/api/account/label', { label: label.value }); await loadAccount(); toast('Label saved'); } catch (e) { fail(e); } } }, 'Save')),
|
h('button', { class: 'btn', onclick: async () => { try { await api('PUT', '/api/account/label', { label: label.value }); await loadAccount(); refreshNavAccount(true); toast('Label saved'); } catch (e) { fail(e); } } }, 'Save')),
|
||||||
h('small', {}, 'Pithos cannot see the login email, so name it here: the email you approved the connection with is a good label.')),
|
h('small', {}, 'Pithos cannot see the login email, so name it here: the email you approved the connection with is a good label.')),
|
||||||
siaAccountBlock(accountInfo, async () => { try { accountInfo = await api('GET', '/api/account?fresh=1'); draw(); } catch (e) { fail(e); } }),
|
siaAccountBlock(accountInfo, async () => { try { accountInfo = await api('GET', '/api/account?fresh=1'); draw(); } catch (e) { fail(e); } }),
|
||||||
h('div', { class: 'row', style: 'margin-top:16px' },
|
h('div', { class: 'row', style: 'margin-top:16px' },
|
||||||
|
|
@ -863,6 +866,7 @@ function setup(main) {
|
||||||
toast(`Previous account set aside in ${r.archived}`);
|
toast(`Previous account set aside in ${r.archived}`);
|
||||||
await refreshStatus();
|
await refreshStatus();
|
||||||
await loadAccount();
|
await loadAccount();
|
||||||
|
refreshNavAccount(true);
|
||||||
Object.assign(wiz, { step: 1, accountReady: false, openedUrl: null, key: null });
|
Object.assign(wiz, { step: 1, accountReady: false, openedUrl: null, key: null });
|
||||||
navTo('#/start');
|
navTo('#/start');
|
||||||
} catch (e) { fail(e); }
|
} catch (e) { fail(e); }
|
||||||
|
|
@ -1282,6 +1286,50 @@ function siaAccountBlock(a, onRefresh) {
|
||||||
'The same phrase approved from two different logins makes two different accounts; the same phrase from the same login is the same account, even on another computer.'));
|
'The same phrase approved from two different logins makes two different accounts; the same phrase from the same login is the same account, even on another computer.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "N files are waiting to upload to Sia" with Upload now. s3d holds small
|
||||||
|
// uploads until about 40 MB has built up; until then they exist only on this
|
||||||
|
// computer, which is easy to miss.
|
||||||
|
function pendingBanner() {
|
||||||
|
const el = h('div', { hidden: true });
|
||||||
|
const st = daemonState();
|
||||||
|
if (st !== 'running' && st !== 'external') return el;
|
||||||
|
Promise.all([api('GET', '/api/stats'), api('GET', '/api/autoflush').catch(() => null)]).then(([x, af]) => {
|
||||||
|
if (!x?.pendingObjects) return;
|
||||||
|
const n = x.pendingObjects;
|
||||||
|
const when = af?.enabled
|
||||||
|
? (af.dueAt ? `They go up by themselves in about ${Math.max(1, Math.ceil((af.dueAt - Date.now()) / 60_000))} min if nothing new arrives.` : `They go up by themselves after ${af.minutes} min with no new uploads.`)
|
||||||
|
: 'They go up only when about 40 MB has built up, or when you upload them now.';
|
||||||
|
const btn = h('button', { class: 'btn primary small', onclick: async () => {
|
||||||
|
btn.disabled = true; put(btn, h('span', { class: 'spinner' }), ' Uploading…');
|
||||||
|
try { await api('POST', '/api/flush'); toast('Your files are on Sia'); el.hidden = true; }
|
||||||
|
catch (e) { fail(e); btn.disabled = false; put(btn, 'Upload now'); }
|
||||||
|
} }, 'Upload now');
|
||||||
|
put(el, h('div', { class: 'banner info' },
|
||||||
|
h('span', {}, h('strong', {}, `${n.toLocaleString()} file${n === 1 ? '' : 's'} (${fmtBytes(x.pendingSize)}) waiting to upload to Sia. `),
|
||||||
|
h('span', { class: 'small' }, `Until then they are only on this computer. ${when}`)),
|
||||||
|
btn));
|
||||||
|
el.hidden = false;
|
||||||
|
}).catch(() => {});
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Accounts card at the top of the navigation: which Sia account this is
|
||||||
|
// and how much room it has left. Refreshed at most every 30 s as pages change.
|
||||||
|
let navAccountAt = 0;
|
||||||
|
function refreshNavAccount(force) {
|
||||||
|
if (!force && Date.now() - navAccountAt < 30_000) return;
|
||||||
|
navAccountAt = Date.now();
|
||||||
|
loadAccount().then((a) => { $('#na-name').textContent = a?.registered ? accountName(a) : 'Not connected yet'; }).catch(() => {});
|
||||||
|
api('GET', '/api/space').then((s) => {
|
||||||
|
const el = $('#na-space');
|
||||||
|
if (!s?.available || !(s.maxPinnedData > 0)) { el.hidden = true; return; }
|
||||||
|
const pct = Math.min(100, Math.round((s.pinnedData / s.maxPinnedData) * 100));
|
||||||
|
put(el, h('span', {}, `${fmtBytes(s.remainingStorage)} free of ${fmtBytes(s.maxPinnedData)}`),
|
||||||
|
h('span', { class: `bar${pct >= 90 ? ' full' : ''}` }, h('i', { style: `width:${Math.max(pct, s.pinnedData > 0 ? 2 : 0)}%` })));
|
||||||
|
el.hidden = false;
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
// Space on the Sia account: "12.4 GiB of 46.6 GiB used · 34.2 GiB free" and a bar.
|
// Space on the Sia account: "12.4 GiB of 46.6 GiB used · 34.2 GiB free" and a bar.
|
||||||
function spaceLine() {
|
function spaceLine() {
|
||||||
const el = h('div', { class: 'space', hidden: true });
|
const el = h('div', { class: 'space', hidden: true });
|
||||||
|
|
@ -1311,7 +1359,7 @@ async function bucketList(head, body, user, userSel) {
|
||||||
h('div', { class: 'row' }, userPicker(userSel), h('button', { class: 'btn primary', onclick: create }, 'New drive')));
|
h('div', { class: 'row' }, userPicker(userSel), h('button', { class: 'btn primary', onclick: create }, 'New drive')));
|
||||||
const card = h('div', { class: 'card' }, h('p', { class: 'muted' }, 'Loading…'));
|
const card = h('div', { class: 'card' }, h('p', { class: 'muted' }, 'Loading…'));
|
||||||
const bar = h('div', { class: 'view-bar' });
|
const bar = h('div', { class: 'view-bar' });
|
||||||
put(body, bar, card);
|
put(body, pendingBanner(), bar, card);
|
||||||
const base = `/api/s3/${encodeURIComponent(user)}/buckets`;
|
const base = `/api/s3/${encodeURIComponent(user)}/buckets`;
|
||||||
let mode = viewMode('drives', 'icons');
|
let mode = viewMode('drives', 'icons');
|
||||||
let list = [];
|
let list = [];
|
||||||
|
|
@ -2304,6 +2352,12 @@ function addSidebarControls() {
|
||||||
} }, '⤢ Full page'));
|
} }, '⤢ Full page'));
|
||||||
$('.sidebar').insertBefore(row, $('#daemon-pill'));
|
$('.sidebar').insertBefore(row, $('#daemon-pill'));
|
||||||
addMenuAction('⤢ Open as full page', () => row.querySelector('button').click());
|
addMenuAction('⤢ Open as full page', () => row.querySelector('button').click());
|
||||||
|
addBarButton('⤢', 'Open as full page', () => row.querySelector('button').click());
|
||||||
|
}
|
||||||
|
|
||||||
|
// A one-click icon in the compact top bar, beside ☰ (narrow layouts only).
|
||||||
|
function addBarButton(icon, label, run) {
|
||||||
|
$('.sidebar').insertBefore(h('button', { type: 'button', class: 'bar-btn', title: label, 'aria-label': label, onclick: run }, icon), $('#nav-toggle'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// An action at the end of the ☰ menu (narrow layouts only; wide ones show
|
// An action at the end of the ☰ menu (narrow layouts only; wide ones show
|
||||||
|
|
@ -2445,6 +2499,7 @@ function addBackToSidebar() {
|
||||||
} }, '⇤ Back to sidebar'));
|
} }, '⇤ Back to sidebar'));
|
||||||
$('.sidebar').insertBefore(row, $('#daemon-pill'));
|
$('.sidebar').insertBefore(row, $('#daemon-pill'));
|
||||||
addMenuAction('⇤ Back to sidebar', () => row.querySelector('button').click());
|
addMenuAction('⇤ Back to sidebar', () => row.querySelector('button').click());
|
||||||
|
addBarButton('⇤', 'Back to sidebar', () => row.querySelector('button').click());
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSignin() {
|
function showSignin() {
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,15 @@
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<div class="brand"><img src="icon.svg" alt="" width="28" height="28"><span>Pithos</span><span class="here" id="here"></span></div>
|
<div class="brand"><img src="icon.svg" alt="" width="28" height="28"><span>Pithos</span><span class="here" id="here"></span></div>
|
||||||
<nav id="nav">
|
<nav id="nav">
|
||||||
|
<a href="#/account" data-view="account" data-title="Accounts" class="nav-account" title="Your Sia account: the storage everything here is kept in">
|
||||||
|
<span class="na-label">Accounts</span>
|
||||||
|
<span class="na-name" id="na-name">Sia account</span>
|
||||||
|
<span class="na-space" id="na-space" hidden></span>
|
||||||
|
</a>
|
||||||
<a href="#/overview" data-view="overview">Overview</a>
|
<a href="#/overview" data-view="overview">Overview</a>
|
||||||
<a href="#/buckets" data-view="buckets">Drives</a>
|
|
||||||
<a href="#/users" data-view="users">Users & keys</a>
|
<a href="#/users" data-view="users">Users & keys</a>
|
||||||
<a href="#/account" data-view="account">Account</a>
|
<a href="#/buckets" data-view="buckets">Drives</a>
|
||||||
|
<div class="nav-group" role="presentation">s3d</div>
|
||||||
<a href="#/settings" data-view="settings">Settings</a>
|
<a href="#/settings" data-view="settings">Settings</a>
|
||||||
<a href="#/logs" data-view="logs">Logs</a>
|
<a href="#/logs" data-view="logs">Logs</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue