Web UI v4.1.6: Admin settings, nav icons, air-gap ready

Admin System Settings page:
- New /admin/settings route with channel key config
- QR code export with tiled print sheet (4x5 on US Letter)
- Server config display (HTTPS, port, auth, DCT/QR status)
- Environment info (version, Python, platform, KDF)

Navigation improvements:
- Icon-only nav with floating labels on hover
- Gold labels slide down below icons
- Gradient pill background on hover

Air-gap ready:
- All vendor libs now local (Bootstrap CSS/JS, Icons, html5-qrcode)
- QRious library for QR generation
- No external CDN dependencies

Other changes:
- Moved About link from nav to footer
- Channel QR export moved from about.html to admin/settings.html
- Print sheet button for QR codes (tiled US Letter output)
- Dev runner script (dev_run.sh) with r/q hotkeys
- Fixed navbar dropdown z-index

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Aaron D. Lee
2026-01-07 18:36:33 -05:00
parent 28b539bcd9
commit 4d8575ce33
18 changed files with 2368 additions and 716 deletions

View File

@@ -317,58 +317,19 @@
</div>
{% if channel_configured %}
<div class="alert alert-success mt-3 mb-3">
<div class="alert alert-success mt-3 mb-0">
<i class="bi bi-shield-lock me-2"></i>
<strong>This server has a channel key configured:</strong>
<code class="ms-2">{{ channel_fingerprint }}</code>
<span class="text-muted ms-2">({{ channel_source }})</span>
</div>
{% else %}
<div class="alert alert-info mt-3 mb-3">
<div class="alert alert-info mt-3 mb-0">
<i class="bi bi-info-circle me-2"></i>
This server is running in <strong>public mode</strong>.
Set <code>STEGASOO_CHANNEL_KEY</code> to enable server-wide channel isolation.
</div>
{% endif %}
<!-- Channel Key QR Generator (Admin only) -->
{% if is_admin %}
<div class="card bg-dark border-secondary">
<div class="card-header">
<i class="bi bi-qr-code me-2"></i>Share Channel Key via QR
<span class="badge bg-warning text-dark ms-2"><i class="bi bi-shield-check me-1"></i>Admin</span>
</div>
<div class="card-body">
<p class="small text-muted mb-3">Generate a QR code to share a channel key with others.</p>
<div class="row g-2 align-items-end">
<div class="col-md-8">
<label class="form-label small">Channel Key</label>
<div class="input-group">
<input type="text" class="form-control font-monospace" id="channelKeyQrInput"
placeholder="Enter or generate a key">
<button class="btn btn-outline-secondary" type="button" id="channelKeyQrGenerate"
title="Generate random key">
<i class="bi bi-shuffle"></i>
</button>
</div>
</div>
<div class="col-md-4">
<button class="btn btn-primary w-100" type="button" id="channelKeyQrShow">
<i class="bi bi-qr-code me-1"></i>Show QR
</button>
</div>
</div>
<div class="text-center mt-3 d-none" id="channelKeyQrContainer">
<canvas id="channelKeyQrCanvas" class="bg-white p-2 rounded"></canvas>
<div class="mt-2">
<button class="btn btn-sm btn-outline-secondary" type="button" id="channelKeyQrDownload">
<i class="bi bi-download me-1"></i>Download PNG
</button>
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
@@ -635,62 +596,3 @@
</div>
{% endblock %}
{% block scripts %}
<!-- QR Code library for channel key sharing -->
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
<script src="{{ url_for('static', filename='js/stegasoo.js') }}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const input = document.getElementById('channelKeyQrInput');
const generateBtn = document.getElementById('channelKeyQrGenerate');
const showBtn = document.getElementById('channelKeyQrShow');
const container = document.getElementById('channelKeyQrContainer');
const canvas = document.getElementById('channelKeyQrCanvas');
const downloadBtn = document.getElementById('channelKeyQrDownload');
// Generate random key
generateBtn?.addEventListener('click', function() {
if (input && typeof Stegasoo !== 'undefined') {
input.value = Stegasoo.generateChannelKey();
}
});
// Show QR code
showBtn?.addEventListener('click', function() {
const key = input?.value?.trim().replace(/-/g, '');
if (!key || key.length !== 32) {
alert('Please enter a valid 32-character channel key');
return;
}
// Format key with dashes for QR
const formatted = key.match(/.{4}/g)?.join('-') || key;
// Generate QR code
if (typeof QRCode !== 'undefined' && canvas) {
QRCode.toCanvas(canvas, formatted, {
width: 200,
margin: 2,
color: { dark: '#000', light: '#fff' }
}, function(error) {
if (error) {
console.error('QR generation error:', error);
return;
}
container?.classList.remove('d-none');
});
}
});
// Download QR as PNG
downloadBtn?.addEventListener('click', function() {
if (canvas) {
const link = document.createElement('a');
link.download = 'stegasoo-channel-key.png';
link.href = canvas.toDataURL('image/png');
link.click();
}
});
});
</script>
{% endblock %}

