feat: organizer Android app and token-gated upload API
CI / lint-and-test (push) Failing after 1m18s
CI / lint-and-test (push) Failing after 1m18s
Replace Syncthing as the product upload path with a share-target app and POST /api/jobs. Web UI live-polls job progress. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
/* live.f12.rocks — live poll + lightbox gallery. No framework. */
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var POLL_MS = 4000;
|
||||
|
||||
function $(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
/* --- live reload while jobs run --- */
|
||||
|
||||
function fingerprint(payload) {
|
||||
if (!payload) return "";
|
||||
if (payload.job_id) {
|
||||
return [
|
||||
payload.status,
|
||||
payload.stage,
|
||||
payload.updated_at,
|
||||
payload.variant_count,
|
||||
(payload.intermediates || []).length,
|
||||
payload.error || "",
|
||||
].join("|");
|
||||
}
|
||||
var jobs = payload.jobs || [];
|
||||
return jobs
|
||||
.map(function (j) {
|
||||
return [j.job_id, j.status, j.updated_at, j.variant_count, j.stage].join(":");
|
||||
})
|
||||
.join(",");
|
||||
}
|
||||
|
||||
function startLivePoll() {
|
||||
if (!document.body.hasAttribute("data-live")) return;
|
||||
var jobId = document.body.getAttribute("data-job-id");
|
||||
var url = jobId ? "/api/jobs/" + encodeURIComponent(jobId) : "/api/jobs";
|
||||
var last = "";
|
||||
var first = true;
|
||||
|
||||
function tick() {
|
||||
fetch(url, { headers: { Accept: "application/json" } })
|
||||
.then(function (r) {
|
||||
if (!r.ok) throw new Error("poll " + r.status);
|
||||
return r.json();
|
||||
})
|
||||
.then(function (data) {
|
||||
var next = fingerprint(data);
|
||||
if (first) {
|
||||
last = next;
|
||||
first = false;
|
||||
return;
|
||||
}
|
||||
if (next !== last) {
|
||||
window.location.reload();
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
/* venue wifi: try again next tick */
|
||||
});
|
||||
}
|
||||
|
||||
tick();
|
||||
setInterval(tick, POLL_MS);
|
||||
}
|
||||
|
||||
/* --- lightbox with prev/next --- */
|
||||
|
||||
function startLightbox() {
|
||||
var box = $("lightbox");
|
||||
var img = $("lightbox-img");
|
||||
var caption = $("lightbox-caption");
|
||||
if (!box || !img) return;
|
||||
|
||||
var items = [];
|
||||
var index = 0;
|
||||
|
||||
function collect() {
|
||||
items = Array.prototype.slice
|
||||
.call(document.querySelectorAll("img.lightboxable"))
|
||||
.map(function (el) {
|
||||
return {
|
||||
src: el.getAttribute("data-full-src") || el.currentSrc || el.src,
|
||||
alt: el.alt || "",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function show(i) {
|
||||
if (!items.length) return;
|
||||
index = (i + items.length) % items.length;
|
||||
img.src = items[index].src;
|
||||
img.alt = items[index].alt;
|
||||
if (caption) caption.textContent = items[index].alt;
|
||||
box.hidden = false;
|
||||
document.body.classList.add("lightbox-open");
|
||||
}
|
||||
|
||||
function close() {
|
||||
box.hidden = true;
|
||||
img.removeAttribute("src");
|
||||
document.body.classList.remove("lightbox-open");
|
||||
}
|
||||
|
||||
document.addEventListener("click", function (e) {
|
||||
var target = e.target;
|
||||
if (!(target instanceof HTMLImageElement)) return;
|
||||
if (!target.classList.contains("lightboxable")) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
collect();
|
||||
var full = target.getAttribute("data-full-src") || target.currentSrc || target.src;
|
||||
var found = items.findIndex(function (it) {
|
||||
return it.src === full;
|
||||
});
|
||||
show(found >= 0 ? found : 0);
|
||||
});
|
||||
|
||||
box.addEventListener("click", function (e) {
|
||||
if (e.target === box || (e.target && e.target.classList.contains("lightbox-close"))) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
if (e.target && e.target.classList.contains("lightbox-next")) {
|
||||
show(index + 1);
|
||||
}
|
||||
if (e.target && e.target.classList.contains("lightbox-prev")) {
|
||||
show(index - 1);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (box.hidden) return;
|
||||
if (e.key === "Escape") close();
|
||||
if (e.key === "ArrowRight") show(index + 1);
|
||||
if (e.key === "ArrowLeft") show(index - 1);
|
||||
});
|
||||
|
||||
var startX = 0;
|
||||
box.addEventListener(
|
||||
"touchstart",
|
||||
function (e) {
|
||||
if (e.changedTouches && e.changedTouches[0]) startX = e.changedTouches[0].screenX;
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
box.addEventListener(
|
||||
"touchend",
|
||||
function (e) {
|
||||
if (!e.changedTouches || !e.changedTouches[0]) return;
|
||||
var dx = e.changedTouches[0].screenX - startX;
|
||||
if (Math.abs(dx) < 40) return;
|
||||
show(index + (dx < 0 ? 1 : -1));
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
}
|
||||
|
||||
startLivePoll();
|
||||
startLightbox();
|
||||
})();
|
||||
Reference in New Issue
Block a user