Ekimart078 commited on
Commit
f0442cf
·
1 Parent(s): 0b891c3

Add professional dashboard template with WebSocket integration and dark mode toggle functionality.

Browse files
Files changed (3) hide show
  1. Dockerfile +0 -2
  2. main.py +19 -664
  3. templates/dashboard.html +1149 -0
Dockerfile CHANGED
@@ -15,8 +15,6 @@ WORKDIR /app
15
 
16
  COPY . .
17
 
18
- RUN ls -al /app && cat /app/requirements.txt
19
-
20
  RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
21
  pip install --no-cache-dir -r requirements.txt
22
 
 
15
 
16
  COPY . .
17
 
 
 
18
  RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
19
  pip install --no-cache-dir -r requirements.txt
20
 
main.py CHANGED
@@ -1,9 +1,19 @@
1
  from contextlib import asynccontextmanager
 
 
2
  from uuid import uuid4
3
 
4
  import uvicorn
5
- from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, Depends
 
 
 
 
 
 
 
6
  from fastapi.responses import HTMLResponse
 
7
 
8
  from core.manager.websocket_manager import WebSocketManager
9
  from core.settings.settings import Settings
@@ -17,6 +27,9 @@ from core.types.datamodels import (
17
  )
18
  from core.utils.utils import current_utc_iso
19
 
 
 
 
20
  settings = Settings()
21
  manager = WebSocketManager(settings=settings)
22
 
@@ -92,670 +105,12 @@ async def websocket_endpoint(websocket: WebSocket, user_id: str):
92
 
93
  # Endpoints REST
94
  @app.get("/", response_class=HTMLResponse)
95
- async def get_test_page():
96
  """Page de test pour les connexions WebSocket"""
