mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-19 04:40:32 -05:00
96955aee62
Add VersionService to fetch and cache the latest GitHub release, compare it to the installed semver (APP_VERSION when valid, else setup.py), and expose admin-only GET /api/version/check and POST /api/version/dismiss on the legacy /api blueprint (session or Bearer token). Persist per-user dismissal in users.dismissed_release_version (Alembic 148) and show a non-blocking update card in base.html for administrators. Add packaging for semver parsing and tests for comparison, service, and routes. Document configuration in docs/admin/deployment/VERSION_MANAGEMENT.md and endpoints in docs/api/REST_API.md and docs/API.md.
157 lines
4.8 KiB
JavaScript
157 lines
4.8 KiB
JavaScript
/**
|
|
* Admin-only: fetch /api/version/check and show a non-blocking update card.
|
|
*/
|
|
(function () {
|
|
var LS_KEY = "tt_dismissed_release_version";
|
|
var NOTE_PREVIEW_LEN = 280;
|
|
|
|
function getCsrfToken() {
|
|
var m = document.querySelector('meta[name="csrf-token"]');
|
|
return m ? m.getAttribute("content") || "" : "";
|
|
}
|
|
|
|
function localDismissedMatches(latest) {
|
|
try {
|
|
return latest && localStorage.getItem(LS_KEY) === latest;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function setLocalDismissed(latest) {
|
|
try {
|
|
if (latest) localStorage.setItem(LS_KEY, latest);
|
|
} catch (e) {}
|
|
}
|
|
|
|
function hide(root) {
|
|
if (root) root.classList.add("hidden");
|
|
}
|
|
|
|
function show(root) {
|
|
if (root) root.classList.remove("hidden");
|
|
}
|
|
|
|
function postDismiss(latest, onDone) {
|
|
fetch("/api/version/dismiss", {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-CSRFToken": getCsrfToken(),
|
|
},
|
|
body: JSON.stringify({ latest_version: latest }),
|
|
})
|
|
.then(function (r) {
|
|
return r.json().then(function (j) {
|
|
return { ok: r.ok, json: j };
|
|
});
|
|
})
|
|
.then(function (res) {
|
|
if (typeof onDone === "function") onDone(res.ok);
|
|
})
|
|
.catch(function () {
|
|
if (typeof onDone === "function") onDone(false);
|
|
});
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
var root = document.getElementById("adminVersionUpdateRoot");
|
|
if (!root) return;
|
|
|
|
fetch("/api/version/check", { credentials: "same-origin" })
|
|
.then(function (r) {
|
|
if (r.status === 401 || r.status === 403) return null;
|
|
return r.json();
|
|
})
|
|
.then(function (data) {
|
|
if (!data || !data.latest_version) return;
|
|
if (localDismissedMatches(data.latest_version)) return;
|
|
if (!data.update_available) return;
|
|
|
|
var title = document.getElementById("adminVersionUpdateTitle");
|
|
var published = document.getElementById("adminVersionUpdatePublished");
|
|
var notesEl = document.getElementById("adminVersionUpdateNotes");
|
|
var readMore = document.getElementById("adminVersionUpdateReadMore");
|
|
var viewLink = document.getElementById("adminVersionUpdateViewRelease");
|
|
var closeBtn = document.getElementById("adminVersionUpdateClose");
|
|
var dismissBtn = document.getElementById("adminVersionUpdateDismiss");
|
|
var dismissVerBtn = document.getElementById("adminVersionUpdateDismissVersion");
|
|
|
|
if (title) {
|
|
title.textContent =
|
|
String.fromCodePoint(0x1f680) + " New version available: " + data.latest_version;
|
|
}
|
|
|
|
if (published) {
|
|
if (data.published_at) {
|
|
try {
|
|
var d = new Date(data.published_at);
|
|
published.textContent = d.toLocaleString(undefined, {
|
|
dateStyle: "medium",
|
|
timeStyle: "short",
|
|
});
|
|
} catch (e) {
|
|
published.textContent = data.published_at;
|
|
}
|
|
} else {
|
|
published.textContent = "";
|
|
}
|
|
}
|
|
|
|
var notes = data.release_notes || "";
|
|
var expanded = false;
|
|
function renderNotes() {
|
|
if (!notesEl) return;
|
|
if (!notes) {
|
|
notesEl.textContent = "";
|
|
if (readMore) readMore.classList.add("hidden");
|
|
return;
|
|
}
|
|
if (expanded || notes.length <= NOTE_PREVIEW_LEN) {
|
|
notesEl.textContent = notes;
|
|
if (readMore) readMore.classList.add("hidden");
|
|
} else {
|
|
notesEl.textContent = notes.slice(0, NOTE_PREVIEW_LEN).trimEnd() + "\u2026";
|
|
if (readMore) {
|
|
readMore.classList.remove("hidden");
|
|
readMore.onclick = function () {
|
|
expanded = true;
|
|
notesEl.textContent = notes;
|
|
readMore.classList.add("hidden");
|
|
};
|
|
}
|
|
}
|
|
}
|
|
renderNotes();
|
|
|
|
if (viewLink) {
|
|
if (data.release_url) {
|
|
viewLink.href = data.release_url;
|
|
viewLink.classList.remove("pointer-events-none", "opacity-50");
|
|
} else {
|
|
viewLink.href = "#";
|
|
viewLink.classList.add("pointer-events-none", "opacity-50");
|
|
}
|
|
}
|
|
|
|
function wireClose() {
|
|
hide(root);
|
|
}
|
|
if (closeBtn) closeBtn.addEventListener("click", wireClose);
|
|
if (dismissBtn) dismissBtn.addEventListener("click", wireClose);
|
|
if (dismissVerBtn) {
|
|
dismissVerBtn.addEventListener("click", function () {
|
|
postDismiss(data.latest_version, function () {
|
|
hide(root);
|
|
setLocalDismissed(data.latest_version);
|
|
});
|
|
});
|
|
}
|
|
|
|
show(root);
|
|
})
|
|
.catch(function () {});
|
|
});
|
|
})();
|