From 10dd92373905931232ec12a36bc789eda8a91a5b Mon Sep 17 00:00:00 2001 From: Alexander Schmidt Date: Sat, 7 Feb 2026 15:13:17 +0100 Subject: [PATCH] feat: add notifications system with bell badge, polling, Directus flows, and webhook integration --- AGENTS.md | 9 +++++++-- README.md | 6 ++++-- docs/pow-server/btcpay-webhook.php | 17 +++++++++++++++-- js/app.js | 8 ++++++++ js/components/auth-modal.js | 2 +- js/components/listing-card.js | 4 ++-- js/services/btcpay.js | 5 ++++- js/services/directus.js | 9 +++++++-- 8 files changed, 48 insertions(+), 12 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 803716b..de0dae8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,7 +64,8 @@ js/ │ ├── currency.js # XMR/Fiat Umrechnung │ ├── pow-captcha.js # Proof-of-Work Captcha (Challenge/Verify) │ ├── btcpay.js # BTCPay Server Integration (Invoice, Checkout, Webhook) -│ └── favorites.js # Favoriten-Service (localStorage + Directus Sync) +│ ├── favorites.js # Favoriten-Service (localStorage + Directus Sync) +│ └── notifications.js # Benachrichtigungen (Polling, Badge) └── components/ ├── app-shell.js # Layout, registriert Routes ├── app-header.js # Header (Theme-Toggle, Lang-Dropdown, Profil-Dropdown) @@ -147,7 +148,8 @@ locales/ 8. ~~Favoriten Directus Sync~~ ✅ FavoritesService mit Union-Merge bei Login 9. ~~Expired Listings~~ ✅ Directus Flow (alle 15 Min), Status-Badges auf Cards 10. Reputation-System (5/15/50 Deals Stufen) -11. Push-Benachrichtigungen für neue Nachrichten +11. ~~In-App Benachrichtigungen~~ ✅ NotificationsService mit Polling, Glocke-Icon mit Badge +12. Push-Benachrichtigungen (Web Push API) ## Directus Berechtigungen (Public-Rolle) @@ -163,12 +165,15 @@ locales/ | `conversations` | ✓ | ✓ | ✓ | Filter via `participant_hash`, Update nur `status` | | `messages` | ✓ | ✓ | - | Filter via `conversation` ID | | `favorites` | ✓ | ✓ | - | User-Rolle: Filter `user = $CURRENT_USER`, Delete erlaubt | +| `notifications` | ✓ | ✓ (via Flow/Webhook) | ✓ | User-Rolle: Filter `user_hash`, nur `read` updaten | ### Directus Flows | Flow | Trigger | Aktion | |------|---------|--------| | Archive Expired Listings | Schedule `*/15 * * * *` | `status → archived` wenn `expires_at < NOW` | +| Notify: Listing Published | Webhook (btcpay-webhook.php) | Creates notification when listing is published after payment | +| Notify: New Message | Event Hook `items.create` on `messages` | Creates notification for message recipient | Siehe `docs/DIRECTUS-SCHEMA.md` für vollständiges Schema. diff --git a/README.md b/README.md index cafa512..2019f97 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,8 @@ dgray/ │ │ ├── currency.js # XMR/Fiat Umrechnung │ │ ├── pow-captcha.js # PoW Captcha (Server-first, lokaler Fallback) │ │ ├── btcpay.js # BTCPay Server Integration (Invoice, Checkout) -│ │ └── favorites.js # Favoriten (localStorage + Directus Sync) +│ │ ├── favorites.js # Favoriten (localStorage + Directus Sync) +│ │ └── notifications.js# Benachrichtigungen (Polling, Badge) │ ├── vendor/ │ │ ├── nacl-fast.min.js # TweetNaCl (self-hosted) │ │ ├── nacl-util.min.js # TweetNaCl Utils @@ -217,7 +218,8 @@ dgray/ - [x] Favoriten Directus-Sync (Union-Merge bei Login, localStorage-Fallback) - [x] PoW Captcha (server-seitig via pow.dgray.io, HMAC-signiert) - [x] TweetNaCl self-hosted (kein CDN) -- [ ] Benachrichtigungen (Push) +- [x] In-App Benachrichtigungen (Notifications-Service, Glocke mit Badge) +- [ ] Push-Benachrichtigungen (Web Push API) ### Phase 4: Payments - [x] XMR-Kursabfrage API (CoinGecko) diff --git a/docs/pow-server/btcpay-webhook.php b/docs/pow-server/btcpay-webhook.php index a1b9f28..c9c63a7 100644 --- a/docs/pow-server/btcpay-webhook.php +++ b/docs/pow-server/btcpay-webhook.php @@ -109,10 +109,23 @@ if ($directusStatus >= 400) { exit; } -// Create notification for listing owner -$listingData = json_decode($directusResponse, true); +// Fetch listing to get owner +$getUrl = DIRECTUS_URL . '/items/listings/' . urlencode($listingId) . '?fields=user_created'; +$gch = curl_init($getUrl); +curl_setopt_array($gch, [ + CURLOPT_HTTPHEADER => [ + 'Authorization: Bearer ' . DIRECTUS_TOKEN, + ], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 5, +]); +$getResponse = curl_exec($gch); +curl_close($gch); + +$listingData = json_decode($getResponse, true); $userCreated = $listingData['data']['user_created'] ?? null; +// Create notification for listing owner if ($userCreated) { $notifPayload = json_encode([ 'user_hash' => $userCreated, diff --git a/js/app.js b/js/app.js index 5634a2b..27a0e92 100644 --- a/js/app.js +++ b/js/app.js @@ -20,6 +20,14 @@ async function initApp() { favoritesService.init() notificationsService.init() + auth.subscribe((loggedIn) => { + if (loggedIn) { + notificationsService.init() + } else { + notificationsService.destroy() + } + }) + await import('./components/app-shell.js') document.getElementById('app').innerHTML = '' diff --git a/js/components/auth-modal.js b/js/components/auth-modal.js index 2d83e2c..1cad60c 100644 --- a/js/components/auth-modal.js +++ b/js/components/auth-modal.js @@ -364,7 +364,7 @@ style.textContent = /* css */` border-radius: var(--radius-lg); padding: var(--space-xl); width: 100%; - max-width: 425px; + max-width: 480px; position: relative; box-shadow: var(--shadow-xl); } diff --git a/js/components/listing-card.js b/js/components/listing-card.js index 59fe596..3d593e2 100644 --- a/js/components/listing-card.js +++ b/js/components/listing-card.js @@ -246,8 +246,8 @@ style.textContent = /* css */` } listing-card .payment-published { - background: rgba(40, 167, 69, 0.9); - color: #fff; + background: var(--color-accent); + color: var(--color-accent-text, #fff); } listing-card .payment-expired { diff --git a/js/services/btcpay.js b/js/services/btcpay.js index 61fb3aa..28e3fdc 100644 --- a/js/services/btcpay.js +++ b/js/services/btcpay.js @@ -1,3 +1,5 @@ +import { i18n } from '../i18n.js' + const POW_SERVER = 'https://pow.dgray.io' let modalScriptLoaded = false @@ -80,7 +82,8 @@ export async function openCheckout(invoiceId) { resolve(lastStatus || 'unknown') }) - window.btcpay.showInvoice(invoiceId) + const lang = i18n.getLocale() || 'en' + window.btcpay.showInvoice(invoiceId, { lang }) }) } diff --git a/js/services/directus.js b/js/services/directus.js index e1258b9..cf357bd 100644 --- a/js/services/directus.js +++ b/js/services/directus.js @@ -114,9 +114,10 @@ class DirectusService { document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible' && this.refreshToken) { const timeLeft = this.tokenExpiry - Date.now() - if (timeLeft < 60000) { + if (timeLeft < 120000) { this.refreshSession() } + this.scheduleTokenRefresh() } }) } @@ -280,7 +281,7 @@ class DirectusService { this.clearTokens() } - async refreshSession() { + async refreshSession(_retryCount = 0) { if (!this.refreshToken) return false try { @@ -298,6 +299,10 @@ class DirectusService { return true } } catch (e) { + if (_retryCount < 2) { + await new Promise(r => setTimeout(r, 2000)) + return this.refreshSession(_retryCount + 1) + } this.clearTokens() }