add all files

This commit is contained in:
Rucus
2026-02-17 09:29:34 -06:00
parent b8c8d67c67
commit 782d203799
21925 changed files with 2433086 additions and 0 deletions

View File

@@ -0,0 +1,261 @@
class LivePanel {
constructor(element) {
this.element = element;
this.url = element.dataset.refreshUrl;
const requestedInterval = Number(element.dataset.refreshInterval || 2000);
this.baseInterval = Number.isFinite(requestedInterval) && requestedInterval > 0 ? requestedInterval : 2000;
const minIntervalAttr = Number(element.dataset.refreshMinInterval);
const maxIntervalAttr = Number(element.dataset.refreshMaxInterval);
this.minInterval = Number.isFinite(minIntervalAttr) && minIntervalAttr > 0 ? minIntervalAttr : this.baseInterval;
this.maxInterval = Number.isFinite(maxIntervalAttr) && maxIntervalAttr > 0 ? Math.max(maxIntervalAttr, this.minInterval) : this.baseInterval * 4;
this.currentInterval = this.baseInterval;
this.lastEtag = null;
this.lastModified = null;
this.timeoutId = null;
this.isFetching = false;
if (!this.url) {
console.warn('LivePanel requires a data-refresh-url attribute.');
return;
}
this.renderLoadingState();
this.start();
LivePanel.instances.push(this);
}
renderLoadingState() {
if (!this.element.querySelector('.panel-loading')) {
this.element.innerHTML = `
<div class="panel-loading" role="status" aria-live="polite">
<span class="spinner" aria-hidden="true"></span>
<span>Loading latest data&hellip;</span>
</div>
`;
}
}
start() {
this.stop();
this.currentInterval = this.baseInterval;
this.fetchData();
}
stop() {
if (this.timeoutId) {
window.clearTimeout(this.timeoutId);
this.timeoutId = null;
}
}
scheduleNextFetch(delay) {
if (this.baseInterval <= 0) {
return;
}
if (this.timeoutId) {
window.clearTimeout(this.timeoutId);
}
let nextDelay = typeof delay === 'number' ? delay : this.currentInterval;
if (nextDelay !== 0) {
nextDelay = Math.min(Math.max(nextDelay, this.minInterval), this.maxInterval);
}
this.timeoutId = window.setTimeout(() => this.fetchData(), nextDelay);
}
async fetchData() {
if (this.isFetching) {
return;
}
this.isFetching = true;
try {
const headers = {
'X-Requested-With': 'XMLHttpRequest'
};
if (this.lastEtag) {
headers['If-None-Match'] = this.lastEtag;
}
if (this.lastModified) {
headers['If-Modified-Since'] = this.lastModified;
}
const response = await fetch(this.url, {
headers,
cache: 'no-cache'
});
if (response.status === 304) {
this.currentInterval = Math.min(this.currentInterval * 1.25, this.maxInterval);
return;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const etag = response.headers.get('ETag');
if (etag) {
this.lastEtag = etag;
}
const lastModified = response.headers.get('Last-Modified');
if (lastModified) {
this.lastModified = lastModified;
}
const html = await response.text();
if (html.trim().length) {
this.element.innerHTML = html;
}
const updateEvent = new CustomEvent('livepanel:updated', {
detail: {
panel: this,
html
}
});
this.element.dispatchEvent(updateEvent);
this.currentInterval = this.baseInterval;
} catch (error) {
console.error('Failed to refresh panel', error);
this.element.innerHTML = `
<div class="panel-error" role="alert">
<strong>Unable to load data.</strong>
<span>Please check your connection.</span>
</div>
`;
this.lastEtag = null;
this.lastModified = null;
this.currentInterval = Math.min(
Math.max(this.currentInterval * 1.5, this.minInterval),
this.maxInterval
);
} finally {
this.isFetching = false;
if (this.baseInterval > 0) {
this.scheduleNextFetch(this.currentInterval);
}
}
}
static pauseAll() {
LivePanel.instances.forEach((panel) => panel.stop());
}
static resumeAll() {
LivePanel.instances.forEach((panel) => panel.start());
}
}
LivePanel.instances = [];
function initialiseLivePanels() {
const panels = document.querySelectorAll('[data-refresh-url]');
panels.forEach((panel) => {
if (!panel.dataset.livePanelInitialised) {
panel.dataset.livePanelInitialised = 'true';
new LivePanel(panel);
}
});
}
function setupBackToTop() {
const backToTop = document.querySelector('[data-back-to-top]');
if (!backToTop) {
return;
}
const toggleVisibility = () => {
if (window.scrollY > 320) {
backToTop.classList.add('is-visible');
} else {
backToTop.classList.remove('is-visible');
}
};
window.addEventListener('scroll', toggleVisibility, { passive: true });
backToTop.addEventListener('click', (event) => {
event.preventDefault();
window.scrollTo({ top: 0, behavior: 'smooth' });
});
}
function setupMobileNav() {
const toggle = document.querySelector('[data-menu-toggle]');
const sidebar = document.querySelector('.app-sidebar');
if (!toggle || !sidebar) {
return;
}
toggle.addEventListener('click', () => {
const expanded = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', expanded ? 'false' : 'true');
sidebar.classList.toggle('is-open');
});
}
window.popupCenter = function popupCenter(pageURL, title = 'Details', width = 900, height = 640) {
const screenLeft = window.screenLeft !== undefined ? window.screenLeft : window.screenX;
const screenTop = window.screenTop !== undefined ? window.screenTop : window.screenY;
const dualScreenWidth = window.innerWidth || document.documentElement.clientWidth || screen.width;
const dualScreenHeight = window.innerHeight || document.documentElement.clientHeight || screen.height;
const left = screenLeft + Math.max(0, (dualScreenWidth - width) / 2);
const top = screenTop + Math.max(0, (dualScreenHeight - height) / 2);
const features = [
'toolbar=no',
'location=no',
'directories=no',
'status=no',
'menubar=no',
'scrollbars=yes',
'resizable=yes',
`width=${width}`,
`height=${height}`,
`top=${top}`,
`left=${left}`
].join(',');
const newWindow = window.open(pageURL, title, features);
if (newWindow && newWindow.focus) {
newWindow.focus();
}
return newWindow;
};
window.popitup = function popitup(url, title = 'Details') {
const popup = window.open(url, title, 'width=420,height=520,scrollbars=yes');
if (popup && popup.focus) {
popup.focus();
}
return false;
};
window.addEventListener('DOMContentLoaded', () => {
initialiseLivePanels();
setupBackToTop();
setupMobileNav();
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
LivePanel.pauseAll();
} else {
LivePanel.resumeAll();
}
});
});