#5211 "Lists load as you scroll: audit, alert history, Ansible runs" (milestone 477), per preference 172. - core/paging.py: keyset paging over (time, id), newest first, with an opaque "<iso>|<id>" cursor; a page reads one row past itself to know whether another follows. No OFFSET, so rows added while someone scrolls never shift the pages after them. - _macros.html scroll_more / scroll_end and static/scroll-more.js, written once: an IntersectionObserver with a 600px margin fetches the next page before the reader reaches the end; "Loading more…" while in flight, "End of the list." at the end, and a failed page says so with a Retry button instead of re-firing on every scroll. - Audit log: no "Load more" link and no 500-row cap. - Alert history: no 200-row stop with "pick a shorter range"; alert_svc.history takes the cursor, so the MCP tool and the page share one query. - Ansible runs: no silent cap of 50. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
60 lines
2.3 KiB
JavaScript
60 lines
2.3 KiB
JavaScript
/* Lists that continue themselves (#5211, pref 172).
|
|
*
|
|
* A [data-scroll-more] sentinel (the scroll_more macro) fetches the next page
|
|
* when it comes within MARGIN of the viewport, so the rows arrive before the
|
|
* reader reaches the end, and is replaced by them. While a page is in flight
|
|
* the sentinel says "Loading more…"; a failed page says so with a Retry button
|
|
* and is not re-fetched by scrolling (a dead endpoint must not become a request
|
|
* loop). The last page ends with scroll_end's "End of the list." instead of a sentinel.
|
|
*/
|
|
(function () {
|
|
var MARGIN = "600px 0px";
|
|
|
|
function load(el) {
|
|
el.setAttribute("data-scroll-state", "loading");
|
|
htmx.trigger(el, "scroll-more");
|
|
}
|
|
|
|
var observer = "IntersectionObserver" in window ? new IntersectionObserver(function (entries) {
|
|
entries.forEach(function (entry) {
|
|
var el = entry.target;
|
|
if (!entry.isIntersecting || el.hasAttribute("data-scroll-state")) return;
|
|
observer.unobserve(el);
|
|
load(el);
|
|
});
|
|
}, { rootMargin: MARGIN }) : null;
|
|
|
|
function watch(root) {
|
|
if (!root || !root.querySelectorAll) return;
|
|
var found = Array.prototype.slice.call(root.querySelectorAll("[data-scroll-more]"));
|
|
if (root.matches && root.matches("[data-scroll-more]")) found.push(root);
|
|
found.forEach(function (el) {
|
|
if (el.hasAttribute("data-scroll-state")) return;
|
|
if (observer) observer.observe(el); else load(el);
|
|
});
|
|
}
|
|
|
|
function failed(evt) {
|
|
var el = evt.detail && evt.detail.elt;
|
|
if (!el || !el.hasAttribute || !el.hasAttribute("data-scroll-more")) return;
|
|
el.setAttribute("data-scroll-state", "failed");
|
|
var status = el.querySelector("[data-scroll-status]");
|
|
if (!status) return;
|
|
status.textContent = "Couldn't load more. ";
|
|
var retry = document.createElement("button");
|
|
retry.type = "button";
|
|
retry.className = "btn btn-sm btn-ghost";
|
|
retry.textContent = "Retry";
|
|
retry.addEventListener("click", function () {
|
|
status.textContent = "Loading more…";
|
|
load(el);
|
|
});
|
|
status.appendChild(retry);
|
|
}
|
|
|
|
document.addEventListener("htmx:load", function (evt) { watch(evt.detail.elt); });
|
|
document.addEventListener("htmx:responseError", failed);
|
|
document.addEventListener("htmx:sendError", failed);
|
|
document.addEventListener("htmx:timeout", failed);
|
|
})();
|