diff --git a/expressjs/src/server.js b/expressjs/src/server.js
index 4769f11..9359f24 100644
--- a/expressjs/src/server.js
+++ b/expressjs/src/server.js
@@ -68,6 +68,7 @@ db.serialize(() => {
detail TEXT,
created_at INTEGER NOT NULL
)`);
+ db.run('ALTER TABLE uploads ADD COLUMN downloads INTEGER DEFAULT 0', (err) => { /* ignore if column exists */ });
db.run('CREATE INDEX IF NOT EXISTS admin_logs_event_idx ON admin_logs(event)');
db.run('CREATE INDEX IF NOT EXISTS admin_logs_created_idx ON admin_logs(created_at)');
db.run(`CREATE TABLE IF NOT EXISTS users (
@@ -559,10 +560,15 @@ function renderPage(title, body, mainClass = '') {
}
button.secondary:hover { border-color: var(--text-muted); background: var(--bg); }
- .row { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
- .row .card { margin: 0; padding: 1.25rem; text-align: center; }
- .row .card strong { display: block; font-size: 0.8rem; text-transform: uppercase; color: var(--text-muted); letter-spacing: 0.05em; margin-bottom: 0.5rem; }
- .row .card .muted { font-size: 1.5rem; color: var(--text-main); font-weight: 600; }
+ .row table { margin-bottom: 0; }
+ .row td { padding: 0.5rem 1rem; border-bottom: 1px solid var(--border); }
+ .row tr:last-child td { border-bottom: none; }
+ .row strong { color: var(--text-muted); font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.05em; }
+ .row .muted { font-size: 1.1rem; color: var(--text-main); font-weight: 600; text-align: right; }
+
+ .truncate { max-width: 250px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+ .stack { width: 100%; margin-top: 0.25rem; }
+ .stack button { width: 100%; justify-content: center; }
table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 0.925rem; }
th { text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--border); color: var(--text-muted); font-weight: 600; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; }
@@ -889,12 +895,38 @@ app.get(`${basePath}/admin/dashboard`, requireAdminPage, async (req, res) => {
const stats = `
-
Aktive Uploads${activeCount.count}
-
Aktive Größe${formatBytes(activeBytes.total)}
-
Aktive Nutzer${distinctOwners.count}
-
Uploads gesamt${totalUploads.count}
-
Löschungen gesamt${totalDeletes.count}
-
Letztes Cleanup${lastCleanup.ts ? formatTimestamp(lastCleanup.ts) : '—'}
+
+
+
+ | Aktive Uploads |
+ ${activeCount.count} |
+
+
+ | Aktive Größe |
+ ${formatBytes(activeBytes.total)} |
+
+
+ | Aktive Nutzer |
+ ${distinctOwners.count} |
+
+
+ | Uploads gesamt |
+ ${totalUploads.count} |
+
+
+ | Downloads gesamt |
+ ${await get('SELECT SUM(downloads) as count FROM uploads').then(r => r.count || 0)} |
+
+
+ | Löschungen gesamt |
+ ${totalDeletes.count} |
+
+
+ | Letztes Cleanup |
+ ${lastCleanup.ts ? formatTimestamp(lastCleanup.ts) : '—'} |
+
+
+
`;
@@ -912,10 +944,10 @@ app.get(`${basePath}/admin/dashboard`, requireAdminPage, async (req, res) => {
const fileHref = encodeURI(fileUrl);
return `
- | ${escapeHtml(item.owner)} |
+ ${escapeHtml(item.owner)} |
- ${escapeHtml(item.original_name)}
-
+ ${escapeHtml(item.original_name)}
+
|
${formatBytes(item.size_bytes)} |
@@ -932,6 +964,9 @@ app.get(`${basePath}/admin/dashboard`, requireAdminPage, async (req, res) => {
+
+
+
|
`;
@@ -974,12 +1009,11 @@ app.get(`${basePath}/admin/dashboard`, requireAdminPage, async (req, res) => {
- Aktive Uploads
- ${allUploads.length ? `
+ Aktuelle Uploads
+ ${uploads.length ? `
- | Nutzer |
Datei |
Größe |
Läuft ab |
@@ -987,11 +1021,84 @@ app.get(`${basePath}/admin/dashboard`, requireAdminPage, async (req, res) => {
- ${adminUploadsRows}
+ ${rows}
- ` : 'Keine aktiven Uploads.
'}
+ ` : 'Noch keine Uploads.
'}
+
+
`;
res.send(renderPage('Adminübersicht', body, 'wide'));
});
@@ -1603,9 +1710,9 @@ app.get(`${basePath}/dashboard`, requireAuthPage, async (req, res) => {
const fileHref = encodeURI(fileUrl);
return `
- |
- ${escapeHtml(item.original_name)}
-
+ |
+ ${escapeHtml(item.original_name)}
+
|
${formatBytes(item.size_bytes)} |
@@ -1622,9 +1729,9 @@ app.get(`${basePath}/dashboard`, requireAuthPage, async (req, res) => {
- |
-
-
+
+
+
|
`;
@@ -1670,7 +1777,6 @@ app.get(`${basePath}/dashboard`, requireAuthPage, async (req, res) => {
Größe |
Läuft ab |
Aktionen |
- Link |
@@ -1716,18 +1822,34 @@ app.get(`${basePath}/dashboard`, requireAuthPage, async (req, res) => {
const originalText = button.textContent;
button.addEventListener('click', async () => {
const path = button.dataset.path || '';
+ const name = button.dataset.name || 'Download';
const url = window.location.origin + path;
+
try {
- await navigator.clipboard.writeText(url);
+ // Try to write rich text (HTML link) + plain text fallback
+ const html = `${name}`;
+ const blobHtml = new Blob([html], { type: 'text/html' });
+ const blobText = new Blob([url], { type: 'text/plain' });
+ const data = [new ClipboardItem({
+ 'text/html': blobHtml,
+ 'text/plain': blobText,
+ })];
+ await navigator.clipboard.write(data);
button.textContent = 'Kopiert!';
} catch (err) {
- const helper = document.createElement('textarea');
- helper.value = url;
- document.body.appendChild(helper);
- helper.select();
- document.execCommand('copy');
- document.body.removeChild(helper);
- button.textContent = 'Kopiert!';
+ // Fallback to simple text copy if Clipboard Item API fails or is not supported
+ try {
+ await navigator.clipboard.writeText(url);
+ button.textContent = 'Kopiert!';
+ } catch (fallbackErr) {
+ const helper = document.createElement('textarea');
+ helper.value = url;
+ document.body.appendChild(helper);
+ helper.select();
+ document.execCommand('copy');
+ document.body.removeChild(helper);
+ button.textContent = 'Kopiert!';
+ }
}
setTimeout(() => {
button.textContent = originalText;
@@ -1841,12 +1963,13 @@ app.get('/_share/:filename', async (req, res) => {
return;
}
- const row = await get('SELECT original_name, stored_path FROM uploads WHERE stored_name = ?', [filename]);
+ const row = await get('SELECT id, original_name, stored_path FROM uploads WHERE stored_name = ?', [filename]);
if (!row) {
// If not found in DB, check if it exists on disk (legacy or manual files)
const filePath = path.join(shareDir, filename);
if (fs.existsSync(filePath)) {
+ // Log download for legacy/manual files if needed, or just skip
res.download(filePath, filename); // Fallback: download with stored name
return;
}
@@ -1854,6 +1977,10 @@ app.get('/_share/:filename', async (req, res) => {
return;
}
+ // Log download
+ run('UPDATE uploads SET downloads = downloads + 1 WHERE id = ?', [row.id]).catch(() => undefined);
+ logEvent('download', null, { name: filename, original: row.original_name }).catch(() => undefined);
+
res.download(row.stored_path, row.original_name);
});