View File

@@ -250,7 +250,10 @@
</div>
<div class="modal-footer justify-content-center">
<button type="button" class="btn btn-sm btn-outline-secondary" id="qrDownload">
<i class="bi bi-download me-1"></i>Download PNG
<i class="bi bi-download me-1"></i>Download
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="qrPrint">
<i class="bi bi-printer me-1"></i>Print Sheet
</button>
</div>
</div>
@@ -263,7 +266,7 @@
<script src="{{ url_for('static', filename='js/auth.js') }}"></script>
<script src="{{ url_for('static', filename='js/stegasoo.js') }}"></script>
{% if is_admin %}
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
<script src="{{ url_for('static', filename='js/qrcode.min.js') }}"></script>
{% endif %}
<script>
StegasooAuth.initPasswordConfirmation('accountForm', 'newPasswordInput', 'newPasswordConfirmInput');
@@ -305,20 +308,20 @@ function showKeyQr(channelKey, keyName) {
document.getElementById('qrKeyName').textContent = keyName;
document.getElementById('qrKeyDisplay').textContent = formatted;
// Generate QR code
// Generate QR code using QRious
const canvas = document.getElementById('qrCanvas');
if (typeof QRCode !== 'undefined' && canvas) {
QRCode.toCanvas(canvas, formatted, {
width: 200,
margin: 2,
color: { dark: '#000', light: '#fff' }
}, function(error) {
if (error) {
console.error('QR generation error:', error);
return;
}
if (typeof QRious !== 'undefined' && canvas) {
try {
new QRious({
element: canvas,
value: formatted,
size: 200,
level: 'M'
});
new bootstrap.Modal(document.getElementById('qrModal')).show();
});
} catch (error) {
console.error('QR generation error:', error);
}
}
}
@@ -333,6 +336,96 @@ document.getElementById('qrDownload')?.addEventListener('click', function() {
link.click();
}
});
// Print tiled QR sheet (US Letter)
document.getElementById('qrPrint')?.addEventListener('click', function() {
const canvas = document.getElementById('qrCanvas');
const keyText = document.getElementById('qrKeyDisplay').textContent;
const keyName = document.getElementById('qrKeyName').textContent;
if (canvas && keyText) {
printQrSheet(canvas, keyText, keyName);
}
});
// Print QR codes tiled on US Letter paper (8.5" x 11")
function printQrSheet(canvas, keyText, title) {
const qrDataUrl = canvas.toDataURL('image/png');
const printWindow = window.open('', '_blank');
if (!printWindow) {
alert('Please allow popups to print');
return;
}
// US Letter: 8.5" x 11" - create 4x5 grid of QR codes
const cols = 4;
const rows = 5;
let qrGrid = '';
for (let i = 0; i < rows * cols; i++) {
qrGrid += `
<div class="qr-tile">
<img src="${qrDataUrl}" alt="QR">
<div class="key-text">${keyText}</div>
</div>
`;
}
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>Stegasoo ${title} - Print Sheet</title>
<style>
@page {
size: letter;
margin: 0.2in;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Courier New', monospace;
background: white;
}
.grid {
display: grid;
grid-template-columns: repeat(${cols}, 1fr);
gap: 0.08in;
margin-top: 0.20in;
}
.qr-tile {
border: 1px dashed #ccc;
padding: 0.08in;
text-align: center;
page-break-inside: avoid;
}
.qr-tile img {
width: 1.4in;
height: 1.4in;
}
.key-text {
font-size: 6pt;
color: #333;
margin-top: 0.03in;
word-break: break-all;
}
.footer {
text-align: center;
padding-top: 0.1in;
font-size: 7pt;
color: #999;
}
</style>
</head>
<body>
<div class="grid">${qrGrid}</div>
<div class="footer">Cut along dashed lines</div>
<script>
window.onload = function() { window.print(); };
<\/script>
</body>
</html>
`);
printWindow.document.close();
}
{% endif %}
</script>
{% endblock %}

View File

@@ -0,0 +1,372 @@
{% extends "base.html" %}
{% block title %}System Settings - Stegasoo{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-10">
<!-- Channel Key Configuration -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-broadcast me-2"></i>Channel Key Configuration</h5>
</div>
<div class="card-body">
{% if channel_configured %}
<div class="alert alert-success mb-4">
<i class="bi bi-shield-lock me-2"></i>
<strong>Server channel key active:</strong>
<code class="ms-2">{{ channel_fingerprint }}</code>
<span class="text-muted ms-2">({{ channel_source }})</span>
</div>
{% else %}
<div class="alert alert-info mb-4">
<i class="bi bi-info-circle me-2"></i>
Server running in <strong>public mode</strong>.
Set <code>STEGASOO_CHANNEL_KEY</code> environment variable to enable server-wide channel isolation.
</div>
{% endif %}
<!-- QR Code Generator -->
<div class="card bg-dark border-secondary">
<div class="card-header">
<i class="bi bi-qr-code me-2"></i>Share Channel Key via QR
</div>
<div class="card-body">
<p class="small text-muted mb-3">Generate a QR code to share a channel key with others.</p>
<div class="row g-2 align-items-end">
<div class="col-md-8">
<label class="form-label small">Channel Key</label>
<div class="input-group">
<input type="text" class="form-control font-monospace" id="channelKeyQrInput"
placeholder="Enter or generate a key"
{% if channel_configured %}value="{{ channel_key_full }}"{% endif %}>
<button class="btn btn-outline-secondary" type="button" id="channelKeyQrGenerate"
title="Generate random key">
<i class="bi bi-shuffle"></i>
</button>
</div>
</div>
<div class="col-md-4">
<button class="btn btn-primary w-100" type="button" id="channelKeyQrShow">
<i class="bi bi-qr-code me-1"></i>Show QR
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Server Configuration -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-gear me-2"></i>Server Configuration</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<table class="table table-dark table-sm">
<tbody>
<tr>
<td><i class="bi bi-hdd-network me-2"></i>Hostname</td>
<td><code>{{ hostname }}</code></td>
</tr>
<tr>
<td><i class="bi bi-ethernet me-2"></i>Port</td>
<td><code>{{ port }}</code></td>
</tr>
<tr>
<td><i class="bi bi-shield-lock me-2"></i>HTTPS</td>
<td>
{% if https_enabled %}
<span class="badge bg-success"><i class="bi bi-lock me-1"></i>Enabled</span>
{% else %}
<span class="badge bg-warning text-dark"><i class="bi bi-unlock me-1"></i>Disabled</span>
{% endif %}
</td>
</tr>
<tr>
<td><i class="bi bi-person-lock me-2"></i>Authentication</td>
<td>
{% if auth_enabled %}
<span class="badge bg-success"><i class="bi bi-check me-1"></i>Enabled</span>
{% else %}
<span class="badge bg-danger"><i class="bi bi-x me-1"></i>Disabled</span>
{% endif %}
</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-6">
<table class="table table-dark table-sm">
<tbody>
<tr>
<td><i class="bi bi-file-earmark me-2"></i>Max Payload</td>
<td><code>{{ max_payload_kb }} KB</code></td>
</tr>
<tr>
<td><i class="bi bi-upload me-2"></i>Max Upload</td>
<td><code>{{ max_upload_mb }} MB</code></td>
</tr>
<tr>
<td><i class="bi bi-soundwave me-2"></i>DCT Mode</td>
<td>
{% if dct_available %}
<span class="badge bg-success"><i class="bi bi-check me-1"></i>Available</span>
{% else %}
<span class="badge bg-secondary">Not Available</span>
{% endif %}
</td>
</tr>
<tr>
<td><i class="bi bi-qr-code me-2"></i>QR Support</td>
<td>
{% if qr_available %}
<span class="badge bg-success"><i class="bi bi-check me-1"></i>Available</span>
{% else %}
<span class="badge bg-secondary">Not Available</span>
{% endif %}
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="alert alert-secondary small mt-3 mb-0">
<i class="bi bi-info-circle me-2"></i>
To change server settings, edit environment variables or config file and restart the service.
<br>See <code>STEGASOO_HTTPS_ENABLED</code>, <code>STEGASOO_PORT</code>, <code>STEGASOO_CHANNEL_KEY</code>
</div>
</div>
</div>
<!-- Environment Info -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-info-circle me-2"></i>Environment</h5>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-6 col-md-3 mb-3">
<div class="p-3 bg-dark rounded h-100">
<i class="bi bi-box text-primary fs-3 d-block mb-2"></i>
<div class="small text-muted">Version</div>
<strong>{{ version }}</strong>
</div>
</div>
<div class="col-6 col-md-3 mb-3">
<div class="p-3 bg-dark rounded h-100">
<i class="bi bi-terminal text-info fs-3 d-block mb-2"></i>
<div class="small text-muted">Python</div>
<strong>{{ python_version }}</strong>
</div>
</div>
<div class="col-6 col-md-3 mb-3">
<div class="p-3 bg-dark rounded h-100">
<i class="bi bi-cpu text-warning fs-3 d-block mb-2"></i>
<div class="small text-muted">Platform</div>
<strong>{{ platform }}</strong>
</div>
</div>
<div class="col-6 col-md-3 mb-3">
<div class="p-3 bg-dark rounded h-100">
<i class="bi bi-shield-check text-success fs-3 d-block mb-2"></i>
<div class="small text-muted">KDF</div>
<strong>{{ kdf_type }}</strong>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- QR Code Modal -->
<div class="modal fade" id="channelKeyQrModal" tabindex="-1">
<div class="modal-dialog modal-sm modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h6 class="modal-title"><i class="bi bi-qr-code me-2"></i>Channel Key</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center">
<canvas id="channelKeyQrCanvas" class="bg-white p-2 rounded"></canvas>
<div class="mt-2">
<code class="small" id="channelKeyQrDisplay"></code>
</div>
</div>
<div class="modal-footer justify-content-center">
<button type="button" class="btn btn-sm btn-outline-secondary" id="channelKeyQrDownload">
<i class="bi bi-download me-1"></i>Download
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="channelKeyQrPrint">
<i class="bi bi-printer me-1"></i>Print Sheet
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/qrcode.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/stegasoo.js') }}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const input = document.getElementById('channelKeyQrInput');
const generateBtn = document.getElementById('channelKeyQrGenerate');
const showBtn = document.getElementById('channelKeyQrShow');
const canvas = document.getElementById('channelKeyQrCanvas');
const displayEl = document.getElementById('channelKeyQrDisplay');
const downloadBtn = document.getElementById('channelKeyQrDownload');
const modalEl = document.getElementById('channelKeyQrModal');
const modal = modalEl ? new bootstrap.Modal(modalEl) : null;
// Generate random key
generateBtn?.addEventListener('click', function() {
if (!input) return;
if (typeof Stegasoo !== 'undefined' && Stegasoo.generateChannelKey) {
input.value = Stegasoo.generateChannelKey();
} else {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let key = '';
for (let i = 0; i < 8; i++) {
if (i > 0) key += '-';
for (let j = 0; j < 4; j++) {
key += chars.charAt(Math.floor(Math.random() * chars.length));
}
}
input.value = key;
}
});
// Show QR code in modal
showBtn?.addEventListener('click', function() {
const key = input?.value?.trim().replace(/-/g, '');
if (!key || key.length !== 32) {
alert('Please enter a valid 32-character channel key');
return;
}
const formatted = key.match(/.{4}/g)?.join('-') || key;
if (typeof QRious === 'undefined') {
alert('QR Code library failed to load.');
return;
}
try {
new QRious({
element: canvas,
value: formatted,
size: 200,
level: 'M'
});
if (displayEl) displayEl.textContent = formatted;
modal?.show();
} catch (error) {
alert('Failed to generate QR code: ' + error.message);
}
});
// Download QR as PNG
downloadBtn?.addEventListener('click', function() {
if (canvas) {
const link = document.createElement('a');
link.download = 'stegasoo-channel-key.png';
link.href = canvas.toDataURL('image/png');
link.click();
}
});
// Print tiled QR sheet (US Letter)
document.getElementById('channelKeyQrPrint')?.addEventListener('click', function() {
if (canvas && displayEl) {
printQrSheet(canvas, displayEl.textContent, 'Channel Key');
}
});
});
// Print QR codes tiled on US Letter paper (8.5" x 11")
function printQrSheet(canvas, keyText, title) {
const qrDataUrl = canvas.toDataURL('image/png');
const printWindow = window.open('', '_blank');
if (!printWindow) {
alert('Please allow popups to print');
return;
}
// US Letter: 8.5" x 11" - create 4x5 grid of QR codes
const cols = 4;
const rows = 5;
let qrGrid = '';
for (let i = 0; i < rows * cols; i++) {
qrGrid += `
<div class="qr-tile">
<img src="${qrDataUrl}" alt="QR">
<div class="key-text">${keyText}</div>
</div>
`;
}
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>Stegasoo ${title} - Print Sheet</title>
<style>
@page {
size: letter;
margin: 0.2in;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Courier New', monospace;
background: white;
}
.grid {
display: grid;
grid-template-columns: repeat(${cols}, 1fr);
gap: 0.08in;
margin-top: 0.20in;
}
.qr-tile {
border: 1px dashed #ccc;
padding: 0.08in;
text-align: center;
page-break-inside: avoid;
}
.qr-tile img {
width: 1.4in;
height: 1.4in;
}
.key-text {
font-size: 6pt;
color: #333;
margin-top: 0.03in;
word-break: break-all;
}
.footer {
text-align: center;
padding-top: 0.1in;
font-size: 7pt;
color: #999;
}
</style>
</head>
<body>
<div class="grid">${qrGrid}</div>
<div class="footer">Cut along dashed lines</div>
<script>
window.onload = function() { window.print(); };
<\/script>
</body>
</html>
`);
printWindow.document.close();
}
</script>
{% endblock %}

