feat: add real-time backend online/offline status indicator and live upload byte progress

This commit is contained in:
katR 2026-09-12 10:07:59 +05:30
parent 5d426ff72a
commit 7f3c84763e
4 changed files with 134 additions and 4 deletions

View file

@ -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;

View file

@ -15,7 +15,14 @@
<main class="container">
<header class="header">
<h1>katdrop</h1>
<p class="subtitle">// minimal file transfer</p>
<div class="subtitle-row">
<span class="subtitle">// minimal file transfer</span>
<span class="status-sep">•</span>
<div id="backendStatus" class="backend-status" title="Click to refresh status">
<span class="status-dot"></span>
<span id="statusText" class="status-text">connecting...</span>
</div>
</div>
</header>
<div class="drop-zone" id="dropZone">
@ -27,6 +34,7 @@
<div id="fileSelection" class="file-selection"></div>
<div class="progress-wrap" id="progressWrap">
<div class="progress-bar" id="progressBar"></div>
<div class="progress-text" id="progressText"></div>
</div>
</div>

View file

@ -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);
})();

View file

@ -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) {