Public Access
A Zola site in site/, from the design: both themes on the device's palette, notched shapes, self-hosted fonts, the wordmark and icons, two generated hero drawings, nine App cards (the mesh messenger marked planned), real screenshots, the updates and build sections. The Install page builds ESP Web Tools' manifest in the browser from the Gitea API (it needs Caddy to allow the origin), refuses any download that isn't on the project's server, and falls back to the esptool steps. Downloads lists the releases. The focus ring shows on notched controls. A page checker fails the build if a page loads from another origin. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
70 lines
3.4 KiB
JavaScript
70 lines
3.4 KiB
JavaScript
// The Install page: asks the project's Gitea for the latest release, and gives ESP Web Tools a
|
|
// manifest for its factory image, built here, so the page offers a new release as soon as it exists
|
|
// and nothing is copied to this site (docs/milestones/W1.md, Q180). Gitea sits behind Caddy, which
|
|
// allows this origin to read the release API and the downloads.
|
|
(function () {
|
|
var box = document.getElementById("flasher");
|
|
if (!box) return;
|
|
var $ = function (id) { return document.getElementById(id); };
|
|
var status = $("flasher-status");
|
|
var apiOrigin = new URL(box.dataset.api).origin;
|
|
var downloads = box.dataset.releasesPath;
|
|
|
|
// A network error (which is also what a browser makes of a missing CORS header) and a release this
|
|
// page won't offer are different things to tell the visitor.
|
|
function fail(error) {
|
|
var unreachable = error instanceof TypeError;
|
|
status.textContent = (unreachable ? "Couldn't reach the release server (" + error.message + ")" : "This page can't offer the latest release (" + error.message + ")") +
|
|
". Use the esptool steps below, or take the file from the downloads page.";
|
|
status.className = "msg";
|
|
}
|
|
function human(bytes) {
|
|
return bytes >= 1048576 ? (bytes / 1048576).toFixed(2) + " MB" : Math.round(bytes / 1024) + " KB";
|
|
}
|
|
// Only a download from the project's own server, for this repository, is ever handed to the flasher.
|
|
function trusted(url) {
|
|
try {
|
|
var u = new URL(url);
|
|
return u.origin === apiOrigin && u.pathname.indexOf(downloads) === 0 && u.pathname.indexOf("..") < 0 && !u.search;
|
|
} catch (e) { return false; }
|
|
}
|
|
|
|
fetch(box.dataset.api + "/releases/latest", { headers: { Accept: "application/json" } })
|
|
.then(function (r) {
|
|
if (!r.ok) throw new Error("answer " + r.status);
|
|
return r.json();
|
|
})
|
|
.then(function (rel) {
|
|
var assets = rel.assets || [];
|
|
var bin = assets.filter(function (a) { return /-factory\.bin$/.test(a.name); })[0];
|
|
if (!bin) throw new Error("the latest release has no factory image");
|
|
if (!trusted(bin.browser_download_url)) throw new Error("its download isn't on the project's server");
|
|
var sums = assets.filter(function (a) { return a.name === "SHA256SUMS" && trusted(a.browser_download_url); })[0];
|
|
|
|
$("f-version").textContent = rel.tag_name;
|
|
$("f-size").textContent = human(bin.size);
|
|
if (rel.html_url) $("f-link").href = rel.html_url;
|
|
var manifest = {
|
|
name: "roro9stack",
|
|
version: rel.tag_name,
|
|
builds: [{ chipFamily: "ESP32-S3", parts: [{ path: bin.browser_download_url, offset: 0 }] }]
|
|
};
|
|
var url = URL.createObjectURL(new Blob([JSON.stringify(manifest)], { type: "application/json" }));
|
|
var button = $("f-button");
|
|
button.setAttribute("manifest", url);
|
|
button.hidden = false;
|
|
$("flasher-info").hidden = false;
|
|
status.textContent = "Latest release: " + rel.tag_name + ".";
|
|
|
|
if (!sums) { $("f-sha").textContent = "not published"; return; }
|
|
return fetch(sums.browser_download_url)
|
|
.then(function (r) { return r.ok ? r.text() : ""; })
|
|
.then(function (text) {
|
|
var line = text.split("\n").filter(function (l) { return l.indexOf(bin.name) >= 0; })[0];
|
|
$("f-sha").textContent = line ? line.split(/\s+/)[0] : "not listed";
|
|
})
|
|
.catch(function () { $("f-sha").textContent = "unavailable"; });
|
|
})
|
|
.catch(fail);
|
|
})();
|