From 29ab1a5b9bfbb5bb0cfa88f7830ff16613bd7ca2 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Sun, 4 Oct 2026 22:02:15 +0200 Subject: [PATCH] Pithos 0.3.17: Windows desktop app with update notices; accounts named after your Sia Storage email --- bundled-addons/pithos/addon.json | 2 +- bundled-addons/pithos/core/server.js | 21 +++++++- bundled-addons/pithos/ui/app.js | 74 ++++++++++++++++++++++++++-- 3 files changed, 92 insertions(+), 5 deletions(-) diff --git a/bundled-addons/pithos/addon.json b/bundled-addons/pithos/addon.json index 722f1592..4c5a8704 100644 --- a/bundled-addons/pithos/addon.json +++ b/bundled-addons/pithos/addon.json @@ -1,7 +1,7 @@ { "id": "pithos", "name": "Pithos", - "version": "0.3.16", + "version": "0.3.17", "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", "icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIwIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM2RkYzk3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMWU4ZjZhIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTIyIDZoMjB2NWMwIDItMiAzLTIgNSA4IDMgMTQgMTEgMTQgMjEgMCAxMi05IDIxLTIyIDIxUzEwIDQ5IDEwIDM3YzAtMTAgNi0xOCAxNC0yMSAwLTItMi0zLTItNXoiIGZpbGw9InVybCgjZykiLz48cGF0aCBkPSJNMTQgMzBoMzZNMTMgNDBoMzgiIHN0cm9rZT0iIzBiMWYxOCIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9Ii41NSIvPjwvc3ZnPgo=", diff --git a/bundled-addons/pithos/core/server.js b/bundled-addons/pithos/core/server.js index d571687f..527fd9c6 100644 --- a/bundled-addons/pithos/core/server.js +++ b/bundled-addons/pithos/core/server.js @@ -164,6 +164,8 @@ export async function createPithos(opts = {}) { const local = { host: hostName, pithos: pithosVersion, + // electron-builder's portable exe sets this; updates then fetch the portable build. + portable: hostName === 'desktop' && !!process.env.PORTABLE_EXECUTABLE_FILE, daemon: daemon.status(), external: await probeExternal(c), s3d: { ...v, pinned: S3D_VERSION, installable: !!assetForPlatform(), outdated: !!(v?.version && cmpVersion(v.version, S3D_VERSION) < 0) }, @@ -531,6 +533,20 @@ export async function createPithos(opts = {}) { // ---- free space on the Sia account ---------------------------------------- // From the indexer (signed with s3d's app key). Cached for a minute. let spaceCache = { at: 0, value: null }; + // ---- desktop updates: the newest Pithos desktop build on dl.silentmode.st ---- + const RELEASES_URL = process.env.PITHOS_RELEASES_URL || 'https://dl.silentmode.st/releases-manifest.json'; + let releaseCache = null; + route('GET', '/api/desktop-release', async (req, url) => { + if (releaseCache && Date.now() - releaseCache.at < 10 * 60_000 && url.searchParams.get('fresh') !== '1') return releaseCache.body; + const res = await fetch(`${RELEASES_URL}?t=${Math.floor(Date.now() / 60000)}`, { cache: 'no-store', signal: AbortSignal.timeout(15_000) }).catch((e) => { throw new HttpError(502, e.message); }); + if (!res.ok) throw new HttpError(502, `the release list answered ${res.status}`); + const list = ((await res.json())?.releases || []).filter((e) => e.id === 'pithos-desktop' && /^\d+\.\d+\.\d+$/.test(e.version)); + const latest = list.reduce((best, e) => (!best || cmpVersion(e.version, best.version) > 0 ? e : best), null); + const body = { latest: latest && { version: latest.version, date: latest.date || null, changes: String(latest.changes || '').slice(0, 600) } }; + releaseCache = { at: Date.now(), body }; + return body; + }); + route('GET', '/api/space', async () => { // 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; @@ -883,7 +899,10 @@ export async function createPithos(opts = {}) { login.cancel(); for (const s of subscribers) s.end(); await daemon.stop(); - await new Promise((r) => server.close(r)); + // Idle keep-alive sockets would hold close() for seconds; nobody is left to answer. + const closed = new Promise((r) => server.close(r)); + server.closeAllConnections?.(); + await closed; }, }; } diff --git a/bundled-addons/pithos/ui/app.js b/bundled-addons/pithos/ui/app.js index c8ec2609..b3bee57a 100644 --- a/bundled-addons/pithos/ui/app.js +++ b/bundled-addons/pithos/ui/app.js @@ -158,7 +158,13 @@ const eventHandlers = { if (state.logs.length > 2000) state.logs.shift(); logSink?.(l); }, - login(d) { state.login = d; emit(); }, + login(d) { + const was = state.login?.state; + state.login = d; + emit(); + // the account card names the new account as soon as it is connected + if (['done', 'already'].includes(d?.state) && d.state !== was) refreshNavAccount(true); + }, install(p) { if (p.phase === 'extract') toast('Verified checksum, installing…'); }, move(p) { progressSink?.(p); }, convert(p) { progressSink?.(p); }, @@ -432,6 +438,32 @@ function getStarted(main) { ); const go = (n) => { wiz.step = Math.max(0, Math.min(STEPS.length - 1, n)); draw(); }; + + // Sia Storage never tells an app who signed in, so the person types the + // email while they are signing in there, and it becomes the account label. + // One input lives across redraws, so typing survives the login events. + function emailField() { + if (!wiz.emailInput) { + const inp = h('input', { type: 'email', placeholder: 'you@example.com', maxlength: 120, autocomplete: 'email' }); + let timer; + const save = () => { + clearTimeout(timer); + const v = inp.value.trim(); + if (v === (wiz.savedEmail || '')) return; + wiz.savedEmail = v; + api('PUT', '/api/account/label', { label: v }).then(() => refreshNavAccount(true)).catch(() => {}); + }; + inp.addEventListener('input', () => { clearTimeout(timer); timer = setTimeout(save, 800); }); + inp.addEventListener('change', save); + wiz.emailInput = inp; + } + const inp = wiz.emailInput; + if (document.activeElement === inp) queueMicrotask(() => inp.focus()); + return h('label', { class: 'field', style: 'max-width:420px;margin-top:16px' }, + h('span', {}, 'Your Sia Storage email (optional)'), + inp, + h('small', {}, 'Sia Storage does not tell apps who you are, so Pithos names this account with the email you type here. You can change it later on Accounts.')); + } const nav = (opts = {}) => h('div', { class: 'row wizard-nav' }, wiz.step > 0 && wiz.step < STEPS.length - 1 && h('button', { class: 'btn', disabled: opts.backDisabled, onclick: () => go(wiz.step - 1) }, 'Back'), h('span', { style: 'flex:1' }), @@ -661,6 +693,7 @@ function getStarted(main) { h('div', { class: 'row' }, h('button', { class: wiz.autoOpened === false ? 'btn primary' : 'btn', onclick: () => openLink(l.approvalUrl) }, wiz.autoOpened === false ? 'Open the approval page ↗' : 'Open the approval page again ↗'), h('button', { class: 'btn small', onclick: () => copy(l.approvalUrl) }, 'Copy link')), + emailField(), h('p', { class: 'muted row', style: 'margin-top:16px' }, h('span', { class: 'spinner' }), ' Waiting for your approval…'), nav({ next: false, extra: h('button', { class: 'btn danger', onclick: () => api('POST', '/api/login/cancel').then(() => go(3)).catch(fail) }, 'Cancel') }), ]; @@ -807,7 +840,7 @@ function setup(main) { h('label', { class: 'field', style: 'max-width:420px' }, h('span', {}, 'Account 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(); 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', {}, 'Sia Storage does not tell apps your email, so name the account 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); } }), h('div', { class: 'row', style: 'margin-top:16px' }, daemonState() !== 'running' && h('button', { class: 'btn primary', onclick: async () => { await daemonAction(daemonState() === 'crashed' ? 'restart' : 'start'); navTo('#/overview'); } }, 'Start s3d'), @@ -902,7 +935,7 @@ function setup(main) { await refreshStatus(); 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, emailInput: null, savedEmail: '' }); navTo('#/start'); } catch (e) { fail(e); } } @@ -2498,6 +2531,40 @@ $('#unlock-btn').addEventListener('click', async () => { // hours; a newer release replaces the version with "↑ vX". One click // downloads and verifies it, the next restarts Theseus to apply it. const UPDATES_URL = 'https://navigate.st/bns/theseus.x/extensions/pithos/updates.json'; + +// The desktop app: newer builds are listed in Silent Mode's release manifest +// (read by the local server). The chip downloads the matching installer, or +// the portable exe, in the browser. +function desktopUpdates(current, { ver, chip, check }) { + const file = state.status?.portable ? 'Pithos-portable.exe' : 'Pithos-Setup.exe'; + const rest = () => { chip.hidden = true; ver.hidden = false; }; + const flash = (text, cls, title) => { + chip.hidden = false; ver.hidden = true; chip.className = `brandupd ${cls}`; chip.textContent = text; chip.title = title || ''; chip.disabled = true; chip.onclick = null; + setTimeout(() => paint(), 2500); + }; + let latest = null; + function paint() { + if (!latest || cmpSemver(latest.version, current) <= 0) return rest(); + chip.hidden = false; ver.hidden = true; chip.className = 'brandupd'; chip.disabled = false; + chip.textContent = `↑ v${latest.version}`; + chip.title = `Pithos v${latest.version} is available${latest.changes ? ': ' + latest.changes : ''}. Click to download it, then run it to update.`; + chip.onclick = () => window.open(`https://dl.silentmode.st/latest/${file}`, '_blank', 'noopener'); + } + async function scan(manual) { + check.classList.add('spin'); + try { + latest = (await api('GET', `/api/desktop-release${manual ? '?fresh=1' : ''}`)).latest; + if (manual && !(latest && cmpSemver(latest.version, current) > 0)) flash('✓ Up to date', 'ok', `Pithos v${current} is the latest desktop release`); + else paint(); + } catch (e) { + if (manual) flash('⚠ Check failed', 'err', e.message); + } finally { check.classList.remove('spin'); } + } + check.hidden = false; + check.addEventListener('click', () => scan(true)); + setTimeout(() => scan(false), 1500); + setInterval(() => scan(false), 6 * 3600 * 1000); +} // s3d's S3 port is reachable only from this computer (the default). function localOnly() { const a = state.status?.config?.apiAddress || ''; @@ -2521,6 +2588,7 @@ function setupFooter() { if (bridge) { $('#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 || !current) return; // only the Theseus add-on updates itself let latest = null;