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
│ ├── 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.

View File

@@ -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)

View File

@@ -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,

View File

@@ -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 = '<app-shell></app-shell>'

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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 })
})
}

View File

@@ -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()
}