feat: add notifications system with bell badge, polling, Directus flows, and webhook integration

This commit is contained in:
2026-02-07 15:13:17 +01:00
parent f6ba0085f9
commit 10dd923739
8 changed files with 48 additions and 12 deletions

View File

@@ -64,7 +64,8 @@ js/
│ ├── currency.js # XMR/Fiat Umrechnung │ ├── currency.js # XMR/Fiat Umrechnung
│ ├── pow-captcha.js # Proof-of-Work Captcha (Challenge/Verify) │ ├── pow-captcha.js # Proof-of-Work Captcha (Challenge/Verify)
│ ├── btcpay.js # BTCPay Server Integration (Invoice, Checkout, Webhook) │ ├── 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/ └── components/
├── app-shell.js # Layout, registriert Routes ├── app-shell.js # Layout, registriert Routes
├── app-header.js # Header (Theme-Toggle, Lang-Dropdown, Profil-Dropdown) ├── 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 8. ~~Favoriten Directus Sync~~ ✅ FavoritesService mit Union-Merge bei Login
9. ~~Expired Listings~~ ✅ Directus Flow (alle 15 Min), Status-Badges auf Cards 9. ~~Expired Listings~~ ✅ Directus Flow (alle 15 Min), Status-Badges auf Cards
10. Reputation-System (5/15/50 Deals Stufen) 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) ## Directus Berechtigungen (Public-Rolle)
@@ -163,12 +165,15 @@ locales/
| `conversations` | ✓ | ✓ | ✓ | Filter via `participant_hash`, Update nur `status` | | `conversations` | ✓ | ✓ | ✓ | Filter via `participant_hash`, Update nur `status` |
| `messages` | ✓ | ✓ | - | Filter via `conversation` ID | | `messages` | ✓ | ✓ | - | Filter via `conversation` ID |
| `favorites` | ✓ | ✓ | - | User-Rolle: Filter `user = $CURRENT_USER`, Delete erlaubt | | `favorites` | ✓ | ✓ | - | User-Rolle: Filter `user = $CURRENT_USER`, Delete erlaubt |
| `notifications` | ✓ | ✓ (via Flow/Webhook) | ✓ | User-Rolle: Filter `user_hash`, nur `read` updaten |
### Directus Flows ### Directus Flows
| Flow | Trigger | Aktion | | Flow | Trigger | Aktion |
|------|---------|--------| |------|---------|--------|
| Archive Expired Listings | Schedule `*/15 * * * *` | `status → archived` wenn `expires_at < NOW` | | 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. Siehe `docs/DIRECTUS-SCHEMA.md` für vollständiges Schema.

View File

@@ -155,7 +155,8 @@ dgray/
│ │ ├── currency.js # XMR/Fiat Umrechnung │ │ ├── currency.js # XMR/Fiat Umrechnung
│ │ ├── pow-captcha.js # PoW Captcha (Server-first, lokaler Fallback) │ │ ├── pow-captcha.js # PoW Captcha (Server-first, lokaler Fallback)
│ │ ├── btcpay.js # BTCPay Server Integration (Invoice, Checkout) │ │ ├── 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/ │ ├── vendor/
│ │ ├── nacl-fast.min.js # TweetNaCl (self-hosted) │ │ ├── nacl-fast.min.js # TweetNaCl (self-hosted)
│ │ ├── nacl-util.min.js # TweetNaCl Utils │ │ ├── nacl-util.min.js # TweetNaCl Utils
@@ -217,7 +218,8 @@ dgray/
- [x] Favoriten Directus-Sync (Union-Merge bei Login, localStorage-Fallback) - [x] Favoriten Directus-Sync (Union-Merge bei Login, localStorage-Fallback)
- [x] PoW Captcha (server-seitig via pow.dgray.io, HMAC-signiert) - [x] PoW Captcha (server-seitig via pow.dgray.io, HMAC-signiert)
- [x] TweetNaCl self-hosted (kein CDN) - [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 ### Phase 4: Payments
- [x] XMR-Kursabfrage API (CoinGecko) - [x] XMR-Kursabfrage API (CoinGecko)

View File

@@ -109,10 +109,23 @@ if ($directusStatus >= 400) {
exit; exit;
} }
// Create notification for listing owner // Fetch listing to get owner
$listingData = json_decode($directusResponse, true); $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; $userCreated = $listingData['data']['user_created'] ?? null;
// Create notification for listing owner
if ($userCreated) { if ($userCreated) {
$notifPayload = json_encode([ $notifPayload = json_encode([
'user_hash' => $userCreated, 'user_hash' => $userCreated,

View File

@@ -20,6 +20,14 @@ async function initApp() {
favoritesService.init() favoritesService.init()
notificationsService.init() notificationsService.init()
auth.subscribe((loggedIn) => {
if (loggedIn) {
notificationsService.init()
} else {
notificationsService.destroy()
}
})
await import('./components/app-shell.js') await import('./components/app-shell.js')
document.getElementById('app').innerHTML = '<app-shell></app-shell>' document.getElementById('app').innerHTML = '<app-shell></app-shell>'

View File

@@ -364,7 +364,7 @@ style.textContent = /* css */`
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
padding: var(--space-xl); padding: var(--space-xl);
width: 100%; width: 100%;
max-width: 425px; max-width: 480px;
position: relative; position: relative;
box-shadow: var(--shadow-xl); box-shadow: var(--shadow-xl);
} }

View File

@@ -246,8 +246,8 @@ style.textContent = /* css */`
} }
listing-card .payment-published { listing-card .payment-published {
background: rgba(40, 167, 69, 0.9); background: var(--color-accent);
color: #fff; color: var(--color-accent-text, #fff);
} }
listing-card .payment-expired { listing-card .payment-expired {

View File

@@ -1,3 +1,5 @@
import { i18n } from '../i18n.js'
const POW_SERVER = 'https://pow.dgray.io' const POW_SERVER = 'https://pow.dgray.io'
let modalScriptLoaded = false let modalScriptLoaded = false
@@ -80,7 +82,8 @@ export async function openCheckout(invoiceId) {
resolve(lastStatus || 'unknown') resolve(lastStatus || 'unknown')
}) })
window.btcpay.showInvoice(invoiceId) const lang = i18n.getLocale() || 'en'
window.btcpay.showInvoice(invoiceId, { lang })
}) })
} }

View File

@@ -114,9 +114,10 @@ class DirectusService {
document.addEventListener('visibilitychange', () => { document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible' && this.refreshToken) { if (document.visibilityState === 'visible' && this.refreshToken) {
const timeLeft = this.tokenExpiry - Date.now() const timeLeft = this.tokenExpiry - Date.now()
if (timeLeft < 60000) { if (timeLeft < 120000) {
this.refreshSession() this.refreshSession()
} }
this.scheduleTokenRefresh()
} }
}) })
} }
@@ -280,7 +281,7 @@ class DirectusService {
this.clearTokens() this.clearTokens()
} }
async refreshSession() { async refreshSession(_retryCount = 0) {
if (!this.refreshToken) return false if (!this.refreshToken) return false
try { try {
@@ -298,6 +299,10 @@ class DirectusService {
return true return true
} }
} catch (e) { } catch (e) {
if (_retryCount < 2) {
await new Promise(r => setTimeout(r, 2000))
return this.refreshSession(_retryCount + 1)
}
this.clearTokens() this.clearTokens()
} }