mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 11:09:18 +02:00
feat: migrate admin system page to HTMX with tab-based navigation and log viewer
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -24,3 +24,4 @@ Thumbs.db
|
|||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
/node_modules
|
||||||
|
|||||||
1
TODO.md
1
TODO.md
@@ -12,3 +12,4 @@
|
|||||||
- [x] Delete public/admin/pages-edit.php
|
- [x] Delete public/admin/pages-edit.php
|
||||||
- [x] Delete public/admin/actions/page.php
|
- [x] Delete public/admin/actions/page.php
|
||||||
- [x] Update storage/schema.sql with apropos_contents table + trigger
|
- [x] Update storage/schema.sql with apropos_contents table + trigger
|
||||||
|
- [x] Rework system.php/system.js: replace custom fetch() JS with HTMX, inline onclick for copy + collapse
|
||||||
|
|||||||
17
package.json
Normal file
17
package.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "posterg",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"vendor": "cp node_modules/htmx.org/dist/htmx.min.js public/assets/js/htmx.min.js"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"packageManager": "pnpm@10.33.0",
|
||||||
|
"dependencies": {
|
||||||
|
"htmx.org": "^2.0.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
pnpm-lock.yaml
generated
Normal file
22
pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
importers:
|
||||||
|
|
||||||
|
.:
|
||||||
|
dependencies:
|
||||||
|
htmx.org:
|
||||||
|
specifier: ^2.0.8
|
||||||
|
version: 2.0.8
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
htmx.org@2.0.8:
|
||||||
|
resolution: {integrity: sha512-fm297iru0iWsNJlBrjvtN7V9zjaxd+69Oqjh4F/Vq9Wwi2kFisLcrLCiv5oBX0KLfOX/zG8AUo9ROMU5XUB44Q==}
|
||||||
|
|
||||||
|
snapshots:
|
||||||
|
|
||||||
|
htmx.org@2.0.8: {}
|
||||||
@@ -77,7 +77,8 @@ if ($tab === 'nginx_config') {
|
|||||||
<div class="log-empty">Le fichier de configuration est vide.</div>
|
<div class="log-empty">Le fichier de configuration est vide.</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="log-output" id="log-output" role="region" aria-label="Configuration nginx">
|
<div class="log-output" id="log-output" role="region" aria-label="Configuration nginx">
|
||||||
<button class="log-copy-btn" id="log-copy-btn" type="button" title="Copier la configuration">Copier</button>
|
<button class="log-copy-btn" id="log-copy-btn" type="button" title="Copier la configuration"
|
||||||
|
onclick="copyLogContent(this);return false">Copier</button>
|
||||||
<?php foreach ($lines as $i => $line): ?>
|
<?php foreach ($lines as $i => $line): ?>
|
||||||
<span class="log-line <?= SystemController::nginxLineClass($line) ?>"
|
<span class="log-line <?= SystemController::nginxLineClass($line) ?>"
|
||||||
data-n="<?= $i + 1 ?>"><?= htmlspecialchars($line, ENT_QUOTES | ENT_SUBSTITUTE) ?></span>
|
data-n="<?= $i + 1 ?>"><?= htmlspecialchars($line, ENT_QUOTES | ENT_SUBSTITUTE) ?></span>
|
||||||
@@ -93,12 +94,19 @@ if ($tab === 'nginx_config') {
|
|||||||
$logMeta = $data['meta'];
|
$logMeta = $data['meta'];
|
||||||
?>
|
?>
|
||||||
<div class="log-toolbar">
|
<div class="log-toolbar">
|
||||||
<label for="lines-select">Afficher</label>
|
<form id="lines-form" hx-get="/admin/system-fragment.php"
|
||||||
<select id="lines-select" aria-label="Nombre de lignes">
|
hx-target="#sys-tab-panel"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-indicator="#sys-tab-panel"
|
||||||
|
hx-trigger="change"
|
||||||
|
hx-vals='{"tab":"<?= htmlspecialchars($tab) ?>"}'>
|
||||||
|
<label for="lines-select">Afficher</label>
|
||||||
|
<select id="lines-select" name="n" aria-label="Nombre de lignes">
|
||||||
<?php foreach (SystemController::ALLOWED_LINES as $opt): ?>
|
<?php foreach (SystemController::ALLOWED_LINES as $opt): ?>
|
||||||
<option value="<?= $opt ?>" <?= $opt === $n ? 'selected' : '' ?>><?= $opt ?> dernières lignes</option>
|
<option value="<?= $opt ?>" <?= $opt === $n ? 'selected' : '' ?>><?= $opt ?> dernières lignes</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
|
</form>
|
||||||
<?php if ($logLines !== null && count($logLines) > 0): ?>
|
<?php if ($logLines !== null && count($logLines) > 0): ?>
|
||||||
<span class="log-count-badge"><?= count($logLines) ?> ligne(s)</span>
|
<span class="log-count-badge"><?= count($logLines) ?> ligne(s)</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -127,7 +135,8 @@ if ($tab === 'nginx_config') {
|
|||||||
<div class="log-empty">Le fichier journal est vide.</div>
|
<div class="log-empty">Le fichier journal est vide.</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="log-output" id="log-output" role="log" aria-live="off" aria-label="Contenu du journal">
|
<div class="log-output" id="log-output" role="log" aria-live="off" aria-label="Contenu du journal">
|
||||||
<button class="log-copy-btn" id="log-copy-btn" type="button" title="Copier le contenu">Copier</button>
|
<button class="log-copy-btn" id="log-copy-btn" type="button" title="Copier le contenu"
|
||||||
|
onclick="copyLogContent(this);return false">Copier</button>
|
||||||
<?php foreach ($logLines as $i => $line): ?>
|
<?php foreach ($logLines as $i => $line): ?>
|
||||||
<span class="log-line <?= SystemController::logLineClass($line) ?>"
|
<span class="log-line <?= SystemController::logLineClass($line) ?>"
|
||||||
data-n="<?= count($logLines) - $i ?>"><?= htmlspecialchars($line, ENT_QUOTES | ENT_SUBSTITUTE) ?></span>
|
data-n="<?= count($logLines) - $i ?>"><?= htmlspecialchars($line, ENT_QUOTES | ENT_SUBSTITUTE) ?></span>
|
||||||
|
|||||||
@@ -70,8 +70,12 @@ if ($activeTab === 'nginx_config') {
|
|||||||
|
|
||||||
$isAdmin = true; $bodyClass = 'admin-body';
|
$isAdmin = true; $bodyClass = 'admin-body';
|
||||||
$extraCss = ['/assets/css/system.css'];
|
$extraCss = ['/assets/css/system.css'];
|
||||||
$extraJs = ['/assets/js/system.js'];
|
// HTMX loaded once in footer; status collapse + copy via inline JS
|
||||||
require_once APP_ROOT . '/templates/head.php';
|
require_once APP_ROOT . '/templates/head.php';
|
||||||
|
|
||||||
|
// Restore collapsed state from cookie
|
||||||
|
$collapsed = $_COOKIE['sys_collapsed'] ?? null;
|
||||||
|
$statusInitiallyCollapsed = $collapsed === '1';
|
||||||
?>
|
?>
|
||||||
<?php include APP_ROOT . '/templates/header.php'; ?>
|
<?php include APP_ROOT . '/templates/header.php'; ?>
|
||||||
|
|
||||||
@@ -103,11 +107,14 @@ require_once APP_ROOT . '/templates/head.php';
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</h2>
|
</h2>
|
||||||
<button id="sys-status-toggle" class="sys-status-toggle"
|
<button id="sys-status-toggle" class="sys-status-toggle"
|
||||||
aria-expanded="true" aria-controls="sys-status-body"
|
aria-expanded="<?= $statusInitiallyCollapsed ? 'false' : 'true' ?>" aria-controls="sys-status-body"
|
||||||
type="button">▲ Réduire</button>
|
type="button"
|
||||||
|
onclick="var b=document.getElementById('sys-status-body');var c=b.hidden;b.hidden=!c;this.setAttribute('aria-expanded',c);this.textContent=c?'▲ Réduire':'▼ Développer';document.cookie='sys_collapsed='+(!c)+';path=/;max-age=31536000';return false">
|
||||||
|
<?= $statusInitiallyCollapsed ? '▼ Développer' : '▲ Réduire' ?>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="sys-status-body">
|
<div id="sys-status-body"<?= $statusInitiallyCollapsed ? ' hidden' : '' ?>>
|
||||||
<div class="srv-grid">
|
<div class="srv-grid">
|
||||||
<?php foreach ($checks as $check): ?>
|
<?php foreach ($checks as $check): ?>
|
||||||
<?php $st = $check['status'] ?? 'unknown'; ?>
|
<?php $st = $check['status'] ?? 'unknown'; ?>
|
||||||
@@ -154,6 +161,11 @@ require_once APP_ROOT . '/templates/head.php';
|
|||||||
<?php foreach (SystemController::LOG_FILES as $key => $def): ?>
|
<?php foreach (SystemController::LOG_FILES as $key => $def): ?>
|
||||||
<a href="?tab=<?= htmlspecialchars($key) ?>&n=<?= $selectedN ?>"
|
<a href="?tab=<?= htmlspecialchars($key) ?>&n=<?= $selectedN ?>"
|
||||||
class="sys-tab <?= $activeTab === $key ? 'active' : '' ?>"
|
class="sys-tab <?= $activeTab === $key ? 'active' : '' ?>"
|
||||||
|
hx-get="/admin/system-fragment.php?tab=<?= htmlspecialchars($key) ?>&n=<?= $selectedN ?>"
|
||||||
|
hx-target="#sys-tab-panel"
|
||||||
|
hx-push-url="?tab=<?= htmlspecialchars($key) ?>&n=<?= $selectedN ?>"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-indicator="#sys-tab-panel"
|
||||||
data-tab="<?= htmlspecialchars($key) ?>"
|
data-tab="<?= htmlspecialchars($key) ?>"
|
||||||
<?= $activeTab === $key ? 'aria-current="page"' : '' ?>>
|
<?= $activeTab === $key ? 'aria-current="page"' : '' ?>>
|
||||||
<?= htmlspecialchars($def['label']) ?>
|
<?= htmlspecialchars($def['label']) ?>
|
||||||
@@ -161,14 +173,17 @@ require_once APP_ROOT . '/templates/head.php';
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<a href="?tab=nginx_config"
|
<a href="?tab=nginx_config"
|
||||||
class="sys-tab <?= $activeTab === 'nginx_config' ? 'active' : '' ?>"
|
class="sys-tab <?= $activeTab === 'nginx_config' ? 'active' : '' ?>"
|
||||||
|
hx-get="/admin/system-fragment.php?tab=nginx_config"
|
||||||
|
hx-target="#sys-tab-panel"
|
||||||
|
hx-push-url="?tab=nginx_config"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-indicator="#sys-tab-panel"
|
||||||
data-tab="nginx_config"
|
data-tab="nginx_config"
|
||||||
<?= $activeTab === 'nginx_config' ? 'aria-current="page"' : '' ?>>nginx — config</a>
|
<?= $activeTab === 'nginx_config' ? 'aria-current="page"' : '' ?>>nginx — config</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Tab panel — content swapped by fetch(); data-* attrs drive JS state -->
|
<!-- Tab panel — content swapped by HTMX -->
|
||||||
<div id="sys-tab-panel"
|
<div id="sys-tab-panel">
|
||||||
data-tab="<?= htmlspecialchars($activeTab) ?>"
|
|
||||||
data-n="<?= $selectedN ?>">
|
|
||||||
|
|
||||||
<?php if ($activeTab === 'nginx_config'): ?>
|
<?php if ($activeTab === 'nginx_config'): ?>
|
||||||
<!-- ════════════════════════════════════════════════════════════════════
|
<!-- ════════════════════════════════════════════════════════════════════
|
||||||
@@ -205,7 +220,8 @@ require_once APP_ROOT . '/templates/head.php';
|
|||||||
|
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="log-output" id="log-output" role="region" aria-label="Configuration nginx">
|
<div class="log-output" id="log-output" role="region" aria-label="Configuration nginx">
|
||||||
<button class="log-copy-btn" id="log-copy-btn" type="button" title="Copier la configuration">
|
<button class="log-copy-btn" id="log-copy-btn" type="button" title="Copier la configuration"
|
||||||
|
onclick="copyLogContent(this);return false">
|
||||||
Copier
|
Copier
|
||||||
</button>
|
</button>
|
||||||
<?php foreach ($nginxConfigLines as $i => $line): ?>
|
<?php foreach ($nginxConfigLines as $i => $line): ?>
|
||||||
@@ -223,13 +239,21 @@ require_once APP_ROOT . '/templates/head.php';
|
|||||||
<!-- Lines selector (submits via JS on change; no button needed) -->
|
<!-- Lines selector (submits via JS on change; no button needed) -->
|
||||||
<div class="log-toolbar">
|
<div class="log-toolbar">
|
||||||
<label for="lines-select">Afficher</label>
|
<label for="lines-select">Afficher</label>
|
||||||
<select id="lines-select" aria-label="Nombre de lignes">
|
<form id="lines-form" hx-get="/admin/system-fragment.php"
|
||||||
|
hx-target="#sys-tab-panel"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-indicator="#sys-tab-panel"
|
||||||
|
hx-trigger="change"
|
||||||
|
hx-vals='{"tab":"<?= htmlspecialchars($activeTab) ?>"}'>
|
||||||
|
<label for="lines-select">Afficher</label>
|
||||||
|
<select id="lines-select" name="n" aria-label="Nombre de lignes">
|
||||||
<?php foreach (SystemController::ALLOWED_LINES as $n): ?>
|
<?php foreach (SystemController::ALLOWED_LINES as $n): ?>
|
||||||
<option value="<?= $n ?>" <?= $n === $selectedN ? 'selected' : '' ?>>
|
<option value="<?= $n ?>" <?= $n === $selectedN ? 'selected' : '' ?>>
|
||||||
<?= $n ?> dernières lignes
|
<?= $n ?> dernières lignes
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
|
</form>
|
||||||
<?php if ($logLines !== null && count($logLines) > 0): ?>
|
<?php if ($logLines !== null && count($logLines) > 0): ?>
|
||||||
<span class="log-count-badge"><?= count($logLines) ?> ligne(s)</span>
|
<span class="log-count-badge"><?= count($logLines) ?> ligne(s)</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -262,7 +286,8 @@ require_once APP_ROOT . '/templates/head.php';
|
|||||||
|
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="log-output" id="log-output" role="log" aria-live="off" aria-label="Contenu du journal">
|
<div class="log-output" id="log-output" role="log" aria-live="off" aria-label="Contenu du journal">
|
||||||
<button class="log-copy-btn" id="log-copy-btn" type="button" title="Copier le contenu">
|
<button class="log-copy-btn" id="log-copy-btn" type="button" title="Copier le contenu"
|
||||||
|
onclick="copyLogContent(this);return false">
|
||||||
Copier
|
Copier
|
||||||
</button>
|
</button>
|
||||||
<?php foreach ($logLines as $i => $line): ?>
|
<?php foreach ($logLines as $i => $line): ?>
|
||||||
@@ -278,5 +303,56 @@ require_once APP_ROOT . '/templates/head.php';
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function copyLogContent(btn) {
|
||||||
|
var logOut = document.querySelector('#log-output');
|
||||||
|
if (!logOut) return;
|
||||||
|
var text = Array.from(logOut.querySelectorAll('.log-line'))
|
||||||
|
.map(function(el){ return el.textContent; }).join('\n');
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(text).then(function(){
|
||||||
|
btn.textContent = '\u2713 Copi\u00e9';
|
||||||
|
btn.classList.add('copied');
|
||||||
|
setTimeout(function(){ btn.textContent = 'Copier'; btn.classList.remove('copied'); }, 2000);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
fallbackCopy(text, btn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function fallbackCopy(text, btn) {
|
||||||
|
var ta = document.createElement('textarea');
|
||||||
|
ta.value = text;
|
||||||
|
ta.style.cssText = 'position:fixed;opacity:0';
|
||||||
|
document.body.appendChild(ta); ta.select();
|
||||||
|
try { document.execCommand('copy'); btn.textContent = '\u2713 Copi\u00e9'; btn.classList.add('copied');
|
||||||
|
setTimeout(function(){ btn.textContent = 'Copier'; btn.classList.remove('copied'); }, 2000);
|
||||||
|
} catch(e) {}
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
}
|
||||||
|
// Update active tab class after each HTMX swap on #sys-tab-panel
|
||||||
|
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
||||||
|
if (evt.detail.target && evt.detail.target.id === 'sys-tab-panel') {
|
||||||
|
var rc = evt.detail.requestConfig;
|
||||||
|
var tab = null;
|
||||||
|
// Tab clicks carry ?tab=… in the path
|
||||||
|
var qIdx = rc.path.indexOf('?');
|
||||||
|
if (qIdx !== -1) {
|
||||||
|
tab = new URLSearchParams(rc.path.substring(qIdx + 1)).get('tab');
|
||||||
|
}
|
||||||
|
// Line-count form sends tab via hx-vals in parameters
|
||||||
|
if (!tab && rc.parameters && rc.parameters.tab) {
|
||||||
|
tab = rc.parameters.tab;
|
||||||
|
}
|
||||||
|
if (tab) {
|
||||||
|
document.querySelectorAll('.sys-tabs .sys-tab').forEach(function(a) {
|
||||||
|
var isActive = a.getAttribute('data-tab') === tab;
|
||||||
|
a.classList.toggle('active', isActive);
|
||||||
|
if (isActive) a.setAttribute('aria-current', 'page');
|
||||||
|
else a.removeAttribute('aria-current');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>
|
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>
|
||||||
|
|||||||
@@ -184,12 +184,12 @@
|
|||||||
min-height: 8rem;
|
min-height: 8rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
#sys-tab-panel.sys-panel-loading {
|
#sys-tab-panel.htmx-request {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition: opacity 0.1s;
|
transition: opacity 0.1s;
|
||||||
}
|
}
|
||||||
#sys-tab-panel.sys-panel-loading::after {
|
#sys-tab-panel.htmx-request::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|||||||
2
public/assets/js/htmx.min.js
vendored
2
public/assets/js/htmx.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,146 +0,0 @@
|
|||||||
(function () {
|
|
||||||
|
|
||||||
// ── Status section collapse toggle ────────────────────────────────
|
|
||||||
var toggleBtn = document.getElementById('sys-status-toggle');
|
|
||||||
var statusBody = document.getElementById('sys-status-body');
|
|
||||||
if (toggleBtn && statusBody) {
|
|
||||||
toggleBtn.addEventListener('click', function () {
|
|
||||||
var collapsed = statusBody.hidden;
|
|
||||||
statusBody.hidden = !collapsed;
|
|
||||||
toggleBtn.setAttribute('aria-expanded', collapsed ? 'true' : 'false');
|
|
||||||
toggleBtn.textContent = collapsed ? '▲ Réduire' : '▼ Développer';
|
|
||||||
try { localStorage.setItem('sys_status_collapsed', collapsed ? '0' : '1'); } catch(e) {}
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
if (localStorage.getItem('sys_status_collapsed') === '1') {
|
|
||||||
statusBody.hidden = true;
|
|
||||||
toggleBtn.setAttribute('aria-expanded', 'false');
|
|
||||||
toggleBtn.textContent = '▼ Développer';
|
|
||||||
}
|
|
||||||
} catch(e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── fetch()-based tab panel loading ───────────────────────────────
|
|
||||||
var panel = document.getElementById('sys-tab-panel');
|
|
||||||
var tabNav = document.querySelector('.sys-tabs');
|
|
||||||
if (!panel || !tabNav || !window.fetch) return;
|
|
||||||
|
|
||||||
var currentTab = panel.dataset.tab;
|
|
||||||
var currentN = panel.dataset.n;
|
|
||||||
|
|
||||||
function loadPanel(tab, n, pushState) {
|
|
||||||
var url = new URL(window.location.href);
|
|
||||||
var fragUrl = new URL('/admin/system-fragment.php', window.location.origin);
|
|
||||||
fragUrl.searchParams.set('tab', tab);
|
|
||||||
fragUrl.searchParams.set('n', n);
|
|
||||||
|
|
||||||
// Mark the panel as loading
|
|
||||||
panel.classList.add('sys-panel-loading');
|
|
||||||
|
|
||||||
fetch(fragUrl.toString(), { credentials: 'same-origin' })
|
|
||||||
.then(function (r) {
|
|
||||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
||||||
return r.text();
|
|
||||||
})
|
|
||||||
.then(function (html) {
|
|
||||||
panel.innerHTML = html;
|
|
||||||
panel.dataset.tab = tab;
|
|
||||||
panel.dataset.n = n;
|
|
||||||
currentTab = tab;
|
|
||||||
currentN = n;
|
|
||||||
panel.classList.remove('sys-panel-loading');
|
|
||||||
|
|
||||||
// Update active tab indicators
|
|
||||||
tabNav.querySelectorAll('.sys-tab').forEach(function (a) {
|
|
||||||
var isActive = a.dataset.tab === tab;
|
|
||||||
a.classList.toggle('active', isActive);
|
|
||||||
if (isActive) {
|
|
||||||
a.setAttribute('aria-current', 'page');
|
|
||||||
} else {
|
|
||||||
a.removeAttribute('aria-current');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Re-bind inner controls (lines-select, copy btn)
|
|
||||||
bindPanelControls();
|
|
||||||
|
|
||||||
// Update browser URL without reloading
|
|
||||||
if (pushState) {
|
|
||||||
url.searchParams.set('tab', tab);
|
|
||||||
url.searchParams.set('n', n);
|
|
||||||
history.pushState({ tab: tab, n: n }, '', url.toString());
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function () {
|
|
||||||
// Graceful degradation: fall back to full page load
|
|
||||||
panel.classList.remove('sys-panel-loading');
|
|
||||||
url.searchParams.set('tab', tab);
|
|
||||||
url.searchParams.set('n', n);
|
|
||||||
window.location.href = url.toString();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wire tab links
|
|
||||||
tabNav.querySelectorAll('.sys-tab').forEach(function (a) {
|
|
||||||
a.addEventListener('click', function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var tab = a.dataset.tab;
|
|
||||||
if (tab && tab !== currentTab) {
|
|
||||||
loadPanel(tab, currentN, true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle browser back/forward
|
|
||||||
window.addEventListener('popstate', function (e) {
|
|
||||||
if (e.state && e.state.tab) {
|
|
||||||
loadPanel(e.state.tab, e.state.n || 100, false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function bindPanelControls() {
|
|
||||||
// Lines-select
|
|
||||||
var sel = panel.querySelector('#lines-select');
|
|
||||||
if (sel) {
|
|
||||||
sel.addEventListener('change', function () {
|
|
||||||
loadPanel(currentTab, parseInt(this.value, 10), true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Copy button
|
|
||||||
var copyBtn = panel.querySelector('#log-copy-btn');
|
|
||||||
var logOut = panel.querySelector('#log-output');
|
|
||||||
if (copyBtn && logOut) {
|
|
||||||
copyBtn.addEventListener('click', function () {
|
|
||||||
var text = Array.from(logOut.querySelectorAll('.log-line'))
|
|
||||||
.map(function (el) { return el.textContent; })
|
|
||||||
.join('\n');
|
|
||||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
||||||
navigator.clipboard.writeText(text).then(function () {
|
|
||||||
copyBtn.textContent = '✓ Copié';
|
|
||||||
copyBtn.classList.add('copied');
|
|
||||||
setTimeout(function () {
|
|
||||||
copyBtn.textContent = 'Copier';
|
|
||||||
copyBtn.classList.remove('copied');
|
|
||||||
}, 2000);
|
|
||||||
}).catch(function () { fallbackCopy(text); });
|
|
||||||
} else {
|
|
||||||
fallbackCopy(text);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bind controls already present on first load
|
|
||||||
bindPanelControls();
|
|
||||||
|
|
||||||
function fallbackCopy(text) {
|
|
||||||
var ta = document.createElement('textarea');
|
|
||||||
ta.value = text;
|
|
||||||
ta.style.cssText = 'position:fixed;opacity:0;top:0;left:0';
|
|
||||||
document.body.appendChild(ta);
|
|
||||||
ta.select();
|
|
||||||
try { document.execCommand('copy'); } catch (e) {}
|
|
||||||
document.body.removeChild(ta);
|
|
||||||
}
|
|
||||||
|
|
||||||
})();
|
|
||||||
@@ -4,5 +4,6 @@
|
|||||||
<?php if (!empty($extraJsInline)): ?>
|
<?php if (!empty($extraJsInline)): ?>
|
||||||
<script><?= $extraJsInline ?></script>
|
<script><?= $extraJsInline ?></script>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<script src="/assets/js/htmx.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user