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:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user