Theseus: bundle Pithos 0.3.8

This commit is contained in:
Local Dev 2026-10-04 02:38:21 +02:00
parent 6b9ada6dbd
commit 155b445c68
6 changed files with 139 additions and 1 deletions

View file

@ -1,7 +1,7 @@
{
"id": "pithos",
"name": "Pithos",
"version": "0.3.7",
"version": "0.3.8",
"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=",

View file

@ -65,6 +65,13 @@ export async function createPithos(opts = {}) {
const tickets = new Map();
const subscribers = new Set();
let versionCache = null;
// Pithos's own version: addon.json in the Theseus add-on, package.json elsewhere.
const pithosVersion = (() => {
for (const f of ['../addon.json', '../package.json']) {
try { const v = JSON.parse(fs.readFileSync(new URL(f, import.meta.url), 'utf8')).version; if (v) return v; } catch {}
}
return null;
})();
const broadcast = (event, data) => {
const msg = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
@ -144,6 +151,7 @@ export async function createPithos(opts = {}) {
const v = await version();
return {
host: hostName,
pithos: pithosVersion,
daemon: daemon.status(),
external: await probeExternal(c),
s3d: { ...v, pinned: S3D_VERSION, installable: !!assetForPlatform(), outdated: !!(v?.version && cmpVersion(v.version, S3D_VERSION) < 0) },

View file

@ -117,6 +117,22 @@ module.exports = {
return { url: p.viewUrl(apiPath) };
});
// Footer updater: stage the newest signed Pithos from the extension
// channel, then (second click) ask Theseus to restart and apply it.
// Theseus asks the user before restarting. Older Theseus builds lack
// these hooks; the panel then points to Settings > Extensions.
api.onMessage("update", async ({ step = "stage" } = {}) => {
if (step === "apply") {
if (typeof api.restartApp !== "function") return { fallback: "settings" };
return await api.restartApp();
}
if (typeof api.checkAndStageSelfUpdate !== "function") return { fallback: "settings" };
const r = await api.checkAndStageSelfUpdate();
const staged = r?.status === "staged" || r?.status === "already-staged";
return { staged, status: r?.status || "unknown", detail: r?.detail || null, current: r?.current || null, next: r?.next || null };
});
api.onMessage("open-settings", ({ section = "" } = {}) => { api.openSettings?.(String(section)); return { ok: true }; });
// A full browser-tab view, for anyone who prefers it to the sidebar.
api.onMessage("open-in-tab", async () => {
const p = await ensureServer();

View file

@ -351,3 +351,27 @@ body.has-player .toasts { bottom: 84px; }
.pl-btn { width: 28px; height: 28px; font-size: 14px; }
.pl-main { width: 34px; height: 34px; }
}
/* Footer: pithos.sia, the version and the updater (like Aegis's) */
.brandfoot { position: fixed; left: 0; right: 0; bottom: 0; z-index: 14; height: 28px; display: flex; align-items: center;
justify-content: space-between; padding: 0 12px; background: var(--surface); border-top: 1px solid var(--border);
font-size: 11.5px; color: var(--muted); }
@media (min-width: 761px) { .brandfoot { left: 220px; } }
.brandlink { display: inline-flex; align-items: center; gap: 5px; color: var(--muted); text-decoration: none; padding: 2px 4px; border-radius: 4px; }
.brandlink:hover { color: var(--accent); }
.brandfoot-right { display: inline-flex; align-items: center; gap: 8px; }
.brandver { font-variant-numeric: tabular-nums; letter-spacing: .3px; }
.brandcheck { appearance: none; background: none; border: 0; color: var(--muted); cursor: pointer; padding: 0 2px; font: inherit;
font-size: 13px; line-height: 1; opacity: .75; }
.brandcheck:hover { color: var(--accent); opacity: 1; }
.brandcheck.spin { animation: spin 1s linear infinite; }
.brandupd { appearance: none; border: 0; font: inherit; font-size: 11.5px; font-weight: 600; padding: 2px 9px; border-radius: 999px;
background: var(--accent-soft); color: var(--accent); cursor: pointer; white-space: nowrap; }
.brandupd:disabled { cursor: default; }
.brandupd.ok, .brandupd.wait { background: var(--surface-2); color: var(--muted); font-weight: 500; }
.brandupd.err { background: var(--danger-soft); color: var(--danger); font-weight: 500; }
body.has-foot main { padding-bottom: 64px; }
body.has-foot .player { bottom: 28px; }
body.has-foot .toasts { bottom: 44px; }
body.has-foot.has-player main { padding-bottom: 124px; }
body.has-foot.has-player .toasts { bottom: 112px; }

View file

@ -1752,6 +1752,87 @@ $('#unlock-btn').addEventListener('click', async () => {
}
});
// ---- Footer: pithos.sia, the version, and (in Theseus) the updater -----------
// Like Aegis: checks the extension channel when Pithos opens and every few
// 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';
function cmpSemver(a, b) {
const pa = String(a || '0').split('.').map((n) => Number(n) || 0);
const pb = String(b || '0').split('.').map((n) => Number(n) || 0);
for (let i = 0; i < Math.max(pa.length, pb.length); i++) { const d = (pa[i] || 0) - (pb[i] || 0); if (d) return d < 0 ? -1 : 1; }
return 0;
}
function setupFooter() {
const foot = $('#brandfoot');
const ver = $('#brandver');
const chip = $('#brandupd');
const check = $('#brandcheck');
const current = state.status?.pithos || null;
ver.textContent = current ? `v${current}` : '';
foot.hidden = false;
document.body.classList.add('has-foot');
if (bridge) {
$('#brandlink').addEventListener('click', (e) => { e.preventDefault(); bridge.invoke('open-external', { url: 'https://pithos.sia/' }).catch(fail); });
}
if (!bridge || !current) return; // only the Theseus add-on updates itself
let latest = null;
let busy = false;
const showChip = (text, cls, title, onclick) => {
chip.hidden = false; ver.hidden = true;
chip.className = `brandupd${cls ? ' ' + cls : ''}`;
chip.textContent = text; chip.title = title || '';
chip.disabled = !onclick; chip.onclick = onclick || null;
};
const rest = () => { chip.hidden = true; ver.hidden = false; };
const flash = (text, cls, title) => { showChip(text, cls, title, null); setTimeout(() => { if (!busy && chip.classList.contains(cls)) paint(); }, 2500); };
function paint() {
if (latest && cmpSemver(latest, current) > 0) showChip(`↑ v${latest}`, '', `Pithos v${latest} is available. Click to install it.`, install);
else rest();
}
async function scan(manual) {
check.classList.add('spin');
try {
const r = await fetch(`${UPDATES_URL}?t=${Math.floor(Date.now() / 60000)}`, { cache: 'no-store' });
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const list = (await r.json())?.addons || [];
latest = list.reduce((best, e) => (!best || cmpSemver(e.version, best) > 0 ? e.version : best), null);
if (busy) return;
if (manual && !(latest && cmpSemver(latest, current) > 0)) flash('✓ Up to date', 'ok', `Pithos v${current} is the latest release`);
else paint();
} catch (e) {
if (manual) flash('⚠ Check failed', 'err', e.message);
} finally { check.classList.remove('spin'); }
}
async function install() {
busy = true;
showChip('Downloading…', 'wait', 'Downloading and verifying the signed release', null);
try {
const r = await bridge.invoke('update', { step: 'stage' });
if (r?.fallback) {
busy = false;
return showChip('Update in Settings', '', 'This Theseus updates extensions in Settings › Extensions', () => bridge.invoke('open-settings', { section: 'addons' }).catch(fail));
}
if (!r?.staged) {
busy = false;
return r?.status === 'up-to-date' ? flash('✓ Up to date', 'ok', '') : flash('⚠ Update failed', 'err', r?.detail || r?.status || '');
}
const next = r.next ? ` v${r.next}` : '';
showChip(`↻ Restart to apply${next}`, '', `Pithos${next} is ready. Theseus restarts to apply it and reopens your tabs.`, async () => {
try {
const a = await bridge.invoke('update', { step: 'apply' });
if (a?.deferred) showChip(`Applies on next start${next}`, 'ok', 'Pithos updates the next time Theseus starts', null);
} catch (e) { flash('⚠ Restart failed', 'err', e.message); }
});
} catch (e) { busy = false; flash('⚠ Update failed', 'err', e.message); }
}
check.hidden = false;
check.addEventListener('click', () => scan(true));
setTimeout(() => scan(false), 1500);
setInterval(() => scan(false), 6 * 3600 * 1000);
}
// The full-page tab opened from the Theseus panel: one button back.
function addBackToSidebar() {
const row = h('div', { class: 'host-controls' },
@ -1789,6 +1870,7 @@ async function boot() {
state.logSeq = state.logs.at(-1)?.seq || 0;
} catch {}
await refreshStatus();
setupFooter();
if (!bridge && state.status?.host === 'theseus') addBackToSidebar();
connectEvents();
window.addEventListener('hashchange', route);

View file

@ -46,6 +46,14 @@
<main id="main"></main>
</div>
<footer class="brandfoot" id="brandfoot" hidden>
<a class="brandlink" id="brandlink" href="https://pithos.sia/" target="_blank" rel="noopener" title="Open pithos.sia"><img src="icon.svg" alt="" width="13" height="13"><span>pithos.sia</span></a>
<span class="brandfoot-right">
<button class="brandcheck" id="brandcheck" type="button" title="Check for updates" aria-label="Check for updates" hidden>↻</button>
<button class="brandupd" id="brandupd" type="button" hidden></button>
<span class="brandver" id="brandver"></span>
</span>
</footer>
<div id="modal-root"></div>
<div id="toasts" class="toasts" aria-live="polite"></div>
<script type="module" src="app.js"></script>