Havre: update pill (NEW VERSION · RELOAD) #215

Merged
teahubxyz merged 3 commits from havre-update-pill into gh-pages 2026-10-21 19:39:36 +00:00
3 changed files with 45 additions and 0 deletions

No files matched your search

+1
View File
@@ -23,6 +23,7 @@
<link rel="alternate" type="text/markdown" href="skill.md" title="Skill">
<link rel="stylesheet" href="style.css">
<script type="module" src="app.js"></script>
<script src="update.js" defer></script>
</head>
<body>
<a class="skip" href="#chat">skip to the chat</a>
+7
View File
@@ -551,6 +551,13 @@ p.note { padding: 8px 10px; border: 1px solid var(--ink); background: var(--hi);
.lefthead [role=tab] { font-size: 19px; }
.lefthead .sep::before { font-size: 19px; }
}
/* a new version is out: a bar at the top to reload (update.js) */
#upd {
position: fixed; top: calc(10px + env(safe-area-inset-top)); left: 50%; transform: translateX(-50%); z-index: 30;
padding: 9px 16px; border: 1px solid var(--paper); border-radius: 0; background: var(--ink); color: var(--paper);
font: 700 13px/1 var(--mono); letter-spacing: .12em; white-space: nowrap; cursor: pointer;
}
#upd:hover, #upd:focus-visible { background: var(--hi); color: var(--ink); border-color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
* { scroll-behavior: auto !important; }
}
+37
View File
@@ -0,0 +1,37 @@
// update.js: an installed Havre has no address bar to refresh from, so the page notices when a new version has been
// published and offers a reload, like gandora's pill. It fingerprints the files the app is made of when it opens,
// checks again whenever it comes back to the foreground (and every ten minutes), and shows the pill when they differ.
// Checking fetches with cache: "reload", which also refreshes the browser's copies, so the reload gets the new files.
(() => {
const FILES = ["./", "app.js", "style.css", "workflows.js", "abilities.js", "card.js", "venice.js", "memory.js",
"threads.js", "media.js", "backup.js", "theme.js"];
let base = null, shown = false, busy = false;
async function print() {
const parts = await Promise.all(FILES.map((f) => fetch(f, { cache: "reload" }).then((r) => (r.ok ? r.text() : "")).catch(() => null)));
if (parts.some((p) => p === null)) return null; // offline: no verdict
const d = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(parts.join("\u0000")));
return Array.from(new Uint8Array(d), (b) => b.toString(16).padStart(2, "0")).join("");
}
function show() {
if (shown) return;
shown = true;
const b = document.createElement("button");
b.id = "upd"; b.type = "button"; b.textContent = "NEW VERSION · RELOAD";
b.addEventListener("click", () => location.reload());
document.body.append(b);
}
async function check() {
if (busy || shown || !crypto.subtle) return;
busy = true;
try {
const now = await print();
if (!now) return;
if (!base) base = now;
else if (now !== base) show();
} finally { busy = false; }
}
if (location.protocol !== "https:") return;
setTimeout(check, 3000);
setInterval(check, 10 * 60 * 1000);
document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") check(); });
})();