finally proper notifications! Frontend polls endpoint repeatedly, modal renders nice list and notifications can be deleted by the click of a button! This took longer then it should have

This commit is contained in:
2026-09-09 19:09:26 +02:00
parent f016d88d55
commit ebd9ec2fb1
6 changed files with 88 additions and 23 deletions
+2
View File
@@ -24,6 +24,8 @@ I'm not a web dev, and the frontend is 90% vibe-coded. Not like single-prompt vi
| `GET /modules/<module-name>/page` | Embeddable html content of module |
| `POST /modules/<module-name>/sync` | Triggers a backend data sync (module-specific) |
| `GET /modules/<module-name>/*` | Custom endpoints registered by module |
| `GET /notifications` | List of open notifications |
| `DELETE /notifications/delete/<noti-id>` | Delete a notification by its id |
## Auth
+1 -1
View File
@@ -17,7 +17,7 @@ mod_manager.load(app, "modules")
def api_notifications():
return jsonify(ntfy.NOTIFICATION_MANAGER.serialized())
@app.route("/notifications/delete/<nid>")
@app.route("/notifications/delete/<nid>", methods=["DELETE"])
def api_delete_notification(nid):
is_deleted = ntfy.NOTIFICATION_MANAGER.delete(nid)
if is_deleted:
-4
View File
@@ -32,10 +32,6 @@ class NotificationManager:
def __init__(self):
self.notifications = {}
# test notifications
self.notify(notification=Notification("socialwatch", Notification.Status.INFO, "some info from sw"))
self.notify(notification=Notification("testmodule",Notification.Status.ERROR, "some error from tm"))
def notify(self, notification: Notification):
self.notifications[str(uuid.uuid4())] = notification
+30 -6
View File
@@ -70,12 +70,36 @@ main {
}
/* notification modal */
.notification-title.notification-info {
color: var(--okay)
.modal-row{display:flex;justify-content:space-between;gap:1rem;padding:.5rem 0;border-bottom:1px solid var(--border);font-size:.82rem}
.modal-row:last-child{border-bottom:none}
.notification-title{
display:flex;
align-items:center;
gap:.6rem;
padding:.6rem .5rem;
border-radius:10px;
margin-bottom:.4rem;
}
.notification-title.notification-warning {
color: var(--warn)
.notification-title:last-child{margin-bottom:0}
.notification-title i{font-size:.85rem;flex-shrink:0}
.notification-title span{
flex:1;
font-size:.8rem;
}
.notification-title.notification-error {
color: var(--critical)
.notification-info{background:var(--primary-bg);color:var(--primary)}
.notification-warning{background:var(--warn-bg);color:var(--warn)}
.notification-error{background:var(--critical-bg);color:var(--critical)}
.notification-read-button{
background:none;border:none;cursor:pointer;
color:var(--dim);font-size:.75rem;
padding:.3rem;border-radius:6px;
flex-shrink:0;
transition:background .12s ease,color .12s ease;
}
.notification-read-button:hover{background:var(--panel-2);color:var(--text)}
+50 -11
View File
@@ -3,6 +3,7 @@ const content = document.getElementById('content');
var active_tab = null;
const last_refreshes = new Map();
let notifications = {};
async function load_modules() {
const res = await fetch('/modules');
@@ -53,6 +54,29 @@ async function select_module(name, btn) {
updateLastSyncTooltip();
}
function formatTime(iso) {
const d = new Date(iso);
return d.toLocaleString(undefined, {
month: 'short', day: 'numeric',
hour: '2-digit', minute: '2-digit'
});
}
async function deleteNotification(nid) {
const res = await fetch(`/notifications/delete/${nid}`, {method: 'DELETE'});
let text = res.ok ? "notification deleted" : "failed to delete";
const el = document.getElementById(`notification-text-${nid}`);
if (!el) return; // cant delete whats not there
el.innerHTML = text;
await new Promise(r => setTimeout(r, 2000));
const noti = document.getElementById(`notification-${nid}`);
if (noti) {
noti.remove();
}
}
function openNotificationModal() {
let html = `
<div class="modal-kicker">NOTIFICATIONS</div>
@@ -60,28 +84,43 @@ function openNotificationModal() {
<span class="modal-title">Whats new:</span>
<div class="scroll">
`
for (let i = 0; i < 50; i++) {
let status = Math.floor(Math.random() * 3)
html += `<p class="notification-title `
switch (status) {
case 0:
Object.entries(notifications).forEach(([nid, n]) => {
html += `<p id="notification-${nid}" class="notification-title `
switch (n.status) {
case "info":
html += `notification-info"><i class="fa-solid fa-fw fa-info`
break;
case 1:
case "warning":
html += `notification-warning"><i class="fa-solid fa-fw fa-triangle-exclamation`
break;
case 2:
case "error":
html += `notification-error"><i class="fa-solid fa-fw fa-circle-xmark`
break;
}
html += `"></i><span> Something happened</span>`
}
html += `"></i>`
html += `<span id="notification-text-${nid}">${formatTime(n.time)} ${n.msg}</span>`
html += `<button class="notification-read-button" onclick="deleteNotification('${nid}')"><i class="fa-solid fa-fw fa-xmark"></i></button></p>`
});
html += `</div>`
AW.modal.open(html);
}
async function notificationsPollLoop() {
let pollDelay_s = 10;
while (true) {
const res = await fetch("/notifications");
if (!res.ok) {
console.error("Failed to fetch notifications");
}
else {
notifications = await res.json()
}
await new Promise(r => setTimeout(r, 1000 * pollDelay_s));
}
}
function updateLastSyncTooltip() {
const t = last_refreshes[active_tab];
@@ -120,4 +159,4 @@ document.getElementById("nav-button-notifications").onclick = () => {
};
load_modules();
notificationsPollLoop();
+5 -1
View File
@@ -41,7 +41,11 @@ class SocialWatchModule(BaseModule):
status = self.addrbook.update()
NOTIFICATION_MANAGER.notify(self.name, Notification(status=Notification.Status.INFO, msg="Updated!"));
NOTIFICATION_MANAGER.notify(Notification(
module=self.name,
status=Notification.Status.INFO,
msg=f"Synced module {self.name}. Contacts: {len(self.addrbook.people)}, Events: {len(self.addrbook.events)}."
));
return jsonify(status), 200