97
- return """
98
- <!DOCTYPE html>
99
- <html>
100
- <head>
101
- <title>WebSocket Notification Service - Interface</title>
102
- <style>
103
- * {
104
- margin: 0;
105
- padding: 0;
106
- box-sizing: border-box;
107
- }
108
-
109
- body {
110
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
111
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
112
- min-height: 100vh;
113
- padding: 20px;
114
- color: #333;
115
- }
116
-
117
- .container {
118
- max-width: 1200px;
119
- margin: 0 auto;
120
- background: white;
121
- border-radius: 15px;
122
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
123
- overflow: hidden;
124
- }
125
-
126
- .header {
127
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
128
- padding: 30px;
129
- text-align: center;
130
- color: white;
131
- }
132
-
133
- .header h1 {
134
- font-size: 2.5em;
135
- margin-bottom: 10px;
136
- font-weight: 300;
137
- }
138
-
139
- .header p {
140
- font-size: 1.1em;
141
- opacity: 0.9;
142
- }
143
-
144
- .main-content {
145
- padding: 30px;
146
- }
147
-
148
- .section {
149
- margin-bottom: 30px;
150
- padding: 25px;
151
- background: #f8f9fa;
152
- border-radius: 10px;
153
- border-left: 4px solid #4facfe;
154
- }
155
-
156
- .section h2 {
157
- color: #2c3e50;
158
- margin-bottom: 15px;
159
- font-size: 1.4em;
160
- }
161
-
162
- .connection-panel {
163
- display: grid;
164
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
165
- gap: 20px;
166
- margin-bottom: 20px;
167
- }
168
-
169
- .input-group {
170
- display: flex;
171
- flex-direction: column;
172
- }
173
-
174
- .input-group label {
175
- margin-bottom: 5px;
176
- font-weight: 600;
177
- color: #555;
178
- }
179
-
180
- .input-group input {
181
- padding: 12px;
182
- border: 2px solid #e0e0e0;
183
- border-radius: 8px;
184
- font-size: 14px;
185
- transition: border-color 0.3s;
186
- }
187
-
188
- .input-group input:focus {
189
- outline: none;
190
- border-color: #4facfe;
191
- }
192
-
193
- .btn-group {
194
- display: flex;
195
- gap: 10px;
196
- flex-wrap: wrap;
197
- }
198
-
199
- .btn {
200
- padding: 12px 24px;
201
- border: none;
202
- border-radius: 8px;
203
- font-size: 14px;
204
- font-weight: 600;
205
- cursor: pointer;
206
- transition: all 0.3s;
207
- text-transform: uppercase;
208
- letter-spacing: 0.5px;
209
- }
210
-
211
- .btn-primary {
212
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
213
- color: white;
214
- }
215
-
216
- .btn-primary:hover {
217
- transform: translateY(-2px);
218
- box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
219
- }
220
-
221
- .btn-secondary {
222
- background: #6c757d;
223
- color: white;
224
- }
225
-
226
- .btn-secondary:hover {
227
- background: #5a6268;
228
- transform: translateY(-2px);
229
- }
230
-
231
- .btn-danger {
232
- background: #dc3545;
233
- color: white;
234
- }
235
-
236
- .btn-danger:hover {
237
- background: #c82333;
238
- transform: translateY(-2px);
239
- }
240
-
241
- .btn-success {
242
- background: #28a745;
243
- color: white;
244
- }
245
-
246
- .btn-success:hover {
247
- background: #218838;
248
- transform: translateY(-2px);
249
- }
250
-
251
- .status-indicator {
252
- display: inline-block;
253
- width: 12px;
254
- height: 12px;
255
- border-radius: 50%;
256
- margin-right: 8px;
257
- }
258
-
259
- .status-connected {
260
- background: #28a745;
261
- box-shadow: 0 0 10px rgba(40, 167, 69, 0.5);
262
- }
263
-
264
- .status-disconnected {
265
- background: #dc3545;
266
- }
267
-
268
- .messages {
269
- height: 400px;
270
- overflow-y: auto;
271
- border: 2px solid #e0e0e0;
272
- border-radius: 10px;
273
- padding: 15px;
274
- background: white;
275
- font-family: 'Courier New', monospace;
276
- font-size: 13px;
277
- }
278
-
279
- .message {
280
- margin: 8px 0;
281
- padding: 10px;
282
- border-radius: 6px;
283
- border-left: 4px solid #ddd;
284
- }
285
-
286
- .message-notification {
287
- background: #e7f3ff;
288
- border-left-color: #4facfe;
289
- }
290
-
291
- .message-system {
292
- background: #f8f9fa;
293
- border-left-color: #6c757d;
294
- }
295
-
296
- .message-error {
297
- background: #f8d7da;
298
- border-left-color: #dc3545;
299
- }
300
-
301
- .message-success {
302
- background: #d4edda;
303
- border-left-color: #28a745;
304
- }
305
-
306
- .message-timestamp {
307
- font-size: 11px;
308
- color: #666;
309
- float: right;
310
- }
311
-
312
- .api-section {
313
- display: grid;
314
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
315
- gap: 20px;
316
- }
317
-
318
- .api-card {
319
- background: white;
320
- border-radius: 10px;
321
- padding: 20px;
322
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
323
- border: 1px solid #e0e0e0;
324
- }
325
-
326
- .api-card h3 {
327
- color: #2c3e50;
328
- margin-bottom: 15px;
329
- font-size: 1.2em;
330
- }
331
-
332
- .api-card textarea {
333
- width: 100%;
334
- height: 100px;
335
- padding: 10px;
336
- border: 1px solid #e0e0e0;
337
- border-radius: 6px;
338
- font-family: 'Courier New', monospace;
339
- font-size: 12px;
340
- resize: vertical;
341
- }
342
-
343
- .stats-grid {
344
- display: grid;
345
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
346
- gap: 20px;
347
- }
348
-
349
- .stat-card {
350
- background: white;
351
- padding: 20px;
352
- border-radius: 10px;
353
- text-align: center;
354
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
355
- }
356
-
357
- .stat-number {
358
- font-size: 2em;
359
- font-weight: bold;
360
- color: #4facfe;
361
- margin-bottom: 5px;
362
- }
363
-
364
- .stat-label {
365
- color: #666;
366
- font-size: 0.9em;
367
- }
368
-
369
- .json-display {
370
- background: #f8f9fa;
371
- padding: 15px;
372
- border-radius: 6px;
373
- border: 1px solid #e0e0e0;
374
- font-family: 'Courier New', monospace;
375
- font-size: 12px;
376
- white-space: pre-wrap;
377
- max-height: 300px;
378
- overflow-y: auto;
379
- }
380
-
381
- @media (max-width: 768px) {
382
- .connection-panel {
383
- grid-template-columns: 1fr;
384
- }
385
-
386
- .btn-group {
387
- flex-direction: column;
388
- }
389
-
390
- .api-section {
391
- grid-template-columns: 1fr;
392
- }
393
- }
394
- </style>
395
- </head>
396
- <body>
397
- <div class="container">
398
- <div class="header">
399
- <h1>WebSocket Notification Service</h1>
400
- <p>Interface de test pour les notifications en temps réel</p>
401
- </div>
402
-
403
- <div class="main-content">
404
- <!-- Section Connexion -->
405
- <div class="section">
406
- <h2>🔌 Connexion WebSocket</h2>
407
- <div class="connection-panel">
408
- <div class="input-group">
409
- <label for="userId">User ID</label>
410
- <input type="text" id="userId" placeholder="user123" value="user123">
411
- </div>
412
- <div class="input-group">
413
- <label for="roles">Rôles (séparés par des virgules)</label>
414
- <input type="text" id="roles" placeholder="admin,user" value="admin,user">
415
- </div>
416
- </div>
417
- <div class="btn-group">
418
- <button class="btn btn-primary" onclick="connect()">
419
- <span class="status-indicator" id="statusIndicator"></span>
420
- Se connecter
421
- </button>
422
- <button class="btn btn-secondary" onclick="disconnect()">Déconnecter</button>
423
- <button class="btn btn-success" onclick="getStats()">Statistiques</button>
424
- <button class="btn btn-success" onclick="getClients()">Clients connectés</button>
425
- </div>
426
- </div>
427
-
428
- <!-- Section Messages -->
429
- <div class="section">
430
- <h2>💬 Messages</h2>
431
- <div id="messages" class="messages"></div>
432
- <div style="margin-top: 15px; display: flex; gap: 10px;">
433
- <input type="text" id="messageInput" placeholder="Tapez votre message..." style="flex: 1; padding: 10px; border: 1px solid #e0e0e0; border-radius: 6px;">
434
- <button class="btn btn-primary" onclick="sendMessage()">Envoyer</button>
435
- <button class="btn btn-secondary" onclick="clearMessages()">Effacer</button>
436
- </div>
437
- </div>
438
-
439
- <!-- Section API REST -->
440
- <div class="section">
441
- <h2>🚀 Tests API REST</h2>
442
- <div class="api-section">
443
- <div class="api-card">
444
- <h3>Notification utilisateur</h3>
445
- <div class="input-group">
446
- <label>User ID cible</label>
447
- <input type="text" id="targetUserId" placeholder="user123" value="user123">
448
- </div>
449
- <div class="input-group">
450
- <label>Message</label>
451
- <textarea id="userNotificationMessage" placeholder='{"type": "info", "title": "Test", "content": "Message de test"}'></textarea>
452
- </div>
453
- <button class="btn btn-primary" onclick="sendUserNotification()">Envoyer</button>
454
- </div>
455
-
456
- <div class="api-card">
457
- <h3>Notification par rôle</h3>
458
- <div class="input-group">
459
- <label>Rôle cible</label>
460
- <input type="text" id="targetRole" placeholder="admin" value="admin">
461
- </div>
462
- <div class="input-group">
463
- <label>Message</label>
464
- <textarea id="roleNotificationMessage" placeholder='{"type": "warning", "title": "Alerte", "content": "Message pour les admins"}'></textarea>
465
- </div>
466
- <button class="btn btn-primary" onclick="sendRoleNotification()">Envoyer</button>
467
- </div>
468
-
469
- <div class="api-card">
470
- <h3>Diffusion globale</h3>
471
- <div class="input-group">
472
- <label>Message</label>
473
- <textarea id="broadcastMessage" placeholder='{"type": "announcement", "title": "Annonce", "content": "Message global"}'></textarea>
474
- </div>
475
- <button class="btn btn-primary" onclick="sendBroadcast()">Diffuser</button>
476
- </div>
477
-
478
- <div class="api-card">
479
- <h3>Notification ciblée</h3>
480
- <div class="input-group">
481
- <label>Type de cible</label>
482
- <select id="targetType" style="width: 100%; padding: 8px; border: 1px solid #e0e0e0; border-radius: 6px;">
483
- <option value="user">Utilisateur</option>
484
- <option value="role">Rôle</option>
485
- <option value="global">Global</option>
486
- </select>
487
- </div>
488
- <div class="input-group">
489
- <label>Valeur cible</label>
490
- <input type="text" id="targetValue" placeholder="user123 ou admin" value="user123">
491
- </div>
492
- <div class="input-group">
493
- <label>Message</label>
494
- <textarea id="targetedMessage" placeholder='{"type": "targeted", "title": "Ciblé", "content": "Message ciblé"}'></textarea>
495
- </div>
496
- <button class="btn btn-primary" onclick="sendTargetedNotification()">Envoyer</button>
497
- </div>
498
- </div>
499
- </div>
500
-
501
- <!-- Section Statistiques -->
502
- <div class="section">
503
- <h2>📊 Statistiques du service</h2>
504
- <div class="stats-grid" id="statsGrid">
505
- <div class="stat-card">
506
- <div class="stat-number" id="connectedClients">0</div>
507
- <div class="stat-label">Clients connectés</div>
508
- </div>
509
- <div class="stat-card">
510
- <div class="stat-number" id="totalNotifications">0</div>
511
- <div class="stat-label">Notifications envoyées</div>
512
- </div>
513
- <div class="stat-card">
514
- <div class="stat-number" id="activeSubscriptions">0</div>
515
- <div class="stat-label">Abonnements actifs</div>
516
- </div>
517
- </div>
518
- <div style="margin-top: 20px;">
519
- <h3>Détails des clients connectés</h3>
520
- <div id="clientDetails" class="json-display">Cliquez sur "Clients connectés" pour voir les détails</div>
521
- </div>
522
- </div>
523
- </div>
524
- </div>
525
-
526
- <script>
527
- let ws = null;
528
- let sessionId = null;
529
- const baseUrl = window.location.origin;
530
-
531
- // Initialisation des messages par défaut
532
- document.getElementById('userNotificationMessage').value = JSON.stringify({
533
- type: "info",
534
- title: "Test utilisateur",
535
- content: "Ceci est un message de test pour un utilisateur spécifique"
536
- }, null, 2);
537
-
538
- document.getElementById('roleNotificationMessage').value = JSON.stringify({
539
- type: "warning",
540
- title: "Alerte admin",
541
- content: "Message important pour les administrateurs"
542
- }, null, 2);
543
-
544
- document.getElementById('broadcastMessage').value = JSON.stringify({
545
- type: "announcement",
546
- title: "Annonce générale",
547
- content: "Message diffusé à tous les clients connectés"
548
- }, null, 2);
549
-
550
- document.getElementById('targetedMessage').value = JSON.stringify({
551
- type: "targeted",
552
- title: "Message ciblé",
553
- content: "Message envoyé via l'API de notification ciblée"
554
- }, null, 2);
555
-
556
- function connect() {
557
- const userId = document.getElementById('userId').value;
558
- const roles = document.getElementById('roles').value;
559
- const protocol = window.location.protocol === "https:" ? "wss" : "ws";
560
- const url = `${protocol}://${window.location.host}/ws/${userId}?roles=${roles}`;
561
-
562
- ws = new WebSocket(url);
563
-
564
- ws.onmessage = function(event) {
565
- const data = JSON.parse(event.data);
566
- addMessage(data, 'notification');
567
- };
568
-
569
- ws.onopen = function() {
570
- updateConnectionStatus(true);
571
- addMessage({message: 'Connexion WebSocket établie', type: 'system'}, 'system');
572
- getStats(); // Rafraîchir les stats
573
- };
574
-
575
- ws.onclose = function() {
576
- updateConnectionStatus(false);
577
- addMessage({message: 'Connexion WebSocket fermée', type: 'system'}, 'system');
578
- };
579
-
580
- ws.onerror = function(error) {
581
- addMessage({message: 'Erreur WebSocket: ' + error, type: 'error'}, 'error');
582
- };
583
- }
584
-
585
- function disconnect() {
586
- if (ws) {
587
- ws.close();
588
- ws = null;
589
- sessionId = null;
590
- updateConnectionStatus(false);
591
- }
592
- }
593
-
594
- function updateConnectionStatus(connected) {
595
- const indicator = document.getElementById('statusIndicator');
596
- if (connected) {
597
- indicator.className = 'status-indicator status-connected';
598
- } else {
599
- indicator.className = 'status-indicator status-disconnected';
600
- }
601
- }
602
-
603
- function sendMessage() {
604
- if (ws && ws.readyState === WebSocket.OPEN) {
605
- const message = document.getElementById('messageInput').value;
606
- if (message.trim()) {
607
- ws.send(JSON.stringify({message: message}));
608
- document.getElementById('messageInput').value = '';
609
- }
610
- } else {
611
- addMessage({message: 'Connexion WebSocket non établie', type: 'error'}, 'error');
612
- }
613
- }
614
-
615
- function addMessage(data, type) {
616
- const messages = document.getElementById('messages');
617
- const messageDiv = document.createElement('div');
618
- messageDiv.className = `message message-${type}`;
619
-
620
- const timestamp = new Date().toLocaleTimeString();
621
- messageDiv.innerHTML = `
622
- <span class="message-timestamp">${timestamp}</span>
623
- <strong>${data.type || 'message'}:</strong> ${data.message || JSON.stringify(data, null, 2)}
624
- `;
625
-
626
- messages.appendChild(messageDiv);
627
- messages.scrollTop = messages.scrollHeight;
628
- }
629
-
630
- function clearMessages() {
631
- document.getElementById('messages').innerHTML = '';
632
- }
633
-
634
- // Fonctions API REST
635
- async function sendUserNotification() {
636
- const userId = document.getElementById('targetUserId').value;
637
- const message = document.getElementById('userNotificationMessage').value;
638
-
639
- try {
640
- const response = await fetch(`${baseUrl}/notifications/user/${userId}`, {
641
- method: 'POST',
642
- headers: {'Content-Type': 'application/json'},
643
- body: message
644
- });
645
-
646
- const result = await response.json();
647
- addMessage({message: `Notification utilisateur envoyée: ${JSON.stringify(result)}`}, 'success');
648
- } catch (error) {
649
- addMessage({message: `Erreur: ${error.message}`}, 'error');
650
- }
651
- }
652
-
653
- async function sendRoleNotification() {
654
- const role = document.getElementById('targetRole').value;
655
- const message = document.getElementById('roleNotificationMessage').value;
656
-
657
- try {
658
- const response = await fetch(`${baseUrl}/notifications/role/${role}`, {
659
- method: 'POST',
660
- headers: {'Content-Type': 'application/json'},
661
- body: message
662
- });
663
-
664
- const result = await response.json();
665
- addMessage({message: `Notification rôle envoyée: ${JSON.stringify(result)}`}, 'success');
666
- } catch (error) {
667
- addMessage({message: `Erreur: ${error.message}`}, 'error');
668
- }
669
- }
670
-
671
- async function sendBroadcast() {
672
- const message = document.getElementById('broadcastMessage').value;
673
-
674
- try {
675
- const response = await fetch(`${baseUrl}/notifications/broadcast`, {
676
- method: 'POST',
677
- headers: {'Content-Type': 'application/json'},
678
- body: message
679
- });
680
-
681
- const result = await response.json();
682
- addMessage({message: `Diffusion envoyée: ${JSON.stringify(result)}`}, 'success');
683
- } catch (error) {
684
- addMessage({message: `Erreur: ${error.message}`}, 'error');
685
- }
686
- }
687
-
688
- async function sendTargetedNotification() {
689
- const targetType = document.getElementById('targetType').value;
690
- const targetValue = document.getElementById('targetValue').value;
691
- const message = JSON.parse(document.getElementById('targetedMessage').value);
692
-
693
- const payload = {
694
- target_type: targetType,
695
- target_value: targetValue,
696
- notification: message
697
- };
698
-
699
- try {
700
- const response = await fetch(`${baseUrl}/notifications/send`, {
701
- method: 'POST',
702
- headers: {'Content-Type': 'application/json'},
703
- body: JSON.stringify(payload)
704
- });
705
-
706
- const result = await response.json();
707
- addMessage({message: `Notification ciblée envoyée: ${JSON.stringify(result)}`}, 'success');
708
- } catch (error) {
709
- addMessage({message: `Erreur: ${error.message}`}, 'error');
710
- }
711
- }
712
-
713
- async function getStats() {
714
- try {
715
- const response = await fetch(`${baseUrl}/stats`);
716
- const stats = await response.json();
717
-
718
- document.getElementById('connectedClients').textContent = stats.connected_clients || 0;
719
- document.getElementById('totalNotifications').textContent = stats.total_notifications || 0;
720
- document.getElementById('activeSubscriptions').textContent = stats.active_subscriptions || 0;
721
-
722
- addMessage({message: `Statistiques mises à jour: ${JSON.stringify(stats)}`}, 'system');
723
- } catch (error) {
724
- addMessage({message: `Erreur récupération stats: ${error.message}`}, 'error');
725
- }
726
- }
727
-
728
- async function getClients() {
729
- try {
730
- const response = await fetch(`${baseUrl}/clients`);
731
- const data = await response.json();
732
-
733
- document.getElementById('clientDetails').textContent = JSON.stringify(data, null, 2);
734
- addMessage({message: `${data.clients.length} clients connectés`}, 'system');
735
- } catch (error) {
736
- addMessage({message: `Erreur récupération clients: ${error.message}`}, 'error');
737
- }
738
- }
739
-
740
- // Gestion des touches Enter
741
- document.getElementById('messageInput').addEventListener('keypress', function(e) {
742
- if (e.key === 'Enter') {
743
- sendMessage();
744
- }
745
- });
746
-
747
- // Connexion automatique au chargement de la page
748
- window.onload = function() {
749
- connect();
750
- getStats();
751
- };
752
-
753
- // Actualisation automatique des stats toutes les 30 secondes
754
- setInterval(getStats, 30000);
755
- </script>
756
- </body>
757
- </html>
758
- """
759
 