View File

@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Stegasoo{% endblock %}</title>
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
<link href="{{ url_for('static', filename='vendor/css/bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='vendor/css/bootstrap-icons.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='style.css') }}" rel="stylesheet">
</head>
<body>
@@ -23,26 +23,23 @@
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<ul class="navbar-nav ms-auto nav-icons">
<li class="nav-item">
<a class="nav-link" href="/"><i class="bi bi-house me-1"></i> Home</a>
<a class="nav-link nav-expand" href="/"><i class="bi bi-house"></i><span>Home</span></a>
</li>
{% if not auth_enabled or is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="/encode"><i class="bi bi-lock me-1"></i> Encode</a>
<a class="nav-link nav-expand" href="/encode"><i class="bi bi-lock"></i><span>Encode</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="/decode"><i class="bi bi-unlock me-1"></i> Decode</a>
<a class="nav-link nav-expand" href="/decode"><i class="bi bi-unlock"></i><span>Decode</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="/generate"><i class="bi bi-key me-1"></i> Generate</a>
<a class="nav-link nav-expand" href="/generate"><i class="bi bi-key"></i><span>Generate</span></a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" href="/about"><i class="bi bi-info-circle me-1"></i> About</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/tools"><i class="bi bi-tools me-1"></i> Tools</a>
<a class="nav-link nav-expand" href="/tools"><i class="bi bi-tools"></i><span>Tools</span></a>
</li>
{% if auth_enabled %}
{% if is_authenticated %}
@@ -54,6 +51,7 @@
<li><a class="dropdown-item" href="/account"><i class="bi bi-gear me-2"></i>Account</a></li>
{% if is_admin %}
<li><a class="dropdown-item" href="/admin/users"><i class="bi bi-people me-2"></i>Users</a></li>
<li><a class="dropdown-item" href="/admin/settings"><i class="bi bi-sliders me-2"></i>System Settings</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/logout"><i class="bi bi-box-arrow-left me-2"></i>Logout</a></li>
@@ -96,13 +94,15 @@
<small>
<img src="{{ url_for('static', filename='favicon.svg') }}" alt="" height="16" class="me-1" style="vertical-align: text-bottom;">
Stegasoo v{{ version }} — Steganography with Reference Photo + Passphrase + PIN/Key
<span class="mx-2">|</span>
<a href="/about" class="text-muted text-decoration-none"><i class="bi bi-info-circle me-1"></i>About</a>
</small>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- QR Code scanning library (v4.1.5) -->
<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
<script src="{{ url_for('static', filename='vendor/js/bootstrap.bundle.min.js') }}"></script>
<!-- QR Code scanning library (local) -->
<script src="{{ url_for('static', filename='vendor/js/html5-qrcode.min.js') }}"></script>
<script>
// Initialize toasts (auto-hide after delay)
document.querySelectorAll('.toast').forEach(el => new bootstrap.Toast(el));

File diff suppressed because it is too large Load Diff