From b7750560da678ecacc0546303edf0461a1463d27 Mon Sep 17 00:00:00 2001 From: ddrayko Date: Tue, 30 Jun 2026 18:24:59 +0200 Subject: [PATCH 01/76] feat: add notify-all feature to admin settings --- public/js/admin.js | 74 ++++++++++++++++++++++++++++++++++++++++++++++ routes/admin.js | 31 +++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/public/js/admin.js b/public/js/admin.js index 71f829a..20262b0 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -780,6 +780,72 @@ function showNotifyModal(userId) { }); } +// ─── Send Notification to All Users Modal ─────────────── +function showNotifyAllModal() { + const content = $a('#admin-modal-content'); + const overlay = $a('#admin-modal-overlay'); + if (!content || !overlay) return; + + content.innerHTML = ahtml` +
+

Notify All Users

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ `; + overlay.style.display = 'flex'; + + $a('#admin-notify-all-form')?.addEventListener('submit', async (e) => { + e.preventDefault(); + const btn = $a('#admin-btn-send-notify-all'); + const msgEl = $a('#admin-notify-all-msg'); + const titleEl = $a('#admin-notify-all-title'); + const messageEl = $a('#admin-notify-all-message'); + const typeEl = $a('#admin-notify-all-type'); + btn.disabled = true; + btn.innerHTML = ' Sending...'; + if (msgEl) msgEl.style.display = 'none'; + try { + const data = await adminApi('/notify-all', { + method: 'POST', + body: JSON.stringify({ + title: titleEl.value.trim(), + message: messageEl.value.trim(), + type: typeEl.value, + }), + }); + if (msgEl) { msgEl.textContent = `Notification sent to ${data.count} user(s) successfully`; msgEl.style.display = 'block'; msgEl.style.color = 'var(--accent-green)'; } + setTimeout(() => { closeAdminModal(); }, 1500); + } catch (err) { + if (msgEl) { msgEl.textContent = err.message; msgEl.style.display = 'block'; msgEl.style.color = 'var(--accent-red)'; } + btn.disabled = false; + btn.innerHTML = 'Send to All Users'; + } + }); +} + // ─── Dashboard ────────────────────────────────────────── async function renderAdminDashboard() { document.querySelectorAll('.admin-page').forEach(p => p.classList.remove('active')); @@ -1262,9 +1328,17 @@ async function renderAdminSettings() {

Eggs Settings

Manage nests, eggs, and per-egg resource overrides

+
+
+ +
+

Send Notification to All Users

+

Send a message to every user's notification inbox

+
`; $a('#settings-eggs-entry')?.addEventListener('click', () => adminNavigateTo('settings/eggs')); + $a('#settings-notify-all-entry')?.addEventListener('click', () => showNotifyAllModal()); initIcons(); } diff --git a/routes/admin.js b/routes/admin.js index 19725fd..1dd55d5 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -431,6 +431,37 @@ router.post('/users/:id/notify', authenticateToken, requireAdmin, async (req, re } }); +router.post('/notify-all', authenticateToken, requireAdmin, async (req, res) => { + try { + const { title, message, type } = req.body; + if (!title || !title.trim()) { + return res.status(400).json({ error: 'Title is required' }); + } + if (!message || !message.trim()) { + return res.status(400).json({ error: 'Message is required' }); + } + + const validTypes = ['info', 'success', 'warning', 'error']; + const notifType = validTypes.includes(type) ? type : 'info'; + + const users = await query('SELECT id FROM users'); + if (users.length === 0) { + return res.status(404).json({ error: 'No users found' }); + } + + for (const user of users) { + await createNotification(user.id, title.trim(), message.trim(), notifType); + } + + await logActivity(req.user.userId, 'admin_notify_all', `Sent notification to all ${users.length} user(s): ${title.trim()}`, null); + + res.json({ success: true, count: users.length }); + } catch (err) { + console.error('Admin notify-all error:', err.message); + res.status(500).json({ error: 'Failed to send notifications: ' + err.message }); + } +}); + router.delete('/users/:id', authenticateToken, requireAdmin, async (req, res) => { try { const userId = parseInt(req.params.id, 10); From 1ea298a3dc663f916025118fb3c71e1d84fe845e Mon Sep 17 00:00:00 2001 From: ddrayko Date: Tue, 30 Jun 2026 18:29:16 +0200 Subject: [PATCH 02/76] feat: add modal popup for truncated notification messages --- public/css/style.css | 81 ++++++++++++++++++++++++++++++++++++++++++++ public/js/app.js | 44 ++++++++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/public/css/style.css b/public/css/style.css index cc73afc..c70339f 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1410,6 +1410,87 @@ tbody tr:hover { margin-top: 6px; } +.notif-view-modal { + position: fixed; + inset: 0; + z-index: 200; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease; +} + +.notif-view-modal.open { + opacity: 1; + pointer-events: auto; +} + +.notif-view-modal-content { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 0; + max-width: 500px; + width: 90%; + max-height: 80vh; + display: flex; + flex-direction: column; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4); + animation: notifModalIn 0.2s ease; +} + +@keyframes notifModalIn { + from { transform: scale(0.95); opacity: 0; } + to { transform: scale(1); opacity: 1; } +} + +.notif-view-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 24px 0; +} + +.notif-view-modal-header h3 { + font-size: 1.05rem; + font-weight: 700; + color: var(--text-primary); + margin: 0; + flex: 1; +} + +.notif-view-modal-close { + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + padding: 4px; + border-radius: var(--radius-sm); + transition: all var(--transition); + flex-shrink: 0; + margin-left: 12px; +} + +.notif-view-modal-close:hover { + color: var(--text-primary); + background: rgba(255, 255, 255, 0.06); +} + +.notif-view-modal-body { + padding: 16px 24px 24px; + overflow-y: auto; + color: var(--text-secondary); + font-size: 0.9rem; + line-height: 1.6; + white-space: pre-wrap; + word-break: break-word; +} + .notif-badge { position: absolute; top: -6px; diff --git a/public/js/app.js b/public/js/app.js index f355db2..f6a262e 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -243,6 +243,43 @@ async function fetchNotifications() { } catch {} } +function showNotifDetailModal(notif) { + const existing = $('#notif-view-modal'); + if (existing) existing.remove(); + + const overlay = document.createElement('div'); + overlay.className = 'notif-view-modal'; + overlay.id = 'notif-view-modal'; + overlay.innerHTML = html` +
+
+

${notif.title}

+ +
+
${notif.message}
+
+ `; + document.body.appendChild(overlay); + + requestAnimationFrame(() => { + overlay.classList.add('open'); + }); + + overlay.addEventListener('click', closeNotifDetailModal); + $('#notif-view-modal-close-btn')?.addEventListener('click', closeNotifDetailModal); + + initIcons(); +} + +function closeNotifDetailModal() { + const overlay = $('#notif-view-modal'); + if (!overlay) return; + overlay.classList.remove('open'); + setTimeout(() => overlay.remove(), 200); +} + function renderNotifications() { const list = $('#notif-panel-list'); if (!list) return; @@ -263,9 +300,16 @@ function renderNotifications() { `).join(''); list.querySelectorAll('.notif-item.notif-unread').forEach(el => { + const msgEl = el.querySelector('.notif-item-msg'); + const isTruncated = msgEl && msgEl.scrollHeight > msgEl.clientHeight; + el.addEventListener('click', () => { const id = parseInt(el.dataset.id, 10); markAsRead(id); + if (isTruncated) { + const notif = state.notifications.find(n => n.id === id); + if (notif) showNotifDetailModal(notif); + } }); }); initIcons(); From f4b491e82dde78bc15fe2af01d14418c9e58601c Mon Sep 17 00:00:00 2001 From: ddrayko Date: Tue, 30 Jun 2026 18:30:49 +0200 Subject: [PATCH 03/76] fix: allow clicking read notifications to view full message in modal --- public/js/app.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/js/app.js b/public/js/app.js index f6a262e..76c083b 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -299,13 +299,15 @@ function renderNotifications() { `).join(''); - list.querySelectorAll('.notif-item.notif-unread').forEach(el => { + list.querySelectorAll('.notif-item').forEach(el => { const msgEl = el.querySelector('.notif-item-msg'); const isTruncated = msgEl && msgEl.scrollHeight > msgEl.clientHeight; el.addEventListener('click', () => { const id = parseInt(el.dataset.id, 10); - markAsRead(id); + if (el.classList.contains('notif-unread')) { + markAsRead(id); + } if (isTruncated) { const notif = state.notifications.find(n => n.id === id); if (notif) showNotifDetailModal(notif); From 54c1239a809b449565b9a03184b1a8e58e99189b Mon Sep 17 00:00:00 2001 From: ddrayko Date: Wed, 1 Jul 2026 02:10:19 +0200 Subject: [PATCH 04/76] =?UTF-8?q?Replace=20logo=20URLs:=20status.zero-host?= =?UTF-8?q?.org/upload/logo1.png=20=E2=86=92=20img.zero-host.org/assets/pi?= =?UTF-8?q?cto.png?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/index.html | 2 +- public/js/admin.js | 4 ++-- public/js/app.js | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/public/index.html b/public/index.html index 495d3f5..dfa3607 100644 --- a/public/index.html +++ b/public/index.html @@ -4,7 +4,7 @@ ZeroHost Dashboard - + diff --git a/public/js/admin.js b/public/js/admin.js index 20262b0..165fae1 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -115,7 +115,7 @@ function renderAdminLogin() {

Admin Panel

@@ -192,7 +192,7 @@ function renderAdminLayout() {