760
 
761
  @app.post("/subscriptions/{session_id}")
 
1
  from contextlib import asynccontextmanager
2
+ from datetime import datetime
3
+ from pathlib import Path
4
  from uuid import uuid4
5
 
6
  import uvicorn
7
+ from fastapi import (
8
+ FastAPI,
9
+ WebSocket,
10
+ Request,
11
+ WebSocketDisconnect,
12
+ HTTPException,
13
+ Depends,
14
+ )
15
  from fastapi.responses import HTMLResponse
16
+ from fastapi.templating import Jinja2Templates
17
 
18
  from core.manager.websocket_manager import WebSocketManager
19
  from core.settings.settings import Settings
 
27
  )
28
  from core.utils.utils import current_utc_iso
29
 
30
+ BASE_DIR = Path(__file__).resolve().parent.absolute()
31
+ templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
32
+
33
  settings = Settings()
34
  manager = WebSocketManager(settings=settings)
35
 
 
105
 
106
  # Endpoints REST
107
  @app.get("/", response_class=HTMLResponse)
108
+ async def root(request: Request):
109
  """Page de test pour les connexions WebSocket"""
110
+ return templates.TemplateResponse(
111
+ "dashboard.html",
112
+ {"request": request, "current_time": datetime.now()},
113
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
 
116
  @app.post("/subscriptions/{session_id}")
templates/dashboard.html ADDED
@@ -0,0 +1,1149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>NotificationHub | Dashboard Professionnel</title>
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
8
+ <style>
9
+ :root {
10
+ --primary: #5e35b1;
11
+ --primary-dark: #4527a0;
12
+ --secondary: #26a69a;
13
+ --success: #43a047;
14
+ --warning: #ff9800;
15
+ --danger: #e53935;
16
+ --dark: #263238;
17
+ --light: #f5f7fa;
18
+ --gray: #90a4ae;
19
+ --card-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
20
+ --transition: all 0.3s ease;
21
+ }
22
+
23
+ * {
24
+ margin: 0;
25
+ padding: 0;
26
+ box-sizing: border-box;
27
+ font-family: 'Segoe UI', 'Roboto', sans-serif;
28
+ }
29
+
30
+ body {
31
+ background-color: #f0f4f8;
32
+ color: #333;
33
+ line-height: 1.6;
34
+ min-height: 100vh;
35
+ display: flex;
36
+ flex-direction: column;
37
+ }
38
+
39
+ /* Header Styles */
40
+ .header {
41
+ background: linear-gradient(135deg, var(--primary), var(--primary-dark));
42
+ color: white;
43
+ padding: 1.5rem;
44
+ display: flex;
45
+ justify-content: space-between;
46
+ align-items: center;
47
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
48
+ position: sticky;
49
+ top: 0;
50
+ z-index: 100;
51
+ }
52
+
53
+ .logo {
54
+ display: flex;
55
+ align-items: center;
56
+ gap: 0.8rem;
57
+ }
58
+
59
+ .logo i {
60
+ font-size: 1.8rem;
61
+ }
62
+
63
+ .logo h1 {
64
+ font-weight: 600;
65
+ font-size: 1.6rem;
66
+ }
67
+
68
+ .user-controls {
69
+ display: flex;
70
+ align-items: center;
71
+ gap: 1.5rem;
72
+ }
73
+
74
+ .theme-toggle {
75
+ background: rgba(255, 255, 255, 0.15);
76
+ border: none;
77
+ border-radius: 50%;
78
+ width: 40px;
79
+ height: 40px;
80
+ display: flex;
81
+ align-items: center;
82
+ justify-content: center;
83
+ color: white;
84
+ cursor: pointer;
85
+ transition: var(--transition);
86
+ }
87
+
88
+ .theme-toggle:hover {
89
+ background: rgba(255, 255, 255, 0.25);
90
+ }
91
+
92
+ .user-profile {
93
+ display: flex;
94
+ align-items: center;
95
+ gap: 0.8rem;
96
+ cursor: pointer;
97
+ padding: 0.4rem 0.8rem;
98
+ border-radius: 50px;
99
+ transition: var(--transition);
100
+ }
101
+
102
+ .user-profile:hover {
103
+ background: rgba(255, 255, 255, 0.15);
104
+ }
105
+
106
+ .avatar {
107
+ width: 36px;
108
+ height: 36px;
109
+ border-radius: 50%;
110
+ background: linear-gradient(135deg, var(--secondary), #00796b);
111
+ display: flex;
112
+ align-items: center;
113
+ justify-content: center;
114
+ font-weight: bold;
115
+ color: white;
116
+ }
117
+
118
+ /* Main Layout */
119
+ .main-container {
120
+ display: flex;
121
+ flex: 1;
122
+ }
123
+
124
+ /* Sidebar Styles */
125
+ .sidebar {
126
+ width: 260px;
127
+ background: white;
128
+ padding: 1.5rem 0;
129
+ box-shadow: var(--card-shadow);
130
+ transition: var(--transition);
131
+ z-index: 90;
132
+ }
133
+
134
+ .nav-section {
135
+ margin-bottom: 2rem;
136
+ }
137
+
138
+ .nav-title {
139
+ padding: 0.5rem 1.5rem;
140
+ font-size: 0.85rem;
141
+ font-weight: 600;
142
+ text-transform: uppercase;
143
+ color: var(--gray);
144
+ letter-spacing: 1px;
145
+ }
146
+
147
+ .nav-item {
148
+ display: flex;
149
+ align-items: center;
150
+ gap: 0.8rem;
151
+ padding: 0.9rem 1.5rem;
152
+ color: var(--dark);
153
+ text-decoration: none;
154
+ transition: var(--transition);
155
+ position: relative;
156
+ font-weight: 500;
157
+ }
158
+
159
+ .nav-item:hover, .nav-item.active {
160
+ background: rgba(94, 53, 177, 0.08);
161
+ color: var(--primary);
162
+ }
163
+
164
+ .nav-item.active:before {
165
+ content: '';
166
+ position: absolute;
167
+ left: 0;
168
+ top: 0;
169
+ height: 100%;
170
+ width: 4px;
171
+ background: var(--primary);
172
+ border-radius: 0 4px 4px 0;
173
+ }
174
+
175
+ .nav-item i {
176
+ font-size: 1.1rem;
177
+ width: 24px;
178
+ }
179
+
180
+ /* Main Content */
181
+ .content {
182
+ flex: 1;
183
+ padding: 2rem;
184
+ display: flex;
185
+ flex-direction: column;
186
+ gap: 2rem;
187
+ }
188
+
189
+ .dashboard-grid {
190
+ display: grid;
191
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
192
+ gap: 1.5rem;
193
+ }
194
+
195
+ /* Card Styles */
196
+ .card {
197
+ background: white;
198
+ border-radius: 16px;
199
+ box-shadow: var(--card-shadow);
200
+ overflow: hidden;
201
+ transition: var(--transition);
202
+ }
203
+
204
+ .card:hover {
205
+ transform: translateY(-5px);
206
+ box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
207
+ }
208
+
209
+ .card-header {
210
+ padding: 1.5rem 1.5rem 1rem;
211
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
212
+ display: flex;
213
+ justify-content: space-between;
214
+ align-items: center;
215
+ }
216
+
217
+ .card-title {
218
+ font-size: 1.2rem;
219
+ font-weight: 600;
220
+ color: var(--dark);
221
+ display: flex;
222
+ align-items: center;
223
+ gap: 0.8rem;
224
+ }
225
+
226
+ .card-title i {
227
+ color: var(--primary);
228
+ }
229
+
230
+ .card-actions {
231
+ display: flex;
232
+ gap: 0.5rem;
233
+ }
234
+
235
+ .card-actions button {
236
+ background: none;
237
+ border: none;
238
+ color: var(--gray);
239
+ cursor: pointer;
240
+ padding: 0.4rem;
241
+ border-radius: 8px;
242
+ transition: var(--transition);
243
+ }
244
+
245
+ .card-actions button:hover {
246
+ background: rgba(0, 0, 0, 0.05);
247
+ color: var(--primary);
248
+ }
249
+
250
+ .card-body {
251
+ padding: 1.5rem;
252
+ }
253
+
254
+ /* Status Indicator */
255
+ .status-indicator {
256
+ display: inline-flex;
257
+ align-items: center;
258
+ gap: 0.5rem;
259
+ padding: 0.4rem 0.8rem;
260
+ border-radius: 50px;
261
+ font-size: 0.9rem;
262
+ font-weight: 500;
263
+ }
264
+
265
+ .status-dot {
266
+ width: 10px;
267
+ height: 10px;
268
+ border-radius: 50%;
269
+ }
270
+
271
+ .status-connected {
272
+ background: var(--success);
273
+ color: var(--success);
274
+ }
275
+
276
+ .status-disconnected {
277
+ background: var(--danger);
278
+ color: var(--danger);
279
+ }
280
+
281
+ /* Form Elements */
282
+ .form-group {
283
+ margin-bottom: 1.2rem;
284
+ }
285
+
286
+ .form-group label {
287
+ display: block;
288
+ margin-bottom: 0.5rem;
289
+ font-weight: 500;
290
+ color: var(--dark);
291
+ font-size: 0.9rem;
292
+ }
293
+
294
+ .form-control {
295
+ width: 100%;
296
+ padding: 0.8rem 1rem;
297
+ border: 1px solid #e0e0e0;
298
+ border-radius: 10px;
299
+ font-size: 1rem;
300
+ transition: var(--transition);
301
+ background: #fafafa;
302
+ }
303
+
304
+ .form-control:focus {
305
+ outline: none;
306
+ border-color: var(--primary);
307
+ box-shadow: 0 0 0 3px rgba(94, 53, 177, 0.15);
308
+ background: white;
309
+ }
310
+
311
+ .btn {
312
+ padding: 0.8rem 1.5rem;
313
+ border: none;
314
+ border-radius: 10px;
315
+ font-size: 0.95rem;
316
+ font-weight: 600;
317
+ cursor: pointer;
318
+ transition: var(--transition);
319
+ display: inline-flex;
320
+ align-items: center;
321
+ justify-content: center;
322
+ gap: 0.5rem;
323
+ }
324
+
325
+ .btn-primary {
326
+ background: linear-gradient(135deg, var(--primary), var(--primary-dark));
327
+ color: white;
328
+ }
329
+
330
+ .btn-primary:hover {
331
+ background: linear-gradient(135deg, var(--primary-dark), #311b92);
332
+ box-shadow: 0 5px 15px rgba(94, 53, 177, 0.3);
333
+ }
334
+
335
+ .btn-secondary {
336
+ background: #eceff1;
337
+ color: var(--dark);
338
+ }
339
+
340
+ .btn-secondary:hover {
341
+ background: #cfd8dc;
342
+ }
343
+
344
+ .btn-success {
345
+ background: linear-gradient(135deg, var(--success), #2e7d32);
346
+ color: white;
347
+ }
348
+
349
+ .btn-success:hover {
350
+ background: linear-gradient(135deg, #2e7d32, #1b5e20);
351
+ }
352
+
353
+ .btn-danger {
354
+ background: linear-gradient(135deg, var(--danger), #c62828);
355
+ color: white;
356
+ }
357
+
358
+ .btn-danger:hover {
359
+ background: linear-gradient(135deg, #c62828, #b71c1c);
360
+ }
361
+
362
+ .btn-group {
363
+ display: flex;
364
+ gap: 0.8rem;
365
+ flex-wrap: wrap;
366
+ }
367
+
368
+ /* Messages Panel */
369
+ .messages-panel {
370
+ display: flex;
371
+ flex-direction: column;
372
+ height: 100%;
373
+ }
374
+
375
+ .messages {
376
+ flex: 1;
377
+ overflow-y: auto;
378
+ border: 1px solid #e0e0e0;
379
+ border-radius: 10px;
380
+ padding: 1rem;
381
+ background: #fafafa;
382
+ margin-bottom: 1rem;
383
+ display: flex;
384
+ flex-direction: column;
385
+ gap: 0.8rem;
386
+ }
387
+
388
+ .message {
389
+ padding: 1rem;
390
+ border-radius: 10px;
391
+ border-left: 4px solid;
392
+ background: white;
393
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.03);
394
+ animation: fadeIn 0.3s ease;
395
+ }
396
+
397
+ @keyframes fadeIn {
398
+ from {
399
+ opacity: 0;
400
+ transform: translateY(10px);
401
+ }
402
+ to {
403
+ opacity: 1;
404
+ transform: translateY(0);
405
+ }
406
+ }
407
+
408
+ .message-header {
409
+ display: flex;
410
+ justify-content: space-between;
411
+ margin-bottom: 0.5rem;
412
+ font-size: 0.9rem;
413
+ }
414
+
415
+ .message-type {
416
+ font-weight: 600;
417
+ text-transform: capitalize;
418
+ }
419
+
420
+ .message-timestamp {
421
+ color: var(--gray);
422
+ font-size: 0.85rem;
423
+ }
424
+
425
+ .message-content {
426
+ line-height: 1.5;
427
+ }
428
+
429
+ .message-system {
430
+ border-left-color: var(--primary);
431
+ }
432
+
433
+ .message-notification {
434
+ border-left-color: var(--secondary);
435
+ }
436
+
437
+ .message-error {
438
+ border-left-color: var(--danger);
439
+ }
440
+
441
+ .message-success {
442
+ border-left-color: var(--success);
443
+ }
444
+
445
+ .message-input-group {
446
+ display: flex;
447
+ gap: 0.8rem;
448
+ }
449
+
450
+ .message-input {
451
+ flex: 1;
452
+ padding: 0.8rem 1rem;
453
+ border: 1px solid #e0e0e0;
454
+ border-radius: 10px;
455
+ font-size: 1rem;
456
+ }
457
+
458
+ /* Stats Cards */
459
+ .stats-grid {
460
+ display: grid;
461
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
462
+ gap: 1.5rem;
463
+ }
464
+
465
+ .stat-card {
466
+ display: flex;
467
+ flex-direction: column;
468
+ align-items: center;
469
+ padding: 1.5rem;
470
+ text-align: center;
471
+ }
472
+
473
+ .stat-icon {
474
+ width: 60px;
475
+ height: 60px;
476
+ border-radius: 50%;
477
+ display: flex;
478
+ align-items: center;
479
+ justify-content: center;
480
+ font-size: 1.8rem;
481
+ margin-bottom: 1rem;
482
+ }
483
+
484
+ .stat-value {
485
+ font-size: 2rem;
486
+ font-weight: 700;
487
+ margin-bottom: 0.3rem;
488
+ color: var(--dark);
489
+ }
490
+
491
+ .stat-label {
492
+ color: var(--gray);
493
+ font-size: 0.95rem;
494
+ }
495
+
496
+ /* Toast Notifications */
497
+ .toast-container {
498
+ position: fixed;
499
+ bottom: 2rem;
500
+ right: 2rem;
501
+ z-index: 1000;
502
+ display: flex;
503
+ flex-direction: column;
504
+ gap: 1rem;
505
+ }
506
+
507
+ .toast {
508
+ min-width: 300px;
509
+ padding: 1rem;
510
+ border-radius: 12px;
511
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
512
+ background: white;
513
+ display: flex;
514
+ align-items: flex-start;
515
+ gap: 1rem;
516
+ animation: slideIn 0.3s ease, fadeOut 0.5s ease 4.5s forwards;
517
+ }
518
+
519
+ @keyframes slideIn {
520
+ from {
521
+ transform: translateX(100%);
522
+ opacity: 0;
523
+ }
524
+ to {
525
+ transform: translateX(0);
526
+ opacity: 1;
527
+ }
528
+ }
529
+
530
+ @keyframes fadeOut {
531
+ to {
532
+ opacity: 0;
533
+ }
534
+ }
535
+
536
+ .toast-icon {
537
+ font-size: 1.5rem;
538
+ margin-top: 0.2rem;
539
+ }
540
+
541
+ .toast-success .toast-icon {
542
+ color: var(--success);
543
+ }
544
+
545
+ .toast-error .toast-icon {
546
+ color: var(--danger);
547
+ }
548
+
549
+ .toast-info .toast-icon {
550
+ color: var(--primary);
551
+ }
552
+
553
+ .toast-warning .toast-icon {
554
+ color: var(--warning);
555
+ }
556
+
557
+ .toast-content {
558
+ flex: 1;
559
+ }
560
+
561
+ .toast-title {
562
+ font-weight: 600;
563
+ margin-bottom: 0.3rem;
564
+ }
565
+
566
+ .toast-message {
567
+ font-size: 0.9rem;
568
+ color: #666;
569
+ }
570
+
571
+ .toast-close {
572
+ background: none;
573
+ border: none;
574
+ color: var(--gray);
575
+ cursor: pointer;
576
+ font-size: 1.2rem;
577
+ }
578
+
579
+ /* Responsive Design */
580
+ @media (max-width: 992px) {
581
+ .sidebar {
582
+ width: 80px;
583
+ }
584
+
585
+ .nav-title, .nav-item span {
586
+ display: none;
587
+ }
588
+
589
+ .nav-item {
590
+ justify-content: center;
591
+ }
592
+ }
593
+
594
+ @media (max-width: 768px) {
595
+ .main-container {
596
+ flex-direction: column;
597
+ }
598
+
599
+ .sidebar {
600
+ width: 100%;
601
+ padding: 1rem;
602
+ }
603
+
604
+ .nav-list {
605
+ display: flex;
606
+ overflow-x: auto;
607
+ gap: 0.5rem;
608
+ }
609
+
610
+ .nav-item {
611
+ padding: 0.8rem;
612
+ }
613
+
614
+ .nav-title {
615
+ display: none;
616
+ }
617
+ }
618
+ </style>
619
+ </head>
620
+ <body>
621
+ <!-- Header -->
622
+ <header class="header">
623
+ <div class="logo">
624
+ <i class="fas fa-bell"></i>
625
+ <h1>NotificationHub</h1>
626
+ </div>
627
+ <div class="user-controls">
628
+ <button class="theme-toggle" id="themeToggle">
629
+ <i class="fas fa-moon"></i>
630
+ </button>
631
+ <div class="user-profile">
632
+ <div class="avatar">AD</div>
633
+ <div>
634
+ <div>Admin User</div>
635
+ <small>Administrateur</small>
636
+ </div>
637
+ </div>
638
+ </div>
639
+ </header>
640
+
641
+ <div class="main-container">
642
+ <!-- Sidebar Navigation -->
643
+ <nav class="sidebar">
644
+ <div class="nav-section">
645
+ <div class="nav-title">Navigation Principale</div>
646
+ <div class="nav-list">
647
+ <a href="#" class="nav-item active">
648
+ <i class="fas fa-home"></i>
649
+ <span>Dashboard</span>
650
+ </a>
651
+ <a href="#" class="nav-item">
652
+ <i class="fas fa-bell"></i>
653
+ <span>Notifications</span>
654
+ </a>
655
+ <a href="#" class="nav-item">
656
+ <i class="fas fa-users"></i>
657
+ <span>Clients</span>
658
+ </a>
659
+ <a href="#" class="nav-item">
660
+ <i class="fas fa-chart-line"></i>
661
+ <span>Statistiques</span>
662
+ </a>
663
+ <a href="#" class="nav-item">
664
+ <i class="fas fa-cog"></i>
665
+ <span>Paramètres</span>
666
+ </a>
667
+ </div>
668
+ </div>
669
+
670
+ <div class="nav-section">
671
+ <div class="nav-title">Outils</div>
672
+ <div class="nav-list">
673
+ <a href="#" class="nav-item">
674
+ <i class="fas fa-plug"></i>
675
+ <span>Connexions</span>
676
+ </a>
677
+ <a href="#" class="nav-item">
678
+ <i class="fas fa-history"></i>
679
+ <span>Historique</span>
680
+ </a>
681
+ <a href="#" class="nav-item">
682
+ <i class="fas fa-shield-alt"></i>
683
+ <span>Sécurité</span>
684
+ </a>
685
+ </div>
686
+ </div>
687
+ </nav>
688
+
689
+ <!-- Main Content -->
690
+ <main class="content">
691
+ <div class="dashboard-grid">
692
+ <!-- Connection Panel -->
693
+ <div class="card">
694
+ <div class="card-header">
695
+ <h2 class="card-title"><i class="fas fa-plug"></i> Connexion WebSocket</h2>
696
+ </div>
697
+ <div class="card-body">
698
+ <div class="form-group">
699
+ <label for="userId">Identifiant Utilisateur</label>
700
+ <input type="text" id="userId" class="form-control" placeholder="user123" value="admin">
701
+ </div>
702
+
703
+ <div class="form-group">
704
+ <label for="roles">Rôles (séparés par des virgules)</label>
705
+ <input type="text" id="roles" class="form-control" placeholder="admin,user" value="admin">
706
+ </div>
707
+
708
+ <div class="form-group">
709
+ <label>État de Connexion</label>
710
+ <div class="status-indicator status-disconnected">
711
+ <span class="status-dot"></span>
712
+ <span id="statusText">Déconnecté</span>
713
+ </div>
714
+ </div>
715
+
716
+ <div class="btn-group">
717
+ <button class="btn btn-primary" onclick="connect()">
718
+ <i class="fas fa-link"></i> Connecter
719
+ </button>
720
+ <button class="btn btn-secondary" onclick="disconnect()">
721
+ <i class="fas fa-unlink"></i> Déconnecter
722
+ </button>
723
+ </div>
724
+ </div>
725
+ </div>
726
+
727
+ <!-- Stats Panel -->
728
+ <div class="card">
729
+ <div class="card-header">
730
+ <h2 class="card-title"><i class="fas fa-chart-bar"></i> Statistiques du Service</h2>
731
+ <div class="card-actions">
732
+ <button title="Actualiser" onclick="getStats()">
733
+ <i class="fas fa-sync-alt"></i>
734
+ </button>
735
+ </div>
736
+ </div>
737
+ <div class="card-body">
738
+ <div class="stats-grid">
739
+ <div class="stat-card">
740
+ <div class="stat-icon" style="background: rgba(67, 160, 71, 0.1); color: #43a047;">
741
+ <i class="fas fa-users"></i>
742
+ </div>
743
+ <div class="stat-value" id="connectedClients">0</div>
744
+ <div class="stat-label">Clients Connectés</div>
745
+ </div>
746
+
747
+ <div class="stat-card">
748
+ <div class="stat-icon" style="background: rgba(94, 53, 177, 0.1); color: #5e35b1;">
749
+ <i class="fas fa-bell"></i>
750
+ </div>
751
+ <div class="stat-value" id="totalNotifications">0</div>
752
+ <div class="stat-label">Notifications</div>
753
+ </div>
754
+
755
+ <div class="stat-card">
756
+ <div class="stat-icon" style="background: rgba(38, 166, 154, 0.1); color: #26a69a;">
757
+ <i class="fas fa-list"></i>
758
+ </div>
759
+ <div class="stat-value" id="activeSubscriptions">0</div>
760
+ <div class="stat-label">Abonnements</div>
761
+ </div>
762
+ </div>
763
+ </div>
764
+ </div>
765
+ </div>
766
+
767
+ <!-- Messages Panel -->
768
+ <div class="card">
769
+ <div class="card-header">
770
+ <h2 class="card-title"><i class="fas fa-comments"></i> Messages en Temps Réel</h2>
771
+ <div class="card-actions">
772
+ <button title="Effacer" onclick="clearMessages()">
773
+ <i class="fas fa-trash-alt"></i>
774
+ </button>
775
+ <button title="Clients" onclick="getClients()">
776
+ <i class="fas fa-users"></i>
777
+ </button>
778
+ </div>
779
+ </div>
780
+ <div class="card-body">
781
+ <div class="messages-panel">
782
+ <div class="messages" id="messages">
783
+ <!-- Messages will appear here -->
784
+ </div>
785
+
786
+ <div class="message-input-group">
787
+ <input type="text" id="messageInput" class="message-input" placeholder="Tapez votre message...">
788
+ <button class="btn btn-primary" onclick="sendMessage()">
789
+ <i class="fas fa-paper-plane"></i> Envoyer
790
+ </button>
791
+ </div>
792
+ </div>
793
+ </div>
794
+ </div>
795
+
796
+ <!-- API Test Panel -->
797
+ <div class="card">
798
+ <div class="card-header">
799
+ <h2 class="card-title"><i class="fas fa-bolt"></i> Tester les Notifications</h2>
800
+ </div>
801
+ <div class="card-body">
802
+ <div class="dashboard-grid">
803
+ <!-- User Notification -->
804
+ <div class="card">
805
+ <div class="card-header">
806
+ <h3 class="card-title"><i class="fas fa-user"></i> À un Utilisateur</h3>
807
+ </div>
808
+ <div class="card-body">
809
+ <div class="form-group">
810
+ <label>ID Utilisateur</label>
811
+ <input type="text" class="form-control" id="targetUserId" value="user123">
812
+ </div>
813
+ <button class="btn btn-primary" onclick="sendUserNotification()">
814
+ <i class="fas fa-paper-plane"></i> Envoyer Notification
815
+ </button>
816
+ </div>
817
+ </div>
818
+
819
+ <!-- Role Notification -->
820
+ <div class="card">
821
+ <div class="card-header">
822
+ <h3 class="card-title"><i class="fas fa-user-tag"></i> À un Rôle</h3>
823
+ </div>
824
+ <div class="card-body">
825
+ <div class="form-group">
826
+ <label>Rôle</label>
827
+ <input type="text" class="form-control" id="targetRole" value="admin">
828
+ </div>
829
+ <button class="btn btn-primary" onclick="sendRoleNotification()">
830
+ <i class="fas fa-paper-plane"></i> Envoyer Notification
831
+ </button>
832
+ </div>
833
+ </div>
834
+
835
+ <!-- Broadcast -->
836
+ <div class="card">
837
+ <div class="card-header">
838
+ <h3 class="card-title"><i class="fas fa-bullhorn"></i> Diffusion Globale</h3>
839
+ </div>
840
+ <div class="card-body">
841
+ <button class="btn btn-success" onclick="sendBroadcast()">
842
+ <i class="fas fa-broadcast-tower"></i> Diffuser à Tous
843
+ </button>
844
+ </div>
845
+ </div>
846
+ </div>
847
+ </div>
848
+ </div>
849
+ </main>
850
+ </div>
851
+
852
+ <!-- Toast Container -->
853
+ <div class="toast-container" id="toastContainer"></div>
854
+
855
+ <script>
856
+ // Global variables
857
+ let ws = null;
858
+ let baseUrl = window.location.origin;
859
+ let isDarkMode = false;
860
+
861
+ // DOM Elements
862
+ const statusIndicator = document.querySelector('.status-indicator');
863
+ const statusText = document.getElementById('statusText');
864
+ const themeToggle = document.getElementById('themeToggle');
865
+
866
+ // Initialize
867
+ document.addEventListener('DOMContentLoaded', function () {
868
+ // Set initial theme
869
+ if (localStorage.getItem('darkMode') === 'true') {
870
+ toggleDarkMode();
871
+ }
872
+
873
+ // Connect automatically
874
+ setTimeout(connect, 1000);
875
+
876
+ // Setup event listeners
877
+ themeToggle.addEventListener('click', toggleDarkMode);
878
+ document.getElementById('messageInput').addEventListener('keypress', function (e) {
879
+ if (e.key === 'Enter') sendMessage();
880
+ });
881
+ });
882
+
883
+ // Toggle dark mode
884
+ function toggleDarkMode() {
885
+ isDarkMode = !isDarkMode;
886
+ document.body.classList.toggle('dark-mode', isDarkMode);
887
+ localStorage.setItem('darkMode', isDarkMode);
888
+
889
+ themeToggle.innerHTML = isDarkMode ?
890
+ '<i class="fas fa-sun"></i>' :
891
+ '<i class="fas fa-moon"></i>';
892
+
893
+ themeToggle.title = isDarkMode ?
894
+ 'Passer en mode clair' :
895
+ 'Passer en mode sombre';
896
+ }
897
+
898
+ // Connection functions
899
+ function connect() {
900
+ const userId = document.getElementById('userId').value;
901
+ const roles = document.getElementById('roles').value;
902
+ const protocol = window.location.protocol === "https:" ? "wss" : "ws";
903
+ const url = `${protocol}://${window.location.host}/ws/${userId}?roles=${roles}`;
904
+
905
+ ws = new WebSocket(url);
906
+
907
+ ws.onmessage = function (event) {
908
+ const data = JSON.parse(event.data);
909
+ addMessage(data, 'notification');
910
+
911
+ // Show toast notification
912
+ showToast(
913
+ 'Nouvelle Notification',
914
+ data.message || data.content || 'Notification reçue',
915
+ 'info'
916
+ );
917
+ };
918
+
919
+ ws.onopen = function () {
920
+ updateConnectionStatus(true);
921
+ addMessage({message: 'Connexion WebSocket établie', type: 'system'}, 'system');
922
+ getStats();
923
+
924
+ showToast(
925
+ 'Connexion réussie',
926
+ 'Vous êtes maintenant connecté au service WebSocket',
927
+ 'success'
928
+ );
929
+ };
930
+
931
+ ws.onclose = function () {
932
+ updateConnectionStatus(false);
933
+ addMessage({message: 'Connexion WebSocket fermée', type: 'system'}, 'system');
934
+
935
+ showToast(
936
+ 'Déconnecté',
937
+ 'La connexion WebSocket a été fermée',
938
+ 'warning'
939
+ );
940
+ };
941
+
942
+ ws.onerror = function (error) {
943
+ addMessage({message: 'Erreur WebSocket: ' + error, type: 'error'}, 'error');
944
+
945
+ showToast(
946
+ 'Erreur de connexion',
947
+ 'Impossible de se connecter au service WebSocket',
948
+ 'error'
949
+ );
950
+ };
951
+ }
952
+
953
+ function disconnect() {
954
+ if (ws) {
955
+ ws.close();
956
+ ws = null;
957
+ updateConnectionStatus(false);
958
+ }
959
+ }
960
+
961
+ function updateConnectionStatus(connected) {
962
+ if (connected) {
963
+ statusIndicator.classList.remove('status-disconnected');
964
+ statusIndicator.classList.add('status-connected');
965
+ statusText.textContent = 'Connecté';
966
+ } else {
967
+ statusIndicator.classList.remove('status-connected');
968
+ statusIndicator.classList.add('status-disconnected');
969
+ statusText.textContent = 'Déconnecté';
970
+ }
971
+ }
972
+
973
+ // Messaging functions
974
+ function sendMessage() {
975
+ if (ws && ws.readyState === WebSocket.OPEN) {
976
+ const message = document.getElementById('messageInput').value;
977
+ if (message.trim()) {
978
+ ws.send(JSON.stringify({message: message}));
979
+ document.getElementById('messageInput').value = '';
980
+ }
981
+ } else {
982
+ addMessage({message: 'Connexion WebSocket non établie', type: 'error'}, 'error');
983
+ }
984
+ }
985
+
986
+ function addMessage(data, type) {
987
+ const messages = document.getElementById('messages');
988
+ const messageDiv = document.createElement('div');
989
+ messageDiv.className = `message message-${type}`;
990
+
991
+ const timestamp = new Date().toLocaleTimeString();
992
+ const content = data.message || JSON.stringify(data, null, 2);
993
+
994
+ messageDiv.innerHTML = `
995
+ <div class="message-header">
996
+ <span class="message-type">${type}</span>
997
+ <span class="message-timestamp">${timestamp}</span>
998
+ </div>
999
+ <div class="message-content">${content}</div>
1000
+ `;
1001
+
1002
+ messages.appendChild(messageDiv);
1003
+ messages.scrollTop = messages.scrollHeight;
1004
+ }
1005
+
1006
+ function clearMessages() {
1007
+ document.getElementById('messages').innerHTML = '';
1008
+ }
1009
+
1010
+ // Notification functions
1011
+ function sendUserNotification() {
1012
+ const userId = document.getElementById('targetUserId').value;
1013
+ const message = {
1014
+ type: "info",
1015
+ title: "Test Utilisateur",
1016
+ content: "Ceci est un message de test pour un utilisateur spécifique"
1017
+ };
1018
+
1019
+ // Simulated API call
1020
+ addMessage({
1021
+ message: `Notification envoyée à l'utilisateur ${userId}`,
1022
+ details: message
1023
+ }, 'success');
1024
+
1025
+ showToast(
1026
+ 'Notification envoyée',
1027
+ `À l'utilisateur: ${userId}`,
1028
+ 'success'
1029
+ );
1030
+ }
1031
+
1032
+ function sendRoleNotification() {
1033
+ const role = document.getElementById('targetRole').value;
1034
+ const message = {
1035
+ type: "warning",
1036
+ title: "Alerte Rôle",
1037
+ content: "Message important pour les membres de ce rôle"
1038
+ };
1039
+
1040
+ // Simulated API call
1041
+ addMessage({
1042
+ message: `Notification envoyée au rôle ${role}`,
1043
+ details: message
1044
+ }, 'success');
1045
+
1046
+ showToast(
1047
+ 'Notification envoyée',
1048
+ `Au rôle: ${role}`,
1049
+ 'success'
1050
+ );
1051
+ }
1052
+
1053
+ function sendBroadcast() {
1054
+ const message = {
1055
+ type: "announcement",
1056
+ title: "Annonce Globale",
1057
+ content: "Message diffusé à tous les clients connectés"
1058
+ };
1059
+
1060
+ // Simulated API call
1061
+ addMessage({
1062
+ message: "Notification diffusée à tous les clients",
1063
+ details: message
1064
+ }, 'success');
1065
+
1066
+ showToast(
1067
+ 'Diffusion globale',
1068
+ 'Notification envoyée à tous les clients',
1069
+ 'success'
1070
+ );
1071
+ }
1072
+
1073
+ // Stats functions
1074
+ function getStats() {
1075
+ // Simulated data
1076
+ const stats = {
1077
+ connected_clients: Math.floor(Math.random() * 100),
1078
+ total_notifications: Math.floor(Math.random() * 500),
1079
+ active_subscriptions: Math.floor(Math.random() * 50)
1080
+ };
1081
+
1082
+ document.getElementById('connectedClients').textContent = stats.connected_clients;
1083
+ document.getElementById('totalNotifications').textContent = stats.total_notifications;
1084
+ document.getElementById('activeSubscriptions').textContent = stats.active_subscriptions;
1085
+
1086
+ addMessage({
1087
+ message: "Statistiques mises à jour",
1088
+ details: stats
1089
+ }, 'system');
1090
+ }
1091
+
1092
+ function getClients() {
1093
+ // Simulated data
1094
+ const clients = [
1095
+ {id: "user123", roles: ["user"], connected: true},
1096
+ {id: "admin", roles: ["admin", "user"], connected: true},
1097
+ {id: "guest", roles: ["guest"], connected: true}
1098
+ ];
1099
+
1100
+ addMessage({
1101
+ message: "Clients connectés récupérés",
1102
+ clients: clients
1103
+ }, 'system');
1104
+
1105
+ showToast(
1106
+ 'Clients connectés',
1107
+ `${clients.length} clients actuellement connectés`,
1108
+ 'info'
1109
+ );
1110
+ }
1111
+
1112
+ // Toast notification function
1113
+ function showToast(title, message, type) {
1114
+ const toastContainer = document.getElementById('toastContainer');
1115
+ const toast = document.createElement('div');
1116
+ toast.className = `toast toast-${type}`;
1117
+
1118
+ toast.innerHTML = `
1119
+ <div class="toast-icon">
1120
+ <i class="fas fa-${type === 'error' ? 'exclamation-circle' :
1121
+ type === 'warning' ? 'exclamation-triangle' :
1122
+ type === 'success' ? 'check-circle' : 'info-circle'}"></i>
1123
+ </div>
1124
+ <div class="toast-content">
1125
+ <div class="toast-title">${title}</div>
1126
+ <div class="toast-message">${message}</div>
1127
+ </div>
1128
+ <button class="toast-close">
1129
+ <i class="fas fa-times"></i>
1130
+ </button>
1131
+ `;
1132
+
1133
+ toastContainer.appendChild(toast);
1134
+
1135
+ // Remove toast on close button click
1136
+ toast.querySelector('.toast-close').addEventListener('click', function () {
1137
+ toast.remove();
1138
+ });
1139
+
1140
+ // Auto-remove toast after 5 seconds
1141
+ setTimeout(() => {
1142
+ if (toast.parentNode) {
1143
+ toast.remove();
1144
+ }
1145
+ }, 5000);
1146
+ }
1147
+ </script>
1148
+ </body>
1149
+ </html>