From 7f3c84763ea08e70577493ec77d5ac22facb215a Mon Sep 17 00:00:00 2001 From: oiupoyt Date: Sat, 12 Sep 2026 10:07:59 +0530 Subject: [PATCH] feat: add real-time backend online/offline status indicator and live upload byte progress --- public/css/style.css | 69 ++++++++++++++++++++++++++++++++++++++++++-- public/index.html | 10 ++++++- public/js/app.js | 54 +++++++++++++++++++++++++++++++++- server.js | 5 ++++ 4 files changed, 134 insertions(+), 4 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index 9dd6214..e48ce7e 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -46,13 +46,70 @@ h1 { margin-bottom: 4px; } +.subtitle-row { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + font-size: 0.78rem; + letter-spacing: 0.04em; +} + .subtitle { - font-size: 0.8rem; color: #555; - letter-spacing: 0.05em; text-transform: lowercase; } +.status-sep { + color: #333; +} + +.backend-status { + display: inline-flex; + align-items: center; + gap: 5px; + color: #666; + font-size: 0.72rem; + cursor: pointer; + user-select: none; + transition: color 0.15s ease; +} + +.backend-status:hover { + color: #999; +} + +.status-dot { + width: 6px; + height: 6px; + background: #444; + display: inline-block; + transition: background-color 0.2s ease, box-shadow 0.2s ease; +} + +.backend-status.online { + color: #4ade80; +} +.backend-status.online .status-dot { + background: #4ade80; + box-shadow: 0 0 6px rgba(74, 222, 128, 0.45); +} + +.backend-status.offline { + color: #ff5555; +} +.backend-status.offline .status-dot { + background: #ff5555; + box-shadow: 0 0 6px rgba(255, 85, 85, 0.45); +} + +.backend-status.checking { + color: #eab308; +} +.backend-status.checking .status-dot { + background: #eab308; +} + /* Upload Drop Zone */ .drop-zone { border: 1px dashed #2a2a2a; @@ -149,6 +206,14 @@ h1 { to { opacity: 1; transform: translateY(0); } } +.progress-text { + margin-top: 8px; + font-size: 0.72rem; + color: #777; + letter-spacing: 0.02em; + font-variant-numeric: tabular-nums; +} + /* Files Section */ .files-section { margin-top: 42px; diff --git a/public/index.html b/public/index.html index 6294a78..b00a53f 100644 --- a/public/index.html +++ b/public/index.html @@ -15,7 +15,14 @@

katdrop

-

// minimal file transfer

+
+ // minimal file transfer + • +
+ + connecting... +
+
@@ -27,6 +34,7 @@
+
diff --git a/public/js/app.js b/public/js/app.js index 8c84446..c59990a 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -11,8 +11,11 @@ const fileSelection = document.getElementById('fileSelection'); const progressWrap = document.getElementById('progressWrap'); const progressBar = document.getElementById('progressBar'); + const progressText = document.getElementById('progressText'); const fileList = document.getElementById('fileList'); const fileCount = document.getElementById('fileCount'); + const backendStatus = document.getElementById('backendStatus'); + const statusText = document.getElementById('statusText'); const toastEl = document.getElementById('toast'); let selectedFiles = []; @@ -38,6 +41,39 @@ }, 2200); } + // Utility: Format bytes + function formatBytes(bytes) { + if (!bytes || bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; + } + + // Backend Status Check + async function checkBackendStatus() { + if (!backendStatus || !statusText) return; + backendStatus.className = 'backend-status checking'; + statusText.textContent = 'checking...'; + + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 4000); + const res = await fetch(`${API_BASE}/health`, { signal: controller.signal }); + clearTimeout(timeoutId); + + if (res.ok) { + backendStatus.className = 'backend-status online'; + statusText.textContent = 'online'; + } else { + throw new Error('Non-200'); + } + } catch (err) { + backendStatus.className = 'backend-status offline'; + statusText.textContent = 'offline'; + } + } + // Interactive Particle Background function initInteractiveParticles() { const canvas = document.getElementById('particles'); @@ -332,14 +368,19 @@ if (e.lengthComputable) { const pct = Math.round((e.loaded / e.total) * 100); progressBar.style.width = `${pct}%`; + if (progressText) { + progressText.textContent = `uploading: ${pct}% (${formatBytes(e.loaded)} / ${formatBytes(e.total)})`; + } } }); xhr.onload = () => { progressBar.style.width = '100%'; + if (progressText) progressText.textContent = 'processing...'; setTimeout(() => { progressWrap.classList.remove('active'); progressBar.style.width = '0%'; + if (progressText) progressText.textContent = ''; fileInput.value = ''; updateFileSelection([]); fetchFiles(); @@ -350,7 +391,9 @@ xhr.onerror = () => { progressWrap.classList.remove('active'); uploadBtn.disabled = false; - showToast('upload failed'); + if (progressText) progressText.textContent = ''; + showToast('upload failed - check connection'); + checkBackendStatus(); }; xhr.open('POST', `${API_BASE}/upload`); @@ -366,6 +409,13 @@ uploadSelectedFiles(); }); + if (backendStatus) { + backendStatus.addEventListener('click', () => { + checkBackendStatus(); + fetchFiles(); + }); + } + // Drag and drop ['dragenter', 'dragover'].forEach(name => { dropZone.addEventListener(name, e => { @@ -392,5 +442,7 @@ // Init initInteractiveParticles(); + checkBackendStatus(); fetchFiles(); + setInterval(checkBackendStatus, 15000); })(); diff --git a/server.js b/server.js index e61b698..011e583 100644 --- a/server.js +++ b/server.js @@ -104,6 +104,11 @@ app.use((req, res, next) => { app.use(express.json()); app.use(express.static(publicDir)); +// Health check endpoint +app.get('/health', (req, res) => { + res.json({ status: 'ok', uptime: Math.floor(process.uptime()) }); +}); + // Upload endpoint (supports single or multiple files) app.post('/upload', upload.array('file', 20), (req, res) => { if (!req.files || req.files.length === 0) {