fix(admin): sidebar hamburger menu for mobile, remove auto Cantaloupe sync

- Replace broken icon-only sidebar collapse with off-canvas hamburger menu
  that slides in from left (hidden by default on <768px)
- Add sidebar overlay backdrop, auto-close on nav/logout
- Rename 'Cantaloupe Sync' to 'Import' in sidebar and 'Excel Import' in page
- Remove 'Sync Now' button and backend subprocess auto-export entirely
- Keep manual Excel file upload as the only import path
- Update empty-state text and upload handler to work without Sync Now btn
This commit is contained in:
Leo
2026-05-22 16:08:07 -04:00
parent 301c3d5fb1
commit cd1f04a519
2 changed files with 73 additions and 83 deletions
+5 -37
View File
@@ -386,7 +386,7 @@ async def run_sync(
file_path: Optional[str] = Query(None, description="Server-side path to Excel file"),
):
"""
Run a full Cantaloupe sync from an Excel file.
Import an Excel file into a staged batch for review.
Accepts either:
- An uploaded Excel file (multipart form)
@@ -416,42 +416,10 @@ async def run_sync(
excel_path = str(p)
display_path = file_path
else:
# Try subprocess call to cantaloupe export
try:
import subprocess
import sys
cantaloupe_dir = os.path.expanduser("~/projects/cantaloupe-downloader")
result = subprocess.run(
[sys.executable, "-m", "cantaloupe", "export", "--scheduled"],
capture_output=True, text=True, timeout=120,
cwd=cantaloupe_dir,
)
if result.returncode != 0:
raise HTTPException(
status_code=400,
detail=f"Cantaloupe export failed: {result.stderr[:500]}"
)
# Parse output to find the exported file path
for line in result.stdout.splitlines() + result.stderr.splitlines():
line = line.strip()
if line and Path(line).exists() and line.endswith((".xlsx", ".xls")):
excel_path = line
display_path = line
break
else:
raise HTTPException(
status_code=400,
detail="Cantaloupe export ran but produced no recognizable file path. Provide file upload or file_path parameter."
)
except FileNotFoundError:
raise HTTPException(
status_code=400,
detail="No file provided and cantaloupe module not found. Upload an Excel file or provide file_path."
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Cantaloupe export error: {e}")
raise HTTPException(
status_code=400,
detail="No file provided. Upload an Excel file to import asset data."
)
conn = _get_db()
try:
+68 -46
View File
@@ -514,30 +514,51 @@
.hidden { display: none !important; }
.mono { font-family: 'Courier New', monospace; font-size: 12px; }
/* Responsive: collapse sidebar on narrow screens */
/* ═══════════════════════════════════════════════════════════════════════
HAMBURGER BUTTON & SIDEBAR OVERLAY (mobile)
═══════════════════════════════════════════════════════════════════════ */
.hamburger {
display: none;
background: none; border: none; color: var(--text); font-size: 24px;
cursor: pointer; padding: 6px; margin-right: 10px;
line-height: 1; flex-shrink: 0;
}
.hamburger:hover { color: var(--accent2); }
.sidebar-overlay {
display: none;
position: fixed; inset: 0; background: rgba(0,0,0,0.5);
z-index: 99; opacity: 0; transition: opacity 0.25s;
pointer-events: none;
}
.sidebar-overlay.open { opacity: 1; pointer-events: auto; }
/* Responsive: off-canvas sidebar on narrow screens */
@media (max-width: 768px) {
.hamburger { display: inline-flex; align-items: center; }
.sidebar-overlay { display: block; }
.sidebar {
width: 56px;
overflow: hidden;
transform: translateX(-100%);
transition: transform 0.25s ease;
z-index: 100;
}
.sidebar .sidebar-header span,
.sidebar .sidebar-user,
.sidebar .nav-item span,
.sidebar .nav-item .ni-badge,
.sidebar .sidebar-footer span,
.sidebar .sidebar-footer .logout-btn { display: none; }
.sidebar .nav-item { justify-content: center; padding: 11px 0; }
.sidebar .nav-item .ni-icon { font-size: 20px; width: auto; }
.sidebar .sidebar-header { justify-content: center; }
.sidebar .sidebar-header .sh-icon { font-size: 22px; }
.main { margin-left: 56px; padding: 16px; max-width: calc(100vw - 56px); }
.sidebar.open { transform: translateX(0); }
.main {
margin-left: 0;
padding: 16px;
max-width: 100vw;
}
.page-title .pt-actions .btn .btn-label { display: none; }
.page-title .pt-actions .btn { min-width: 36px; justify-content: center; }
/* Stats grid: tighter cards */
.stats-grid { grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 6px; }
.stat-card { padding: 10px; }
.stat-card .stat-value { font-size: 22px; }
/* Cantaloupe Sync batch rows stack */
/* Import batch rows stack */
.cs-batch-row .csb-status { width: 60px; }
.cs-batch-row .csb-time { width: auto; font-size: 11px; }
.cs-batch-row .csb-rows { width: auto; text-align: left; margin-left: auto; }
@@ -567,7 +588,7 @@
.page-title .pt-actions { width: 100%; }
.page-title .pt-actions button { flex: 1; }
/* Cantaloupe diff badges stack 2x2 */
/* Import diff badges stack 2x2 */
.cs-diff-summary { grid-template-columns: repeat(2, 1fr); }
.cs-diff-badge { padding: 8px; }
.cs-diff-badge .csd-val { font-size: 18px; }
@@ -586,7 +607,7 @@
}
/* ═══════════════════════════════════════════════════════════════════════
CANTALOUPE SYNC
EXCEL IMPORT
═══════════════════════════════════════════════════════════════════════ */
.cs-diff-summary {
display: grid;
@@ -657,8 +678,11 @@
</div>
</div>
<!-- ═══ SIDEBAR OVERLAY (mobile) ═══ -->
<div id="sidebarOverlay" class="sidebar-overlay" onclick="toggleSidebar()"></div>
<!-- ═══ SIDEBAR ═══ -->
<div class="sidebar">
<div class="sidebar" id="sidebar"> <!-- add hamburger to title bar-->
<div class="sidebar-header">
<span class="sh-icon">📊</span>
<span>Canteen Admin</span>
@@ -690,7 +714,7 @@
<span class="ni-icon">📤</span><span>Export</span>
</button>
<button class="nav-item" data-page="cantaloupe" onclick="switchPage('cantaloupe')">
<span class="ni-icon">🔄</span><span>Cantaloupe Sync</span>
<span class="ni-icon">📥</span><span>Import</span>
</button>
</div>
<div class="sidebar-footer">
@@ -702,6 +726,11 @@
<!-- ═══ MAIN CONTENT ═══ -->
<div class="main">
<!-- Hamburger button (mobile) -->
<div style="display:flex;align-items:center;margin-bottom:12px;" class="mobile-title-row">
<button class="hamburger" onclick="toggleSidebar()" aria-label="Toggle menu"></button>
</div>
<!-- ── DASHBOARD ── -->
<div id="pageDashboard" class="page">
<div class="page-title">
@@ -858,14 +887,13 @@
</div>
</div>
<!-- ── CANTALOUPE SYNC ── -->
<!-- ── EXCEL IMPORT ── -->
<div id="pageCantaloupe" class="page hidden">
<div class="page-title">
<span>Cantaloupe Sync</span>
<span>Excel Import</span>
<div class="pt-actions">
<button class="btn btn-sm btn-primary" onclick="runSync()" id="csSyncBtn">🔄 Sync Now</button>
<input type="file" id="csFileInput" accept=".xlsx,.xls" style="display:none" onchange="uploadSyncFile(event)">
<button class="btn btn-sm btn-outline" onclick="document.getElementById('csFileInput').click()">📤 Upload Excel</button>
<button class="btn btn-sm btn-primary" onclick="document.getElementById('csFileInput').click()">📤 Upload Excel</button>
<button class="btn btn-sm btn-outline" onclick="loadCantaloupeSync()">↻ Refresh</button>
</div>
</div>
@@ -969,6 +997,12 @@ function updateUserUI() {
if (logoutBtn) logoutBtn.style.display = isLoggedIn() ? 'block' : 'none';
}
// ── Sidebar toggle (mobile) ──
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('open');
document.getElementById('sidebarOverlay').classList.toggle('open');
}
// ═════════════════════════════════════════════════════════════════════════════
// TOAST & MODAL
// ═════════════════════════════════════════════════════════════════════════════
@@ -1009,6 +1043,9 @@ function switchPage(page) {
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
const navItem = document.querySelector(`.nav-item[data-page="${page}"]`);
if (navItem) navItem.classList.add('active');
// Close sidebar on mobile after nav
document.getElementById('sidebar')?.classList.remove('open');
document.getElementById('sidebarOverlay')?.classList.remove('open');
// Lazy load
if (page === 'dashboard') loadDashboard();
else if (page === 'settings') renderAllSettings();
@@ -1062,6 +1099,9 @@ function doLogout() {
localStorage.removeItem('adminToken');
document.getElementById('loginOverlay').classList.remove('hidden');
updateUserUI();
// Close sidebar on mobile
document.getElementById('sidebar')?.classList.remove('open');
document.getElementById('sidebarOverlay')?.classList.remove('open');
}
async function initAuth() {
@@ -2099,7 +2139,7 @@ async function loadCantaloupeSync() {
function renderCsOverview() {
const el = document.getElementById('csOverviewContent');
if (!csLatestBatch) {
el.innerHTML = '<div class="empty-state"><div class="es-icon">🔄</div>No sync batches yet. Click "Sync Now" to import data.</div>';
el.innerHTML = '<div class="empty-state"><div class="es-icon">📥</div>No imports yet. Upload an Excel file to import asset data.</div>';
return;
}
const b = csLatestBatch;
@@ -2305,32 +2345,15 @@ async function toggleBatchHistory(batchId) {
}
}
// ── Sync Now ──
async function runSync() {
const btn = document.getElementById('csSyncBtn');
btn.disabled = true;
btn.innerHTML = '<div class="spinner"></div> Syncing...';
try {
const result = await api('/api/admin/cantaloupe/sync', { method: 'POST' });
showToast('Sync completed — batch #' + result.id);
await loadCantaloupeSync();
} catch (e) {
showToast('Sync failed: ' + e.message, true);
} finally {
btn.disabled = false;
btn.innerHTML = '🔄 Sync Now';
}
}
// ── Upload Excel File ──
let _csUploading = false;
async function uploadSyncFile(event) {
const file = event.target.files?.[0];
if (!file) return;
if (_csUploading) return;
_csUploading = true;
const fd = new FormData();
fd.append('file', file);
const btn = document.getElementById('csSyncBtn');
btn.disabled = true;
btn.innerHTML = '<div class="spinner"></div> Uploading...';
try {
const result = await api('/api/admin/cantaloupe/sync', { method: 'POST', body: fd });
showToast('Upload processed — batch #' + result.id);
@@ -2338,8 +2361,7 @@ async function uploadSyncFile(event) {
} catch (e) {
showToast('Upload failed: ' + e.message, true);
} finally {
btn.disabled = false;
btn.innerHTML = '🔄 Sync Now';
_csUploading = false;
event.target.value = ''; // reset so same file can be re-picked
}
}