feat(tower): Merlin Studio web UI
This commit is contained in:
@@ -0,0 +1,533 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Merlin Studio</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #fdf7ff;
|
||||
--card: #ffffff;
|
||||
--violet: #7c4dff;
|
||||
--violet-d: #5e35b1;
|
||||
--orange: #ff8a3d;
|
||||
--orange-d: #f06c1d;
|
||||
--ink: #3a2e4d;
|
||||
--muted: #8a7ea0;
|
||||
--ok: #2e9e5b;
|
||||
--err: #d8324b;
|
||||
--line: #ece3f6;
|
||||
--radius: 18px;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font-family: "Comic Sans MS", "Trebuchet MS", system-ui, sans-serif;
|
||||
font-size: 17px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.wrap { max-width: 700px; margin: 0 auto; padding: 16px clamp(12px, 4vw, 24px) 60px; }
|
||||
header { text-align: center; padding: 18px 0 8px; }
|
||||
header h1 { margin: 0; font-size: 2rem; color: var(--violet-d); }
|
||||
header .sub { color: var(--orange-d); font-size: 1.1rem; font-weight: bold; }
|
||||
|
||||
.tabs { display: flex; gap: 8px; margin: 18px 0; }
|
||||
.tabs button {
|
||||
flex: 1; padding: 14px 8px; font-size: 1.05rem; cursor: pointer;
|
||||
border: none; border-radius: var(--radius); background: #efe7fb;
|
||||
color: var(--violet-d); font-family: inherit; font-weight: bold;
|
||||
transition: transform .08s ease;
|
||||
}
|
||||
.tabs button:active { transform: scale(0.97); }
|
||||
.tabs button.active { background: var(--violet); color: #fff; box-shadow: 0 4px 12px rgba(124,77,255,.35); }
|
||||
|
||||
section.panel { display: none; }
|
||||
section.panel.active { display: block; }
|
||||
|
||||
.card {
|
||||
background: var(--card); border: 2px solid var(--line); border-radius: var(--radius);
|
||||
padding: 16px; margin: 12px 0; box-shadow: 0 3px 10px rgba(124,77,255,.06);
|
||||
}
|
||||
.card h3 { margin: 0 0 10px; color: var(--violet-d); }
|
||||
.row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||||
.row.between { justify-content: space-between; }
|
||||
.grow { flex: 1 1 auto; min-width: 0; }
|
||||
|
||||
label { display: block; margin: 8px 0 4px; color: var(--muted); font-size: .95rem; }
|
||||
input[type=text], input[type=url], input[type=number], input[type=file] {
|
||||
width: 100%; padding: 12px; font-size: 1rem; font-family: inherit;
|
||||
border: 2px solid var(--line); border-radius: 12px; background: #fbf8ff; color: var(--ink);
|
||||
}
|
||||
input:focus { outline: none; border-color: var(--violet); }
|
||||
|
||||
button.btn {
|
||||
padding: 13px 18px; font-size: 1rem; font-family: inherit; font-weight: bold;
|
||||
border: none; border-radius: 14px; cursor: pointer; color: #fff; background: var(--violet);
|
||||
transition: transform .08s ease, opacity .15s ease;
|
||||
}
|
||||
button.btn:active { transform: scale(0.97); }
|
||||
button.btn.orange { background: var(--orange); }
|
||||
button.btn.ghost { background: #efe7fb; color: var(--violet-d); }
|
||||
button.btn:disabled { opacity: .45; cursor: not-allowed; }
|
||||
button.icon {
|
||||
border: none; background: transparent; cursor: pointer; font-size: 1.3rem;
|
||||
padding: 6px 8px; border-radius: 10px; line-height: 1;
|
||||
}
|
||||
button.icon:active { background: #f3eafe; }
|
||||
|
||||
.feedname { font-weight: bold; font-size: 1.1rem; }
|
||||
.badge {
|
||||
display: inline-block; background: #e3f6e9; color: var(--ok);
|
||||
border-radius: 10px; padding: 2px 8px; font-size: .8rem; font-weight: bold; margin-left: 6px;
|
||||
}
|
||||
ul.eps { list-style: none; margin: 12px 0 0; padding: 0; }
|
||||
ul.eps li { display: flex; align-items: flex-start; gap: 10px; padding: 9px 0; border-top: 1px solid var(--line); }
|
||||
ul.eps li label { margin: 0; color: var(--ink); flex: 1; cursor: pointer; }
|
||||
ul.eps input[type=checkbox] { width: 22px; height: 22px; margin-top: 2px; accent-color: var(--violet); flex: none; }
|
||||
.dur { color: var(--muted); font-size: .85rem; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { text-align: left; padding: 10px 6px; border-bottom: 1px solid var(--line); }
|
||||
th { color: var(--muted); font-size: .85rem; }
|
||||
td.num { text-align: right; white-space: nowrap; color: var(--muted); }
|
||||
.fname { word-break: break-all; }
|
||||
|
||||
.stat { display: flex; justify-content: space-between; padding: 10px 4px; border-bottom: 1px solid var(--line); }
|
||||
.stat .k { color: var(--muted); }
|
||||
.stat .v { font-weight: bold; }
|
||||
|
||||
.spinner { display: inline-block; width: 22px; height: 22px; border: 3px solid var(--line);
|
||||
border-top-color: var(--violet); border-radius: 50%; animation: spin .8s linear infinite; vertical-align: middle; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.loading { color: var(--muted); padding: 14px 0; }
|
||||
|
||||
#banner {
|
||||
display: none; background: #ffe2e6; color: var(--err); border: 2px solid #f7b9c3;
|
||||
border-radius: 14px; padding: 12px 14px; margin: 12px 0; font-weight: bold;
|
||||
}
|
||||
#banner.show { display: block; }
|
||||
|
||||
#toast {
|
||||
position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%) translateY(140%);
|
||||
background: var(--violet-d); color: #fff; padding: 14px 22px; border-radius: 16px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,.25); font-weight: bold; max-width: 90vw; text-align: center;
|
||||
transition: transform .3s ease; z-index: 50;
|
||||
}
|
||||
#toast.show { transform: translateX(-50%) translateY(0); }
|
||||
#toast.err { background: var(--err); }
|
||||
|
||||
.empty { color: var(--muted); text-align: center; padding: 18px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>
|
||||
<h1>🪄 Merlin Studio</h1>
|
||||
<div class="sub">Lisael Box</div>
|
||||
</header>
|
||||
|
||||
<div id="banner"></div>
|
||||
|
||||
<div class="tabs">
|
||||
<button id="tab-podcasts" class="active" onclick="showTab('podcasts')">🎧 Podcasts</button>
|
||||
<button id="tab-fichiers" onclick="showTab('fichiers')">📁 Fichiers</button>
|
||||
<button id="tab-box" onclick="showTab('box')">📦 Box</button>
|
||||
</div>
|
||||
|
||||
<!-- ===== PODCASTS ===== -->
|
||||
<section id="panel-podcasts" class="panel active">
|
||||
<div id="feeds"><div class="loading"><span class="spinner"></span> chargement…</div></div>
|
||||
|
||||
<div class="card">
|
||||
<h3>+ Ajouter un flux</h3>
|
||||
<label for="f-name">Nom de l'émission</label>
|
||||
<input type="text" id="f-name" placeholder="Une histoire et Oli">
|
||||
<label for="f-rss">URL du flux RSS</label>
|
||||
<input type="url" id="f-rss" placeholder="https://…/rss.xml">
|
||||
<label for="f-n">Nombre d'épisodes récents</label>
|
||||
<input type="number" id="f-n" value="2" min="1" max="20">
|
||||
<div class="row" style="margin-top:12px">
|
||||
<button class="btn" onclick="addFeed()">Ajouter</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== FICHIERS ===== -->
|
||||
<section id="panel-fichiers" class="panel">
|
||||
<div class="card">
|
||||
<h3>Fichiers sur le staging</h3>
|
||||
<div id="files"><div class="loading"><span class="spinner"></span> chargement…</div></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>⬆️ Ajouter un fichier audio</h3>
|
||||
<label for="u-file">Fichier audio (MP3)</label>
|
||||
<input type="file" id="u-file" accept="audio/*">
|
||||
<label for="u-show">Émission</label>
|
||||
<input type="text" id="u-show" value="Perso">
|
||||
<label for="u-title">Titre</label>
|
||||
<input type="text" id="u-title" placeholder="Mon histoire">
|
||||
<div class="row" style="margin-top:12px">
|
||||
<button id="u-btn" class="btn orange" onclick="uploadFile()">Envoyer</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== BOX ===== -->
|
||||
<section id="panel-box" class="panel">
|
||||
<div class="card">
|
||||
<h3>État de la box</h3>
|
||||
<div id="boxinfo"><div class="loading"><span class="spinner"></span> chargement…</div></div>
|
||||
<div class="row" style="margin-top:14px">
|
||||
<button id="push-btn" class="btn orange" onclick="pushNow()" disabled>📤 Pousser maintenant</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="toast"></div>
|
||||
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
// ---------- helpers ----------
|
||||
function $(id) { return document.getElementById(id); }
|
||||
|
||||
function esc(s) {
|
||||
return String(s == null ? "" : s)
|
||||
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
||||
.replace(/"/g, """).replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function showBanner(msg) {
|
||||
const b = $("banner");
|
||||
b.textContent = "⚠️ " + msg;
|
||||
b.classList.add("show");
|
||||
}
|
||||
function clearBanner() { $("banner").classList.remove("show"); }
|
||||
|
||||
let toastTimer = null;
|
||||
function toast(msg, isErr) {
|
||||
const t = $("toast");
|
||||
t.textContent = msg;
|
||||
t.classList.toggle("err", !!isErr);
|
||||
t.classList.add("show");
|
||||
if (toastTimer) clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(function () { t.classList.remove("show"); }, 3200);
|
||||
}
|
||||
|
||||
// All API calls go through here: same-origin credentials (browser handles Basic Auth),
|
||||
// JSON parsing, and uniform error surfacing in the banner.
|
||||
async function api(path, opts) {
|
||||
opts = opts || {};
|
||||
opts.credentials = "same-origin";
|
||||
const res = await fetch(path, opts);
|
||||
let data = null;
|
||||
const ct = res.headers.get("content-type") || "";
|
||||
if (ct.indexOf("application/json") !== -1) {
|
||||
try { data = await res.json(); } catch (e) { data = null; }
|
||||
}
|
||||
if (!res.ok) {
|
||||
const detail = (data && data.error) ? data.error : ("HTTP " + res.status);
|
||||
const err = new Error(detail);
|
||||
err.status = res.status;
|
||||
throw err;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function humanSize(n) {
|
||||
n = Number(n) || 0;
|
||||
if (n >= 1048576) return (n / 1048576).toFixed(1) + " Mo";
|
||||
if (n >= 1024) return (n / 1024).toFixed(0) + " Ko";
|
||||
return n + " o";
|
||||
}
|
||||
|
||||
// ---------- tabs ----------
|
||||
const TABS = ["podcasts", "fichiers", "box"];
|
||||
function showTab(name) {
|
||||
TABS.forEach(function (t) {
|
||||
$("panel-" + t).classList.toggle("active", t === name);
|
||||
$("tab-" + t).classList.toggle("active", t === name);
|
||||
});
|
||||
if (name === "fichiers") loadFiles();
|
||||
if (name === "box") loadBox();
|
||||
}
|
||||
|
||||
// ---------- Podcasts ----------
|
||||
async function loadFeeds() {
|
||||
const box = $("feeds");
|
||||
box.innerHTML = '<div class="loading"><span class="spinner"></span> chargement…</div>';
|
||||
try {
|
||||
clearBanner();
|
||||
const feeds = await api("/api/feeds");
|
||||
if (!feeds || !feeds.length) {
|
||||
box.innerHTML = '<div class="card empty">Aucun flux pour le moment.</div>';
|
||||
return;
|
||||
}
|
||||
box.innerHTML = "";
|
||||
feeds.forEach(function (f) {
|
||||
const card = document.createElement("div");
|
||||
card.className = "card";
|
||||
card.innerHTML =
|
||||
'<div class="row between">' +
|
||||
'<span class="feedname">' + esc(f.name) + '</span>' +
|
||||
'<button class="icon" title="Supprimer ce flux">✕</button>' +
|
||||
'</div>' +
|
||||
'<div class="row" style="margin-top:10px">' +
|
||||
'<button class="btn ghost">👀 Voir les épisodes</button>' +
|
||||
'</div>' +
|
||||
'<div class="eps-host"></div>';
|
||||
const delBtn = card.querySelector(".icon");
|
||||
const seeBtn = card.querySelector(".btn.ghost");
|
||||
const host = card.querySelector(".eps-host");
|
||||
delBtn.onclick = function () { removeFeed(f.key, f.name); };
|
||||
seeBtn.onclick = function () { loadEpisodes(f.key, host, seeBtn); };
|
||||
box.appendChild(card);
|
||||
});
|
||||
} catch (e) {
|
||||
box.innerHTML = '<div class="card empty">Impossible de charger les flux.</div>';
|
||||
showBanner("Flux: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function addFeed() {
|
||||
const name = $("f-name").value.trim();
|
||||
const rss = $("f-rss").value.trim();
|
||||
const n = parseInt($("f-n").value, 10) || 2;
|
||||
if (!name || !rss) { toast("Nom et URL requis", true); return; }
|
||||
// derive a stable key from the name (server keys feeds by this)
|
||||
let key = name.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "")
|
||||
.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 24);
|
||||
if (!key) key = "flux-" + Date.now();
|
||||
try {
|
||||
await api("/api/feeds", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ key: key, name: name, rss: rss, n: n }),
|
||||
});
|
||||
$("f-name").value = ""; $("f-rss").value = ""; $("f-n").value = "2";
|
||||
toast("Flux ajouté ✓");
|
||||
loadFeeds();
|
||||
} catch (e) {
|
||||
toast("Erreur: " + e.message, true);
|
||||
showBanner("Ajout flux: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeFeed(key, name) {
|
||||
if (!confirm("Supprimer le flux « " + name + " » ?")) return;
|
||||
try {
|
||||
await api("/api/feeds/" + encodeURIComponent(key), { method: "DELETE" });
|
||||
toast("Flux supprimé");
|
||||
loadFeeds();
|
||||
} catch (e) {
|
||||
toast("Erreur: " + e.message, true);
|
||||
showBanner("Suppression flux: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEpisodes(key, host, btn) {
|
||||
host.innerHTML = '<div class="loading"><span class="spinner"></span> chargement des épisodes (1-2 s)…</div>';
|
||||
btn.disabled = true;
|
||||
try {
|
||||
clearBanner();
|
||||
const data = await api("/api/episodes?feed=" + encodeURIComponent(key));
|
||||
const eps = (data && data.episodes) || [];
|
||||
if (!eps.length) {
|
||||
host.innerHTML = '<div class="empty">Aucun épisode trouvé.</div>';
|
||||
return;
|
||||
}
|
||||
const ul = document.createElement("ul");
|
||||
ul.className = "eps";
|
||||
eps.forEach(function (ep, i) {
|
||||
const li = document.createElement("li");
|
||||
const cbId = "ep_" + key + "_" + i;
|
||||
const dur = ep.duration ? '<span class="dur"> · ' + esc(ep.duration) + '</span>' : "";
|
||||
const badge = ep.staged ? '<span class="badge">✓ déjà sur la box</span>' : "";
|
||||
li.innerHTML =
|
||||
'<input type="checkbox" id="' + cbId + '"' + (ep.staged ? " disabled" : "") + '>' +
|
||||
'<label for="' + cbId + '">' + esc(ep.title) + dur + badge + '</label>';
|
||||
const cb = li.querySelector("input");
|
||||
cb.dataset.url = ep.url;
|
||||
cb.dataset.title = ep.title;
|
||||
cb.dataset.staged = ep.staged ? "1" : "";
|
||||
ul.appendChild(li);
|
||||
});
|
||||
host.innerHTML = "";
|
||||
host.appendChild(ul);
|
||||
const action = document.createElement("div");
|
||||
action.className = "row";
|
||||
action.style.marginTop = "12px";
|
||||
const send = document.createElement("button");
|
||||
send.className = "btn orange";
|
||||
send.textContent = "✨ Mettre sur la box";
|
||||
send.onclick = function () { selectEpisodes(key, ul, send); };
|
||||
action.appendChild(send);
|
||||
host.appendChild(action);
|
||||
} catch (e) {
|
||||
host.innerHTML = '<div class="empty">Erreur de chargement.</div>';
|
||||
showBanner("Épisodes: " + e.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function selectEpisodes(key, ul, btn) {
|
||||
const chosen = [];
|
||||
ul.querySelectorAll("input[type=checkbox]").forEach(function (cb) {
|
||||
if (cb.checked && !cb.dataset.staged) {
|
||||
chosen.push({ url: cb.dataset.url, title: cb.dataset.title });
|
||||
}
|
||||
});
|
||||
if (!chosen.length) { toast("Coche au moins un épisode", true); return; }
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const r = await api("/api/select", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ feed: key, episodes: chosen }),
|
||||
});
|
||||
const n = (r && r.queued != null) ? r.queued : chosen.length;
|
||||
toast(n + " épisode(s) en cours de préparation…");
|
||||
// transcode runs server-side; refresh the Fichiers list shortly
|
||||
setTimeout(loadFiles, 3000);
|
||||
} catch (e) {
|
||||
toast("Erreur: " + e.message, true);
|
||||
showBanner("Sélection: " + e.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Fichiers ----------
|
||||
async function loadFiles() {
|
||||
const box = $("files");
|
||||
box.innerHTML = '<div class="loading"><span class="spinner"></span> chargement…</div>';
|
||||
try {
|
||||
clearBanner();
|
||||
const files = await api("/api/files");
|
||||
if (!files || !files.length) {
|
||||
box.innerHTML = '<div class="empty">Aucun fichier sur le staging.</div>';
|
||||
return;
|
||||
}
|
||||
let html = '<table><thead><tr><th>Nom</th><th>Taille</th><th>Type</th><th></th></tr></thead><tbody>';
|
||||
files.forEach(function (f) {
|
||||
html +=
|
||||
'<tr>' +
|
||||
'<td class="fname">' + esc(f.name) + '</td>' +
|
||||
'<td class="num">' + humanSize(f.size) + '</td>' +
|
||||
'<td>' + esc(f.type) + '</td>' +
|
||||
'<td class="num"><button class="icon" data-name="' + esc(f.name) + '" title="Supprimer">🗑</button></td>' +
|
||||
'</tr>';
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
box.innerHTML = html;
|
||||
box.querySelectorAll("button.icon").forEach(function (b) {
|
||||
b.onclick = function () { removeFile(b.dataset.name); };
|
||||
});
|
||||
} catch (e) {
|
||||
box.innerHTML = '<div class="empty">Impossible de charger les fichiers.</div>';
|
||||
showBanner("Fichiers: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeFile(name) {
|
||||
if (!confirm("Supprimer « " + name + " » ?")) return;
|
||||
try {
|
||||
await api("/api/files/" + encodeURIComponent(name), { method: "DELETE" });
|
||||
toast("Fichier supprimé");
|
||||
loadFiles();
|
||||
} catch (e) {
|
||||
toast("Erreur: " + e.message, true);
|
||||
showBanner("Suppression fichier: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadFile() {
|
||||
const input = $("u-file");
|
||||
const file = input.files && input.files[0];
|
||||
if (!file) { toast("Choisis un fichier audio", true); return; }
|
||||
const show = $("u-show").value.trim() || "Perso";
|
||||
const title = $("u-title").value.trim() || file.name.replace(/\.[^.]+$/, "");
|
||||
const btn = $("u-btn");
|
||||
btn.disabled = true;
|
||||
const oldLabel = btn.textContent;
|
||||
btn.textContent = "Envoi…";
|
||||
try {
|
||||
const buf = await file.arrayBuffer();
|
||||
const qs = "?show=" + encodeURIComponent(show) + "&title=" + encodeURIComponent(title);
|
||||
const r = await api("/api/upload" + qs, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "audio/mpeg" },
|
||||
body: buf,
|
||||
});
|
||||
toast("Envoyé ✓ " + ((r && r.file) ? r.file : ""));
|
||||
input.value = "";
|
||||
$("u-title").value = "";
|
||||
loadFiles();
|
||||
} catch (e) {
|
||||
toast("Erreur: " + e.message, true);
|
||||
showBanner("Upload: " + e.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = oldLabel;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Box ----------
|
||||
async function loadBox() {
|
||||
const box = $("boxinfo");
|
||||
const pushBtn = $("push-btn");
|
||||
box.innerHTML = '<div class="loading"><span class="spinner"></span> chargement…</div>';
|
||||
pushBtn.disabled = true;
|
||||
try {
|
||||
clearBanner();
|
||||
const data = await api("/api/box");
|
||||
const b = (data && data.box) || {};
|
||||
const delta = (data && data.delta) || [];
|
||||
const have = (b.have || []);
|
||||
const ip = b.ip || null;
|
||||
box.innerHTML =
|
||||
'<div class="stat"><span class="k">Adresse IP</span><span class="v">' + esc(ip || "jamais annoncée") + '</span></div>' +
|
||||
'<div class="stat"><span class="k">Dernière annonce</span><span class="v">' + esc(b.last_seen || "—") + '</span></div>' +
|
||||
'<div class="stat"><span class="k">Fichiers sur la box</span><span class="v">' + have.length + '</span></div>' +
|
||||
'<div class="stat"><span class="k">En attente d\'envoi</span><span class="v">' + delta.length + '</span></div>';
|
||||
pushBtn.disabled = !ip;
|
||||
} catch (e) {
|
||||
box.innerHTML = '<div class="empty">Impossible de lire l\'état de la box.</div>';
|
||||
showBanner("Box: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function pushNow() {
|
||||
const btn = $("push-btn");
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const r = await api("/api/push", { method: "POST" });
|
||||
if (r && r.started) {
|
||||
toast("Envoi démarré (" + (r.delta || 0) + " fichier(s))");
|
||||
} else {
|
||||
toast("Rien à envoyer ou envoi déjà en cours");
|
||||
}
|
||||
setTimeout(loadBox, 1500);
|
||||
} catch (e) {
|
||||
if (e.status === 409) {
|
||||
toast("La box ne s'est jamais annoncée", true);
|
||||
} else {
|
||||
toast("Erreur: " + e.message, true);
|
||||
showBanner("Push: " + e.message);
|
||||
}
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- init ----------
|
||||
window.addEventListener("DOMContentLoaded", function () {
|
||||
loadFeeds();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user