Compare commits
6 Commits
5545328e53
...
e756f8e0bb
| Author | SHA1 | Date | |
|---|---|---|---|
| e756f8e0bb | |||
| fea7575ac8 | |||
| 6fbba3939f | |||
| f3c15e2582 | |||
| 51fa5a8a3c | |||
| 4a838b68ca |
1727
assets/css/base.css
Normal file
1727
assets/css/base.css
Normal file
File diff suppressed because it is too large
Load Diff
@@ -148,14 +148,6 @@ body::before {
|
|||||||
100% { transform: translate(0); }
|
100% { transform: translate(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.ascii-frame-outer:hover {
|
|
||||||
animation: flicker 0.15s ease-in-out 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Random screen glitch every 30 seconds */
|
|
||||||
body {
|
|
||||||
animation: flicker 0.2s ease-in-out 30s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Subtle data stream effect in corner */
|
/* Subtle data stream effect in corner */
|
||||||
body::after {
|
body::after {
|
||||||
@@ -321,7 +313,7 @@ a:not(.btn):hover::after {
|
|||||||
|
|
||||||
/* Smooth table row selection animation */
|
/* Smooth table row selection animation */
|
||||||
tbody tr {
|
tbody tr {
|
||||||
transition: all 0.2s ease;
|
transition: background-color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Button press effect */
|
/* Button press effect */
|
||||||
@@ -1628,7 +1620,7 @@ button {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: all 0.3s ease;
|
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease, text-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn::before,
|
.btn::before,
|
||||||
@@ -1651,8 +1643,6 @@ button:hover {
|
|||||||
border-color: var(--terminal-amber);
|
border-color: var(--terminal-amber);
|
||||||
text-shadow: var(--glow-amber-intense);
|
text-shadow: var(--glow-amber-intense);
|
||||||
box-shadow: var(--glow-amber-intense);
|
box-shadow: var(--glow-amber-intense);
|
||||||
transform: translateY(-2px);
|
|
||||||
animation: pulse-glow-box 1.5s ease-in-out infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:active,
|
.btn:active,
|
||||||
@@ -1785,7 +1775,7 @@ th {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--terminal-amber);
|
color: var(--terminal-amber);
|
||||||
text-shadow: var(--glow-amber);
|
text-shadow: var(--glow-amber);
|
||||||
transition: all 0.3s ease;
|
transition: background-color 0.2s ease, text-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
th:hover {
|
th:hover {
|
||||||
@@ -1807,7 +1797,6 @@ th:has(input[type="checkbox"])::before {
|
|||||||
|
|
||||||
tr:hover {
|
tr:hover {
|
||||||
background-color: rgba(0, 255, 65, 0.08);
|
background-color: rgba(0, 255, 65, 0.08);
|
||||||
box-shadow: inset 0 0 20px rgba(0, 255, 65, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keyboard navigation selected row */
|
/* Keyboard navigation selected row */
|
||||||
|
|||||||
796
assets/js/base.js
Normal file
796
assets/js/base.js
Normal file
@@ -0,0 +1,796 @@
|
|||||||
|
/**
|
||||||
|
* LOTUSGUILD TERMINAL DESIGN SYSTEM v1.0 — base.js
|
||||||
|
* Core JavaScript utilities shared across all LotusGuild applications
|
||||||
|
*
|
||||||
|
* Apps: Tinker Tickets (PHP), PULSE (Node.js), GANDALF (Flask)
|
||||||
|
* Namespace: window.lt
|
||||||
|
*
|
||||||
|
* CONTENTS
|
||||||
|
* 1. HTML Escape
|
||||||
|
* 2. Toast Notifications
|
||||||
|
* 3. Terminal Audio (beep)
|
||||||
|
* 4. Modal Management
|
||||||
|
* 5. Tab Management
|
||||||
|
* 6. Boot Sequence Animation
|
||||||
|
* 7. Keyboard Shortcuts
|
||||||
|
* 8. Sidebar Collapse
|
||||||
|
* 9. CSRF Token Helpers
|
||||||
|
* 10. Fetch Helpers (JSON API wrapper)
|
||||||
|
* 11. Time Formatting
|
||||||
|
* 12. Bytes Formatting
|
||||||
|
* 13. Table Keyboard Navigation
|
||||||
|
* 14. Sortable Table Headers
|
||||||
|
* 15. Stats Widget Filtering
|
||||||
|
* 16. Auto-refresh Manager
|
||||||
|
* 17. Initialisation
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function (global) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
1. HTML ESCAPE
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
/**
|
||||||
|
* Escape a value for safe insertion into innerHTML.
|
||||||
|
* Always prefer textContent/innerText when possible, but use this
|
||||||
|
* when you must build HTML strings (e.g. template literals for lists).
|
||||||
|
*
|
||||||
|
* @param {*} str
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function escHtml(str) {
|
||||||
|
if (str === null || str === undefined) return '';
|
||||||
|
return String(str)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
2. TOAST NOTIFICATIONS
|
||||||
|
----------------------------------------------------------------
|
||||||
|
Usage:
|
||||||
|
lt.toast.success('Ticket saved');
|
||||||
|
lt.toast.error('Network error', 5000);
|
||||||
|
lt.toast.warning('Rate limit approaching');
|
||||||
|
lt.toast.info('Workflow started');
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
const _toastQueue = [];
|
||||||
|
let _toastActive = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} message
|
||||||
|
* @param {'success'|'error'|'warning'|'info'} type
|
||||||
|
* @param {number} [duration=3500] ms before auto-dismiss
|
||||||
|
*/
|
||||||
|
function showToast(message, type, duration) {
|
||||||
|
type = type || 'info';
|
||||||
|
duration = duration || 3500;
|
||||||
|
|
||||||
|
if (_toastActive) {
|
||||||
|
_toastQueue.push({ message, type, duration });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_displayToast(message, type, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _displayToast(message, type, duration) {
|
||||||
|
_toastActive = true;
|
||||||
|
|
||||||
|
let container = document.querySelector('.lt-toast-container');
|
||||||
|
if (!container) {
|
||||||
|
container = document.createElement('div');
|
||||||
|
container.className = 'lt-toast-container';
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
const icons = { success: '✓', error: '✗', warning: '!', info: 'i' };
|
||||||
|
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = 'lt-toast lt-toast-' + type;
|
||||||
|
toast.setAttribute('role', 'alert');
|
||||||
|
toast.setAttribute('aria-live', 'polite');
|
||||||
|
|
||||||
|
const iconEl = document.createElement('span');
|
||||||
|
iconEl.className = 'lt-toast-icon';
|
||||||
|
iconEl.textContent = '[' + (icons[type] || 'i') + ']';
|
||||||
|
|
||||||
|
const msgEl = document.createElement('span');
|
||||||
|
msgEl.className = 'lt-toast-msg';
|
||||||
|
msgEl.textContent = message;
|
||||||
|
|
||||||
|
const closeEl = document.createElement('button');
|
||||||
|
closeEl.className = 'lt-toast-close';
|
||||||
|
closeEl.textContent = '✕';
|
||||||
|
closeEl.setAttribute('aria-label', 'Dismiss');
|
||||||
|
closeEl.addEventListener('click', () => _dismissToast(toast));
|
||||||
|
|
||||||
|
toast.appendChild(iconEl);
|
||||||
|
toast.appendChild(msgEl);
|
||||||
|
toast.appendChild(closeEl);
|
||||||
|
container.appendChild(toast);
|
||||||
|
|
||||||
|
/* Auto-dismiss */
|
||||||
|
const timer = setTimeout(() => _dismissToast(toast), duration);
|
||||||
|
toast._lt_timer = timer;
|
||||||
|
|
||||||
|
/* Optional audio feedback */
|
||||||
|
_beep(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _dismissToast(toast) {
|
||||||
|
if (!toast || !toast.parentNode) return;
|
||||||
|
clearTimeout(toast._lt_timer);
|
||||||
|
toast.style.opacity = '0';
|
||||||
|
toast.style.transition = 'opacity 0.3s ease';
|
||||||
|
setTimeout(() => {
|
||||||
|
if (toast.parentNode) toast.parentNode.removeChild(toast);
|
||||||
|
_toastActive = false;
|
||||||
|
if (_toastQueue.length) {
|
||||||
|
const next = _toastQueue.shift();
|
||||||
|
_displayToast(next.message, next.type, next.duration);
|
||||||
|
}
|
||||||
|
}, 320);
|
||||||
|
}
|
||||||
|
|
||||||
|
const toast = {
|
||||||
|
success: (msg, dur) => showToast(msg, 'success', dur),
|
||||||
|
error: (msg, dur) => showToast(msg, 'error', dur),
|
||||||
|
warning: (msg, dur) => showToast(msg, 'warning', dur),
|
||||||
|
info: (msg, dur) => showToast(msg, 'info', dur),
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
3. TERMINAL AUDIO
|
||||||
|
----------------------------------------------------------------
|
||||||
|
Usage: lt.beep('success' | 'error' | 'info')
|
||||||
|
Silent-fails if Web Audio API is unavailable.
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
function _beep(type) {
|
||||||
|
try {
|
||||||
|
const ctx = new (global.AudioContext || global.webkitAudioContext)();
|
||||||
|
const osc = ctx.createOscillator();
|
||||||
|
const gain = ctx.createGain();
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(ctx.destination);
|
||||||
|
osc.frequency.value = type === 'success' ? 880
|
||||||
|
: type === 'error' ? 220
|
||||||
|
: 440;
|
||||||
|
osc.type = 'sine';
|
||||||
|
gain.gain.setValueAtTime(0.08, ctx.currentTime);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.12);
|
||||||
|
osc.start(ctx.currentTime);
|
||||||
|
osc.stop(ctx.currentTime + 0.12);
|
||||||
|
} catch (_) { /* silently fail */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
4. MODAL MANAGEMENT
|
||||||
|
----------------------------------------------------------------
|
||||||
|
Usage:
|
||||||
|
lt.modal.open('my-modal-id');
|
||||||
|
lt.modal.close('my-modal-id');
|
||||||
|
lt.modal.closeAll();
|
||||||
|
|
||||||
|
HTML contract:
|
||||||
|
<div id="my-modal-id" class="lt-modal-overlay">
|
||||||
|
<div class="lt-modal">
|
||||||
|
<div class="lt-modal-header">
|
||||||
|
<span class="lt-modal-title">Title</span>
|
||||||
|
<button class="lt-modal-close" data-modal-close>✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="lt-modal-body">…</div>
|
||||||
|
<div class="lt-modal-footer">…</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
function openModal(id) {
|
||||||
|
const el = typeof id === 'string' ? document.getElementById(id) : id;
|
||||||
|
if (!el) return;
|
||||||
|
el.classList.add('show');
|
||||||
|
el.setAttribute('aria-hidden', 'false');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
/* Focus first focusable element */
|
||||||
|
const first = el.querySelector('button, input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
||||||
|
if (first) setTimeout(() => first.focus(), 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal(id) {
|
||||||
|
const el = typeof id === 'string' ? document.getElementById(id) : id;
|
||||||
|
if (!el) return;
|
||||||
|
el.classList.remove('show');
|
||||||
|
el.setAttribute('aria-hidden', 'true');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAllModals() {
|
||||||
|
document.querySelectorAll('.lt-modal-overlay.show').forEach(closeModal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Delegated close handlers */
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
/* Click on overlay backdrop (outside .lt-modal) */
|
||||||
|
if (e.target.classList.contains('lt-modal-overlay')) {
|
||||||
|
closeModal(e.target);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* [data-modal-close] button */
|
||||||
|
const closeBtn = e.target.closest('[data-modal-close]');
|
||||||
|
if (closeBtn) {
|
||||||
|
const overlay = closeBtn.closest('.lt-modal-overlay');
|
||||||
|
if (overlay) closeModal(overlay);
|
||||||
|
}
|
||||||
|
/* [data-modal-open="id"] trigger */
|
||||||
|
const openBtn = e.target.closest('[data-modal-open]');
|
||||||
|
if (openBtn) openModal(openBtn.dataset.modalOpen);
|
||||||
|
});
|
||||||
|
|
||||||
|
const modal = { open: openModal, close: closeModal, closeAll: closeAllModals };
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
5. TAB MANAGEMENT
|
||||||
|
----------------------------------------------------------------
|
||||||
|
Usage:
|
||||||
|
lt.tabs.init(); // auto-wires all .lt-tab elements
|
||||||
|
lt.tabs.switch('tab-panel-id');
|
||||||
|
|
||||||
|
HTML contract:
|
||||||
|
<div class="lt-tabs">
|
||||||
|
<button class="lt-tab active" data-tab="panel-one">One</button>
|
||||||
|
<button class="lt-tab" data-tab="panel-two">Two</button>
|
||||||
|
</div>
|
||||||
|
<div id="panel-one" class="lt-tab-panel active">…</div>
|
||||||
|
<div id="panel-two" class="lt-tab-panel">…</div>
|
||||||
|
|
||||||
|
Persistence: localStorage key 'lt_activeTab_<page>'
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
function switchTab(panelId) {
|
||||||
|
document.querySelectorAll('.lt-tab').forEach(t => t.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.lt-tab-panel').forEach(p => p.classList.remove('active'));
|
||||||
|
|
||||||
|
const btn = document.querySelector('.lt-tab[data-tab="' + panelId + '"]');
|
||||||
|
const panel = document.getElementById(panelId);
|
||||||
|
if (btn) btn.classList.add('active');
|
||||||
|
if (panel) panel.classList.add('active');
|
||||||
|
|
||||||
|
try { localStorage.setItem('lt_activeTab_' + location.pathname, panelId); } catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTabs() {
|
||||||
|
/* Restore from localStorage */
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('lt_activeTab_' + location.pathname);
|
||||||
|
if (saved && document.getElementById(saved)) { switchTab(saved); return; }
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
/* Wire click handlers */
|
||||||
|
document.querySelectorAll('.lt-tab[data-tab]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => switchTab(btn.dataset.tab));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabs = { init: initTabs, switch: switchTab };
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
6. BOOT SEQUENCE ANIMATION
|
||||||
|
----------------------------------------------------------------
|
||||||
|
Usage:
|
||||||
|
lt.boot.run('APP NAME'); // shows once per session
|
||||||
|
lt.boot.run('APP NAME', true); // force show even if already seen
|
||||||
|
|
||||||
|
HTML contract (add to <body>, hidden by default):
|
||||||
|
<div id="lt-boot" class="lt-boot-overlay" style="display:none">
|
||||||
|
<pre id="lt-boot-text" class="lt-boot-text"></pre>
|
||||||
|
</div>
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
function runBoot(appName, force) {
|
||||||
|
const storageKey = 'lt_booted_' + (appName || 'app');
|
||||||
|
if (!force && sessionStorage.getItem(storageKey)) return;
|
||||||
|
|
||||||
|
const overlay = document.getElementById('lt-boot');
|
||||||
|
const pre = document.getElementById('lt-boot-text');
|
||||||
|
if (!overlay || !pre) return;
|
||||||
|
|
||||||
|
overlay.style.display = 'flex';
|
||||||
|
overlay.style.opacity = '1';
|
||||||
|
|
||||||
|
const name = (appName || 'TERMINAL').toUpperCase();
|
||||||
|
const titleStr = name + ' v1.0';
|
||||||
|
const innerWidth = 43;
|
||||||
|
const leftPad = Math.max(0, Math.floor((innerWidth - titleStr.length) / 2));
|
||||||
|
const rightPad = Math.max(0, innerWidth - titleStr.length - leftPad);
|
||||||
|
const messages = [
|
||||||
|
'╔═══════════════════════════════════════════╗',
|
||||||
|
'║' + ' '.repeat(leftPad) + titleStr + ' '.repeat(rightPad) + '║',
|
||||||
|
'║ BOOTING SYSTEM... ║',
|
||||||
|
'╚═══════════════════════════════════════════╝',
|
||||||
|
'',
|
||||||
|
'[ OK ] Checking kernel modules...',
|
||||||
|
'[ OK ] Mounting filesystem...',
|
||||||
|
'[ OK ] Initializing database connection...',
|
||||||
|
'[ OK ] Loading user session...',
|
||||||
|
'[ OK ] Applying security headers...',
|
||||||
|
'[ OK ] Rendering terminal interface...',
|
||||||
|
'',
|
||||||
|
'> SYSTEM READY ✓',
|
||||||
|
'',
|
||||||
|
];
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
pre.textContent = '';
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (i < messages.length) {
|
||||||
|
pre.textContent += messages[i] + '\n';
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
clearInterval(interval);
|
||||||
|
setTimeout(() => {
|
||||||
|
overlay.classList.add('fade-out');
|
||||||
|
setTimeout(() => {
|
||||||
|
overlay.style.display = 'none';
|
||||||
|
overlay.classList.remove('fade-out');
|
||||||
|
}, 520);
|
||||||
|
}, 400);
|
||||||
|
sessionStorage.setItem(storageKey, '1');
|
||||||
|
}
|
||||||
|
}, 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
const boot = { run: runBoot };
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
7. KEYBOARD SHORTCUTS
|
||||||
|
----------------------------------------------------------------
|
||||||
|
Register handlers:
|
||||||
|
lt.keys.on('ctrl+k', () => searchBox.focus());
|
||||||
|
lt.keys.on('?', showHelpModal);
|
||||||
|
lt.keys.on('Escape', lt.modal.closeAll);
|
||||||
|
|
||||||
|
Built-in defaults (activate with lt.keys.initDefaults()):
|
||||||
|
ESC → close all modals
|
||||||
|
? → show #lt-keys-help modal if present
|
||||||
|
Ctrl/⌘+K → focus .lt-search-input
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
const _keyHandlers = {};
|
||||||
|
|
||||||
|
function normalizeKey(combo) {
|
||||||
|
return combo
|
||||||
|
.replace(/ctrl\+/i, 'ctrl+')
|
||||||
|
.replace(/cmd\+/i, 'ctrl+') /* treat Cmd as Ctrl */
|
||||||
|
.replace(/meta\+/i, 'ctrl+')
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerKey(combo, handler) {
|
||||||
|
_keyHandlers[normalizeKey(combo)] = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
function unregisterKey(combo) {
|
||||||
|
delete _keyHandlers[normalizeKey(combo)];
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', function (e) {
|
||||||
|
const inInput = ['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName)
|
||||||
|
|| e.target.isContentEditable;
|
||||||
|
|
||||||
|
/* Build the combo string */
|
||||||
|
let combo = '';
|
||||||
|
if (e.ctrlKey || e.metaKey) combo += 'ctrl+';
|
||||||
|
if (e.altKey) combo += 'alt+';
|
||||||
|
if (e.shiftKey) combo += 'shift+';
|
||||||
|
combo += e.key.toLowerCase();
|
||||||
|
|
||||||
|
/* Always fire ESC, Ctrl combos regardless of input focus */
|
||||||
|
const alwaysFire = e.key === 'Escape' || e.ctrlKey || e.metaKey;
|
||||||
|
|
||||||
|
if (inInput && !alwaysFire) return;
|
||||||
|
|
||||||
|
const handler = _keyHandlers[combo];
|
||||||
|
if (handler) {
|
||||||
|
e.preventDefault();
|
||||||
|
handler(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function initDefaultKeys() {
|
||||||
|
registerKey('Escape', closeAllModals);
|
||||||
|
registerKey('?', () => {
|
||||||
|
const helpModal = document.getElementById('lt-keys-help');
|
||||||
|
if (helpModal) openModal(helpModal);
|
||||||
|
});
|
||||||
|
registerKey('ctrl+k', () => {
|
||||||
|
const search = document.querySelector('.lt-search-input');
|
||||||
|
if (search) { search.focus(); search.select(); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = {
|
||||||
|
on: registerKey,
|
||||||
|
off: unregisterKey,
|
||||||
|
initDefaults: initDefaultKeys,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
8. SIDEBAR COLLAPSE
|
||||||
|
----------------------------------------------------------------
|
||||||
|
Usage: lt.sidebar.init();
|
||||||
|
|
||||||
|
HTML contract:
|
||||||
|
<aside class="lt-sidebar" id="lt-sidebar">
|
||||||
|
<div class="lt-sidebar-header">
|
||||||
|
Filters
|
||||||
|
<button class="lt-sidebar-toggle" data-sidebar-toggle="lt-sidebar">◀</button>
|
||||||
|
</div>
|
||||||
|
<div class="lt-sidebar-body">…</div>
|
||||||
|
</aside>
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
function initSidebar() {
|
||||||
|
document.querySelectorAll('[data-sidebar-toggle]').forEach(btn => {
|
||||||
|
const sidebar = document.getElementById(btn.dataset.sidebarToggle);
|
||||||
|
if (!sidebar) return;
|
||||||
|
|
||||||
|
/* Restore state */
|
||||||
|
const collapsed = sessionStorage.getItem('lt_sidebar_' + btn.dataset.sidebarToggle) === '1';
|
||||||
|
if (collapsed) {
|
||||||
|
sidebar.classList.add('collapsed');
|
||||||
|
btn.textContent = '▶';
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
sidebar.classList.toggle('collapsed');
|
||||||
|
const isCollapsed = sidebar.classList.contains('collapsed');
|
||||||
|
btn.textContent = isCollapsed ? '▶' : '◀';
|
||||||
|
try { sessionStorage.setItem('lt_sidebar_' + btn.dataset.sidebarToggle, isCollapsed ? '1' : '0'); } catch (_) {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sidebar = { init: initSidebar };
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
9. CSRF TOKEN HELPERS
|
||||||
|
----------------------------------------------------------------
|
||||||
|
PHP apps: window.CSRF_TOKEN is set by the view via:
|
||||||
|
<script nonce="...">window.CSRF_TOKEN = '<?= CsrfMiddleware::getToken() ?>';</script>
|
||||||
|
Node apps: set via: window.CSRF_TOKEN = '<%= csrfToken %>';
|
||||||
|
Flask: use Flask-WTF meta tag or inject via template.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
const headers = lt.csrf.headers();
|
||||||
|
fetch('/api/foo', { method: 'POST', headers: lt.csrf.headers(), body: … });
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
function csrfHeaders() {
|
||||||
|
const token = global.CSRF_TOKEN || '';
|
||||||
|
return token ? { 'X-CSRF-Token': token } : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const csrf = { headers: csrfHeaders };
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
10. FETCH HELPERS
|
||||||
|
----------------------------------------------------------------
|
||||||
|
Usage:
|
||||||
|
const data = await lt.api.get('/api/tickets');
|
||||||
|
const res = await lt.api.post('/api/update_ticket.php', { id: 1, status: 'Closed' });
|
||||||
|
const res = await lt.api.delete('/api/ticket_dependencies.php', { id: 5 });
|
||||||
|
|
||||||
|
All methods:
|
||||||
|
- Automatically set Content-Type: application/json
|
||||||
|
- Attach CSRF token header
|
||||||
|
- Parse JSON response
|
||||||
|
- On non-2xx: throw an Error with the server's error message
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
async function apiFetch(method, url, body) {
|
||||||
|
const opts = {
|
||||||
|
method,
|
||||||
|
headers: Object.assign(
|
||||||
|
{ 'Content-Type': 'application/json' },
|
||||||
|
csrfHeaders()
|
||||||
|
),
|
||||||
|
};
|
||||||
|
if (body !== undefined) opts.body = JSON.stringify(body);
|
||||||
|
|
||||||
|
let resp;
|
||||||
|
try {
|
||||||
|
resp = await fetch(url, opts);
|
||||||
|
} catch (networkErr) {
|
||||||
|
throw new Error('Network error: ' + networkErr.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = await resp.json();
|
||||||
|
} catch (_) {
|
||||||
|
data = { success: resp.ok };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
throw new Error(data.error || data.message || 'HTTP ' + resp.status);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = {
|
||||||
|
get: (url) => apiFetch('GET', url),
|
||||||
|
post: (url, body) => apiFetch('POST', url, body),
|
||||||
|
put: (url, body) => apiFetch('PUT', url, body),
|
||||||
|
patch: (url, body) => apiFetch('PATCH', url, body),
|
||||||
|
delete: (url, body) => apiFetch('DELETE', url, body),
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
11. TIME FORMATTING
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
/**
|
||||||
|
* Returns a human-readable relative time string.
|
||||||
|
* @param {string|number|Date} value ISO string, Unix ms, or Date
|
||||||
|
* @returns {string} e.g. "5m ago", "2h ago", "3d ago"
|
||||||
|
*/
|
||||||
|
function timeAgo(value) {
|
||||||
|
const date = value instanceof Date ? value : new Date(value);
|
||||||
|
if (isNaN(date)) return '—';
|
||||||
|
const diff = Math.floor((Date.now() - date.getTime()) / 1000);
|
||||||
|
if (diff < 60) return diff + 's ago';
|
||||||
|
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
||||||
|
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
||||||
|
return Math.floor(diff / 86400) + 'd ago';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format seconds → "1h 23m 45s" style.
|
||||||
|
* @param {number} secs
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function formatUptime(secs) {
|
||||||
|
secs = Math.floor(secs);
|
||||||
|
const d = Math.floor(secs / 86400);
|
||||||
|
const h = Math.floor((secs % 86400) / 3600);
|
||||||
|
const m = Math.floor((secs % 3600) / 60);
|
||||||
|
const s = secs % 60;
|
||||||
|
const parts = [];
|
||||||
|
if (d) parts.push(d + 'd');
|
||||||
|
if (h) parts.push(h + 'h');
|
||||||
|
if (m) parts.push(m + 'm');
|
||||||
|
if (!d) parts.push(s + 's');
|
||||||
|
return parts.join(' ') || '0s';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format an ISO datetime string for display.
|
||||||
|
* Uses the timezone configured in window.APP_TIMEZONE (PHP apps)
|
||||||
|
* or falls back to the browser locale.
|
||||||
|
*/
|
||||||
|
function formatDate(value) {
|
||||||
|
const date = value instanceof Date ? value : new Date(value);
|
||||||
|
if (isNaN(date)) return '—';
|
||||||
|
const tz = global.APP_TIMEZONE || undefined;
|
||||||
|
try {
|
||||||
|
return date.toLocaleString(undefined, {
|
||||||
|
timeZone: tz,
|
||||||
|
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||||
|
hour: '2-digit', minute: '2-digit',
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
return date.toLocaleString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const time = { ago: timeAgo, uptime: formatUptime, format: formatDate };
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
12. BYTES FORMATTING
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
/**
|
||||||
|
* @param {number} bytes
|
||||||
|
* @returns {string} e.g. "1.23 GB"
|
||||||
|
*/
|
||||||
|
function formatBytes(bytes) {
|
||||||
|
if (bytes === null || bytes === undefined) return '—';
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
let i = 0;
|
||||||
|
while (bytes >= 1024 && i < units.length - 1) { bytes /= 1024; i++; }
|
||||||
|
return bytes.toFixed(i === 0 ? 0 : 2) + ' ' + units[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
13. TABLE KEYBOARD NAVIGATION (vim-style j/k)
|
||||||
|
----------------------------------------------------------------
|
||||||
|
Usage: lt.tableNav.init('my-table-id');
|
||||||
|
|
||||||
|
Keys registered:
|
||||||
|
j or ArrowDown → move selection down
|
||||||
|
k or ArrowUp → move selection up
|
||||||
|
Enter → follow first <a> in selected row
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
function initTableNav(tableId) {
|
||||||
|
const table = tableId ? document.getElementById(tableId) : document.querySelector('.lt-table');
|
||||||
|
if (!table) return;
|
||||||
|
|
||||||
|
function rows() { return Array.from(table.querySelectorAll('tbody tr')); }
|
||||||
|
function selected() { return table.querySelector('tbody tr.lt-row-selected'); }
|
||||||
|
|
||||||
|
function move(dir) {
|
||||||
|
const all = rows();
|
||||||
|
if (!all.length) return;
|
||||||
|
const cur = selected();
|
||||||
|
const idx = cur ? all.indexOf(cur) : -1;
|
||||||
|
const next = dir === 'down'
|
||||||
|
? all[idx < all.length - 1 ? idx + 1 : 0]
|
||||||
|
: all[idx > 0 ? idx - 1 : all.length - 1];
|
||||||
|
if (cur) cur.classList.remove('lt-row-selected');
|
||||||
|
next.classList.add('lt-row-selected');
|
||||||
|
next.scrollIntoView({ block: 'nearest' });
|
||||||
|
}
|
||||||
|
|
||||||
|
keys.on('j', () => move('down'));
|
||||||
|
keys.on('ArrowDown', () => move('down'));
|
||||||
|
keys.on('k', () => move('up'));
|
||||||
|
keys.on('ArrowUp', () => move('up'));
|
||||||
|
keys.on('Enter', () => {
|
||||||
|
const row = selected();
|
||||||
|
if (!row) return;
|
||||||
|
const link = row.querySelector('a[href]');
|
||||||
|
if (link) global.location.href = link.href;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableNav = { init: initTableNav };
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
14. SORTABLE TABLE HEADERS
|
||||||
|
----------------------------------------------------------------
|
||||||
|
Usage: lt.sortTable.init('my-table-id');
|
||||||
|
|
||||||
|
Markup: add data-sort-key="field" to <th> elements.
|
||||||
|
Sorts rows client-side by the text content of the matching column.
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
function initSortTable(tableId) {
|
||||||
|
const table = document.getElementById(tableId);
|
||||||
|
if (!table) return;
|
||||||
|
|
||||||
|
const ths = Array.from(table.querySelectorAll('th[data-sort-key]'));
|
||||||
|
ths.forEach((th, colIdx) => {
|
||||||
|
th.style.cursor = 'pointer';
|
||||||
|
let dir = 'asc';
|
||||||
|
|
||||||
|
th.addEventListener('click', () => {
|
||||||
|
/* Reset all headers */
|
||||||
|
ths.forEach(h => h.removeAttribute('data-sort'));
|
||||||
|
th.setAttribute('data-sort', dir);
|
||||||
|
|
||||||
|
const tbody = table.querySelector('tbody');
|
||||||
|
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||||||
|
rows.sort((a, b) => {
|
||||||
|
const aText = (a.cells[colIdx] || {}).textContent || '';
|
||||||
|
const bText = (b.cells[colIdx] || {}).textContent || '';
|
||||||
|
const n = !isNaN(parseFloat(aText)) && !isNaN(parseFloat(bText));
|
||||||
|
const cmp = n
|
||||||
|
? parseFloat(aText) - parseFloat(bText)
|
||||||
|
: aText.localeCompare(bText);
|
||||||
|
return dir === 'asc' ? cmp : -cmp;
|
||||||
|
});
|
||||||
|
rows.forEach(r => tbody.appendChild(r));
|
||||||
|
dir = dir === 'asc' ? 'desc' : 'asc';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortTable = { init: initSortTable };
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
15. STATS WIDGET FILTERING
|
||||||
|
----------------------------------------------------------------
|
||||||
|
Usage: lt.statsFilter.init();
|
||||||
|
|
||||||
|
HTML contract:
|
||||||
|
<div class="lt-stat-card" data-filter-key="status" data-filter-val="Open">…</div>
|
||||||
|
<!-- clicking the card adds ?filter=status:Open to the URL and
|
||||||
|
calls the optional window.lt_onStatFilter(key, val) hook -->
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
function initStatsFilter() {
|
||||||
|
document.querySelectorAll('.lt-stat-card[data-filter-key]').forEach(card => {
|
||||||
|
card.addEventListener('click', () => {
|
||||||
|
const key = card.dataset.filterKey;
|
||||||
|
const val = card.dataset.filterVal;
|
||||||
|
|
||||||
|
/* Toggle active state */
|
||||||
|
const wasActive = card.classList.contains('active');
|
||||||
|
document.querySelectorAll('.lt-stat-card').forEach(c => c.classList.remove('active'));
|
||||||
|
if (!wasActive) card.classList.add('active');
|
||||||
|
|
||||||
|
/* Call app-specific filter hook if defined */
|
||||||
|
if (typeof global.lt_onStatFilter === 'function') {
|
||||||
|
global.lt_onStatFilter(wasActive ? null : key, wasActive ? null : val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const statsFilter = { init: initStatsFilter };
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
16. AUTO-REFRESH MANAGER
|
||||||
|
----------------------------------------------------------------
|
||||||
|
Usage:
|
||||||
|
lt.autoRefresh.start(refreshFn, 30000); // every 30 s
|
||||||
|
lt.autoRefresh.stop();
|
||||||
|
lt.autoRefresh.now(); // trigger immediately + restart timer
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
let _arTimer = null;
|
||||||
|
let _arFn = null;
|
||||||
|
let _arInterval = 30000;
|
||||||
|
|
||||||
|
function arStart(fn, intervalMs) {
|
||||||
|
arStop();
|
||||||
|
_arFn = fn;
|
||||||
|
_arInterval = intervalMs || 30000;
|
||||||
|
_arTimer = setInterval(_arFn, _arInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
function arStop() {
|
||||||
|
if (_arTimer) { clearInterval(_arTimer); _arTimer = null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function arNow() {
|
||||||
|
arStop();
|
||||||
|
if (_arFn) {
|
||||||
|
_arFn();
|
||||||
|
_arTimer = setInterval(_arFn, _arInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const autoRefresh = { start: arStart, stop: arStop, now: arNow };
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
17. INITIALISATION
|
||||||
|
----------------------------------------------------------------
|
||||||
|
Called automatically on DOMContentLoaded.
|
||||||
|
Each sub-system can also be initialised manually after the DOM
|
||||||
|
has been updated with AJAX content.
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
function init() {
|
||||||
|
initTabs();
|
||||||
|
initSidebar();
|
||||||
|
initDefaultKeys();
|
||||||
|
initStatsFilter();
|
||||||
|
|
||||||
|
/* Boot sequence: runs if #lt-boot element is present */
|
||||||
|
const bootEl = document.getElementById('lt-boot');
|
||||||
|
if (bootEl) {
|
||||||
|
const appName = bootEl.dataset.appName || document.title;
|
||||||
|
runBoot(appName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------
|
||||||
|
Public API
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
global.lt = {
|
||||||
|
escHtml,
|
||||||
|
toast,
|
||||||
|
beep: _beep,
|
||||||
|
modal,
|
||||||
|
tabs,
|
||||||
|
boot,
|
||||||
|
keys,
|
||||||
|
sidebar,
|
||||||
|
csrf,
|
||||||
|
api,
|
||||||
|
time,
|
||||||
|
bytes: { format: formatBytes },
|
||||||
|
tableNav,
|
||||||
|
sortTable,
|
||||||
|
statsFilter,
|
||||||
|
autoRefresh,
|
||||||
|
};
|
||||||
|
|
||||||
|
}(window));
|
||||||
@@ -1005,14 +1005,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add hover effect
|
|
||||||
row.addEventListener('mouseenter', function() {
|
|
||||||
this.style.backgroundColor = 'rgba(0, 255, 65, 0.08)';
|
|
||||||
});
|
|
||||||
|
|
||||||
row.addEventListener('mouseleave', function() {
|
|
||||||
this.style.backgroundColor = '';
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
19
deploy.sh
19
deploy.sh
@@ -1,19 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "Deploying tinker_tickets to web server..."
|
|
||||||
|
|
||||||
# Deploy web_template (shared UI framework)
|
|
||||||
echo "Syncing web_template to web server..."
|
|
||||||
rsync -avz --delete --exclude='.git' --exclude='node' --exclude='php' --exclude='python' --exclude='README.md' --exclude='Claude.md' /root/code/web_template/ root@10.10.10.45:/var/www/html/web_template/
|
|
||||||
|
|
||||||
# Deploy to web server
|
|
||||||
echo "Syncing to web server (10.10.10.45)..."
|
|
||||||
rsync -avz --delete --exclude='.git' --exclude='deploy.sh' --exclude='.env' ./ root@10.10.10.45:/var/www/html/tinkertickets/
|
|
||||||
|
|
||||||
# Set proper permissions on the web server
|
|
||||||
echo "Setting proper file permissions..."
|
|
||||||
ssh root@10.10.10.45 "chown -R www-data:www-data /var/www/html/web_template /var/www/html/tinkertickets && find /var/www/html/web_template /var/www/html/tinkertickets -type f -exec chmod 644 {} \; && find /var/www/html/web_template /var/www/html/tinkertickets -type d -exec chmod 755 {} \;"
|
|
||||||
|
|
||||||
echo "Deployment to web server complete!"
|
|
||||||
echo "Don't forget to commit and push your changes via VS Code when ready."
|
|
||||||
@@ -11,10 +11,10 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Create New Ticket</title>
|
<title>Create New Ticket</title>
|
||||||
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
||||||
<link rel="stylesheet" href="/web_template/base.css">
|
<link rel="stylesheet" href="/assets/css/base.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css?v=20260126c">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css?v=20260126c">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css?v=20260124e">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css?v=20260124e">
|
||||||
<script nonce="<?php echo $nonce; ?>" src="/web_template/base.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="/assets/js/base.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/utils.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/utils.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js?v=20260205"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js?v=20260205"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>">
|
<script nonce="<?php echo $nonce; ?>">
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Ticket Dashboard</title>
|
<title>Ticket Dashboard</title>
|
||||||
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
||||||
<link rel="stylesheet" href="/web_template/base.css">
|
<link rel="stylesheet" href="/assets/css/base.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css?v=20260131e">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css?v=20260131e">
|
||||||
<script nonce="<?php echo $nonce; ?>" src="/web_template/base.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="/assets/js/base.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/ascii-banner.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/ascii-banner.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/utils.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/utils.js"></script>
|
||||||
|
|||||||
@@ -50,10 +50,10 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Ticket #<?php echo $ticket['ticket_id']; ?></title>
|
<title>Ticket #<?php echo $ticket['ticket_id']; ?></title>
|
||||||
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
||||||
<link rel="stylesheet" href="/web_template/base.css">
|
<link rel="stylesheet" href="/assets/css/base.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css?v=20260131e">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css?v=20260131e">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css?v=20260131e">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css?v=20260131e">
|
||||||
<script nonce="<?php echo $nonce; ?>" src="/web_template/base.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="/assets/js/base.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/utils.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/utils.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/markdown.js?v=20260131e"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/markdown.js?v=20260131e"></script>
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>API Keys - Admin</title>
|
<title>API Keys - Admin</title>
|
||||||
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
||||||
<link rel="stylesheet" href="/web_template/base.css">
|
<link rel="stylesheet" href="/assets/css/base.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
|
||||||
<script nonce="<?php echo $nonce; ?>" src="/web_template/base.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="/assets/js/base.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>">
|
<script nonce="<?php echo $nonce; ?>">
|
||||||
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Audit Log - Admin</title>
|
<title>Audit Log - Admin</title>
|
||||||
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
||||||
<link rel="stylesheet" href="/web_template/base.css">
|
<link rel="stylesheet" href="/assets/css/base.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
|
||||||
<script src="/web_template/base.js"></script>
|
<script src="/assets/js/base.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="user-header">
|
<div class="user-header">
|
||||||
@@ -155,5 +155,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script>if (window.lt) lt.keys.initDefaults();</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Custom Fields - Admin</title>
|
<title>Custom Fields - Admin</title>
|
||||||
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
||||||
<link rel="stylesheet" href="/web_template/base.css">
|
<link rel="stylesheet" href="/assets/css/base.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
|
||||||
<script nonce="<?php echo $nonce; ?>" src="/web_template/base.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="/assets/js/base.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>">
|
<script nonce="<?php echo $nonce; ?>">
|
||||||
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Recurring Tickets - Admin</title>
|
<title>Recurring Tickets - Admin</title>
|
||||||
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
||||||
<link rel="stylesheet" href="/web_template/base.css">
|
<link rel="stylesheet" href="/assets/css/base.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
|
||||||
<script nonce="<?php echo $nonce; ?>" src="/web_template/base.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="/assets/js/base.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>">
|
<script nonce="<?php echo $nonce; ?>">
|
||||||
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Template Management - Admin</title>
|
<title>Template Management - Admin</title>
|
||||||
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
||||||
<link rel="stylesheet" href="/web_template/base.css">
|
<link rel="stylesheet" href="/assets/css/base.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
|
||||||
<script nonce="<?php echo $nonce; ?>" src="/web_template/base.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="/assets/js/base.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>">
|
<script nonce="<?php echo $nonce; ?>">
|
||||||
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>User Activity - Admin</title>
|
<title>User Activity - Admin</title>
|
||||||
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
||||||
<link rel="stylesheet" href="/web_template/base.css">
|
<link rel="stylesheet" href="/assets/css/base.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
|
||||||
<script src="/web_template/base.js"></script>
|
<script src="/assets/js/base.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="user-header">
|
<div class="user-header">
|
||||||
@@ -133,5 +133,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script>if (window.lt) lt.keys.initDefaults();</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Workflow Designer - Admin</title>
|
<title>Workflow Designer - Admin</title>
|
||||||
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
|
||||||
<link rel="stylesheet" href="/web_template/base.css">
|
<link rel="stylesheet" href="/assets/css/base.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
|
||||||
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
|
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
|
||||||
<script nonce="<?php echo $nonce; ?>" src="/web_template/base.js"></script>
|
<script nonce="<?php echo $nonce; ?>" src="/assets/js/base.js"></script>
|
||||||
<script nonce="<?php echo $nonce; ?>">
|
<script nonce="<?php echo $nonce; ?>">
|
||||||
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user