Restructure frontend into templates/ and static/

- Move index.html → templates/index.html (Jinja2, uses url_for)
- Extract CSS → static/css/app.css
- Split JS into static/js/: utils.js, dashboard.js, shares.js,
  quotas.js, users.js, main.js
- Switch app.py from send_file to render_template

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Sonnet 4.6 committed 2026-06-19 22:19:46 +02:00
1 parent 822e6b3b87
commit 1118681fd2
10 files changed
+607 -642

No files matched your search

+70
View File
@@ -0,0 +1,70 @@
let _qCtx = null;
function openQuotaModal(type, name, path, validUsers) {
_qCtx = { type, name, path, users: validUsers ? validUsers.split(/[\s,]+/).filter(Boolean) : [] };
document.getElementById('quotaModalTitle').textContent =
'Quota — ' + (type === 'smb' ? esc(name) : esc(path));
let body = '';
if (type === 'smb' && _qCtx.users.length > 0) {
body = _qCtx.users.map(u => `
<div class="mb-3">
<label class="form-label" style="color:var(--muted); font-size:.85rem">${esc(u)}</label>
<div class="input-group">
<input type="number" class="form-control quota-inp" data-user="${esc(u)}" placeholder="GB" min="1">
<span class="input-group-text">GB</span>
</div>
</div>`).join('');
} else {
const note = type === 'nfs'
? `<p class="mb-3" style="color:var(--muted); font-size:.87rem">Path: <code>${esc(path)}</code></p>`
: `<p class="mb-3" style="color:var(--muted); font-size:.87rem">No valid users on this share — enter a username manually.</p>`;
body = note + `
<div class="mb-3">
<label class="form-label" style="color:var(--muted); font-size:.85rem">Username</label>
<input type="text" id="qManualUser" class="form-control" placeholder="username">
</div>
<div class="mb-3">
<label class="form-label" style="color:var(--muted); font-size:.85rem">Quota</label>
<div class="input-group">
<input type="number" id="qManualGB" class="form-control" placeholder="GB" min="1">
<span class="input-group-text">GB</span>
</div>
</div>`;
}
document.getElementById('quotaModalBody').innerHTML = body;
new bootstrap.Modal(document.getElementById('quotaModal')).show();
}
async function submitQuota() {
if (!_qCtx) return;
const calls = [];
if (_qCtx.type === 'smb' && _qCtx.users.length > 0) {
for (const inp of document.querySelectorAll('.quota-inp')) {
const gb = parseInt(inp.value);
if (!gb || gb <= 0) continue;
calls.push({ type: 'smb', user: inp.dataset.user, quota: gb });
}
} else {
const user = document.getElementById('qManualUser')?.value.trim();
const gb = parseInt(document.getElementById('qManualGB')?.value);
if (!user) return showToast('Username is required.', 'danger');
if (!gb || gb <= 0) return showToast('Quota must be a positive number.', 'danger');
calls.push({ type: _qCtx.type, user, path: _qCtx.path, quota: gb });
}
if (!calls.length) return showToast('No quota values entered.', 'warning');
bootstrap.Modal.getInstance(document.getElementById('quotaModal')).hide();
const results = await Promise.all(calls.map(body =>
apiFetch('/api/quota', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body) })
));
const failed = results.filter(r => r && !r.ok).length;
if (failed) showToast(`${failed} quota(s) failed to apply.`, 'danger');
else showToast(`Quota${calls.length > 1 ? 's' : ''} applied.`);
loadDashboard();
}