feat: add notifications system with bell badge, polling, Directus flows, and webhook integration
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>'
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user