@qwickapps/server 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +12 -2
  4. package/dist/index.js.map +1 -1
  5. package/dist/plugins/bans/bans-plugin.d.ts.map +1 -1
  6. package/dist/plugins/bans/bans-plugin.js +12 -3
  7. package/dist/plugins/bans/bans-plugin.js.map +1 -1
  8. package/dist/plugins/devices/__tests__/devices-plugin.test.d.ts +11 -0
  9. package/dist/plugins/devices/__tests__/devices-plugin.test.d.ts.map +1 -0
  10. package/dist/plugins/devices/__tests__/devices-plugin.test.js +410 -0
  11. package/dist/plugins/devices/__tests__/devices-plugin.test.js.map +1 -0
  12. package/dist/plugins/devices/__tests__/token-utils.test.d.ts +7 -0
  13. package/dist/plugins/devices/__tests__/token-utils.test.d.ts.map +1 -0
  14. package/dist/plugins/devices/__tests__/token-utils.test.js +197 -0
  15. package/dist/plugins/devices/__tests__/token-utils.test.js.map +1 -0
  16. package/dist/plugins/devices/adapters/compute-adapter.d.ts +36 -0
  17. package/dist/plugins/devices/adapters/compute-adapter.d.ts.map +1 -0
  18. package/dist/plugins/devices/adapters/compute-adapter.js +100 -0
  19. package/dist/plugins/devices/adapters/compute-adapter.js.map +1 -0
  20. package/dist/plugins/devices/adapters/index.d.ts +12 -0
  21. package/dist/plugins/devices/adapters/index.d.ts.map +1 -0
  22. package/dist/plugins/devices/adapters/index.js +10 -0
  23. package/dist/plugins/devices/adapters/index.js.map +1 -0
  24. package/dist/plugins/devices/adapters/mobile-adapter.d.ts +41 -0
  25. package/dist/plugins/devices/adapters/mobile-adapter.d.ts.map +1 -0
  26. package/dist/plugins/devices/adapters/mobile-adapter.js +131 -0
  27. package/dist/plugins/devices/adapters/mobile-adapter.js.map +1 -0
  28. package/dist/plugins/devices/devices-plugin.d.ts +70 -0
  29. package/dist/plugins/devices/devices-plugin.d.ts.map +1 -0
  30. package/dist/plugins/devices/devices-plugin.js +453 -0
  31. package/dist/plugins/devices/devices-plugin.js.map +1 -0
  32. package/dist/plugins/devices/index.d.ts +18 -0
  33. package/dist/plugins/devices/index.d.ts.map +1 -0
  34. package/dist/plugins/devices/index.js +18 -0
  35. package/dist/plugins/devices/index.js.map +1 -0
  36. package/dist/plugins/devices/stores/index.d.ts +9 -0
  37. package/dist/plugins/devices/stores/index.d.ts.map +1 -0
  38. package/dist/plugins/devices/stores/index.js +9 -0
  39. package/dist/plugins/devices/stores/index.js.map +1 -0
  40. package/dist/plugins/devices/stores/postgres-store.d.ts +26 -0
  41. package/dist/plugins/devices/stores/postgres-store.d.ts.map +1 -0
  42. package/dist/plugins/devices/stores/postgres-store.js +199 -0
  43. package/dist/plugins/devices/stores/postgres-store.js.map +1 -0
  44. package/dist/plugins/devices/token-utils.d.ts +100 -0
  45. package/dist/plugins/devices/token-utils.d.ts.map +1 -0
  46. package/dist/plugins/devices/token-utils.js +162 -0
  47. package/dist/plugins/devices/token-utils.js.map +1 -0
  48. package/dist/plugins/devices/types.d.ts +307 -0
  49. package/dist/plugins/devices/types.d.ts.map +1 -0
  50. package/dist/plugins/devices/types.js +10 -0
  51. package/dist/plugins/devices/types.js.map +1 -0
  52. package/dist/plugins/index.d.ts +14 -2
  53. package/dist/plugins/index.d.ts.map +1 -1
  54. package/dist/plugins/index.js +13 -1
  55. package/dist/plugins/index.js.map +1 -1
  56. package/dist/plugins/notifications/__tests__/notifications-manager.test.d.ts +5 -0
  57. package/dist/plugins/notifications/__tests__/notifications-manager.test.d.ts.map +1 -0
  58. package/dist/plugins/notifications/__tests__/notifications-manager.test.js +470 -0
  59. package/dist/plugins/notifications/__tests__/notifications-manager.test.js.map +1 -0
  60. package/dist/plugins/notifications/index.d.ts +71 -0
  61. package/dist/plugins/notifications/index.d.ts.map +1 -0
  62. package/dist/plugins/notifications/index.js +72 -0
  63. package/dist/plugins/notifications/index.js.map +1 -0
  64. package/dist/plugins/notifications/notifications-manager.d.ts +182 -0
  65. package/dist/plugins/notifications/notifications-manager.d.ts.map +1 -0
  66. package/dist/plugins/notifications/notifications-manager.js +610 -0
  67. package/dist/plugins/notifications/notifications-manager.js.map +1 -0
  68. package/dist/plugins/notifications/notifications-plugin.d.ts +83 -0
  69. package/dist/plugins/notifications/notifications-plugin.d.ts.map +1 -0
  70. package/dist/plugins/notifications/notifications-plugin.js +337 -0
  71. package/dist/plugins/notifications/notifications-plugin.js.map +1 -0
  72. package/dist/plugins/notifications/types.d.ts +164 -0
  73. package/dist/plugins/notifications/types.d.ts.map +1 -0
  74. package/dist/plugins/notifications/types.js +9 -0
  75. package/dist/plugins/notifications/types.js.map +1 -0
  76. package/dist/plugins/parental/__tests__/parental-plugin.test.d.ts +12 -0
  77. package/dist/plugins/parental/__tests__/parental-plugin.test.d.ts.map +1 -0
  78. package/dist/plugins/parental/__tests__/parental-plugin.test.js +349 -0
  79. package/dist/plugins/parental/__tests__/parental-plugin.test.js.map +1 -0
  80. package/dist/plugins/parental/adapters/index.d.ts +8 -0
  81. package/dist/plugins/parental/adapters/index.d.ts.map +1 -0
  82. package/dist/plugins/parental/adapters/index.js +7 -0
  83. package/dist/plugins/parental/adapters/index.js.map +1 -0
  84. package/dist/plugins/parental/adapters/kids-adapter.d.ts +24 -0
  85. package/dist/plugins/parental/adapters/kids-adapter.d.ts.map +1 -0
  86. package/dist/plugins/parental/adapters/kids-adapter.js +174 -0
  87. package/dist/plugins/parental/adapters/kids-adapter.js.map +1 -0
  88. package/dist/plugins/parental/index.d.ts +14 -0
  89. package/dist/plugins/parental/index.d.ts.map +1 -0
  90. package/dist/plugins/parental/index.js +15 -0
  91. package/dist/plugins/parental/index.js.map +1 -0
  92. package/dist/plugins/parental/parental-plugin.d.ts +88 -0
  93. package/dist/plugins/parental/parental-plugin.d.ts.map +1 -0
  94. package/dist/plugins/parental/parental-plugin.js +666 -0
  95. package/dist/plugins/parental/parental-plugin.js.map +1 -0
  96. package/dist/plugins/parental/stores/index.d.ts +7 -0
  97. package/dist/plugins/parental/stores/index.d.ts.map +1 -0
  98. package/dist/plugins/parental/stores/index.js +7 -0
  99. package/dist/plugins/parental/stores/index.js.map +1 -0
  100. package/dist/plugins/parental/stores/postgres-store.d.ts +10 -0
  101. package/dist/plugins/parental/stores/postgres-store.d.ts.map +1 -0
  102. package/dist/plugins/parental/stores/postgres-store.js +209 -0
  103. package/dist/plugins/parental/stores/postgres-store.js.map +1 -0
  104. package/dist/plugins/parental/types.d.ts +154 -0
  105. package/dist/plugins/parental/types.d.ts.map +1 -0
  106. package/dist/plugins/parental/types.js +10 -0
  107. package/dist/plugins/parental/types.js.map +1 -0
  108. package/dist/plugins/profiles/__tests__/profiles-plugin.test.d.ts +11 -0
  109. package/dist/plugins/profiles/__tests__/profiles-plugin.test.d.ts.map +1 -0
  110. package/dist/plugins/profiles/__tests__/profiles-plugin.test.js +243 -0
  111. package/dist/plugins/profiles/__tests__/profiles-plugin.test.js.map +1 -0
  112. package/dist/plugins/profiles/index.d.ts +12 -0
  113. package/dist/plugins/profiles/index.d.ts.map +1 -0
  114. package/dist/plugins/profiles/index.js +13 -0
  115. package/dist/plugins/profiles/index.js.map +1 -0
  116. package/dist/plugins/profiles/profiles-plugin.d.ts +71 -0
  117. package/dist/plugins/profiles/profiles-plugin.d.ts.map +1 -0
  118. package/dist/plugins/profiles/profiles-plugin.js +481 -0
  119. package/dist/plugins/profiles/profiles-plugin.js.map +1 -0
  120. package/dist/plugins/profiles/stores/index.d.ts +9 -0
  121. package/dist/plugins/profiles/stores/index.d.ts.map +1 -0
  122. package/dist/plugins/profiles/stores/index.js +9 -0
  123. package/dist/plugins/profiles/stores/index.js.map +1 -0
  124. package/dist/plugins/profiles/stores/postgres-store.d.ts +18 -0
  125. package/dist/plugins/profiles/stores/postgres-store.d.ts.map +1 -0
  126. package/dist/plugins/profiles/stores/postgres-store.js +310 -0
  127. package/dist/plugins/profiles/stores/postgres-store.js.map +1 -0
  128. package/dist/plugins/profiles/types.d.ts +289 -0
  129. package/dist/plugins/profiles/types.d.ts.map +1 -0
  130. package/dist/plugins/profiles/types.js +10 -0
  131. package/dist/plugins/profiles/types.js.map +1 -0
  132. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.d.ts +11 -0
  133. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.d.ts.map +1 -0
  134. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.js +305 -0
  135. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.js.map +1 -0
  136. package/dist/plugins/subscriptions/index.d.ts +12 -0
  137. package/dist/plugins/subscriptions/index.d.ts.map +1 -0
  138. package/dist/plugins/subscriptions/index.js +13 -0
  139. package/dist/plugins/subscriptions/index.js.map +1 -0
  140. package/dist/plugins/subscriptions/stores/index.d.ts +9 -0
  141. package/dist/plugins/subscriptions/stores/index.d.ts.map +1 -0
  142. package/dist/plugins/subscriptions/stores/index.js +9 -0
  143. package/dist/plugins/subscriptions/stores/index.js.map +1 -0
  144. package/dist/plugins/subscriptions/stores/postgres-store.d.ts +14 -0
  145. package/dist/plugins/subscriptions/stores/postgres-store.d.ts.map +1 -0
  146. package/dist/plugins/subscriptions/stores/postgres-store.js +359 -0
  147. package/dist/plugins/subscriptions/stores/postgres-store.js.map +1 -0
  148. package/dist/plugins/subscriptions/subscriptions-plugin.d.ts +82 -0
  149. package/dist/plugins/subscriptions/subscriptions-plugin.d.ts.map +1 -0
  150. package/dist/plugins/subscriptions/subscriptions-plugin.js +449 -0
  151. package/dist/plugins/subscriptions/subscriptions-plugin.js.map +1 -0
  152. package/dist/plugins/subscriptions/types.d.ts +308 -0
  153. package/dist/plugins/subscriptions/types.d.ts.map +1 -0
  154. package/dist/plugins/subscriptions/types.js +10 -0
  155. package/dist/plugins/subscriptions/types.js.map +1 -0
  156. package/dist/plugins/usage/__tests__/usage-plugin.test.d.ts +11 -0
  157. package/dist/plugins/usage/__tests__/usage-plugin.test.d.ts.map +1 -0
  158. package/dist/plugins/usage/__tests__/usage-plugin.test.js +218 -0
  159. package/dist/plugins/usage/__tests__/usage-plugin.test.js.map +1 -0
  160. package/dist/plugins/usage/index.d.ts +12 -0
  161. package/dist/plugins/usage/index.d.ts.map +1 -0
  162. package/dist/plugins/usage/index.js +13 -0
  163. package/dist/plugins/usage/index.js.map +1 -0
  164. package/dist/plugins/usage/stores/index.d.ts +9 -0
  165. package/dist/plugins/usage/stores/index.d.ts.map +1 -0
  166. package/dist/plugins/usage/stores/index.js +9 -0
  167. package/dist/plugins/usage/stores/index.js.map +1 -0
  168. package/dist/plugins/usage/stores/postgres-store.d.ts +14 -0
  169. package/dist/plugins/usage/stores/postgres-store.d.ts.map +1 -0
  170. package/dist/plugins/usage/stores/postgres-store.js +146 -0
  171. package/dist/plugins/usage/stores/postgres-store.js.map +1 -0
  172. package/dist/plugins/usage/types.d.ts +195 -0
  173. package/dist/plugins/usage/types.d.ts.map +1 -0
  174. package/dist/plugins/usage/types.js +10 -0
  175. package/dist/plugins/usage/types.js.map +1 -0
  176. package/dist/plugins/usage/usage-plugin.d.ts +51 -0
  177. package/dist/plugins/usage/usage-plugin.d.ts.map +1 -0
  178. package/dist/plugins/usage/usage-plugin.js +412 -0
  179. package/dist/plugins/usage/usage-plugin.js.map +1 -0
  180. package/dist/plugins/users/__tests__/postgres-store.test.d.ts +10 -0
  181. package/dist/plugins/users/__tests__/postgres-store.test.d.ts.map +1 -0
  182. package/dist/plugins/users/__tests__/postgres-store.test.js +229 -0
  183. package/dist/plugins/users/__tests__/postgres-store.test.js.map +1 -0
  184. package/dist/plugins/users/__tests__/users-plugin.test.js +3 -0
  185. package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -1
  186. package/dist/plugins/users/index.d.ts +2 -2
  187. package/dist/plugins/users/index.d.ts.map +1 -1
  188. package/dist/plugins/users/index.js +1 -1
  189. package/dist/plugins/users/index.js.map +1 -1
  190. package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -1
  191. package/dist/plugins/users/stores/postgres-store.js +76 -0
  192. package/dist/plugins/users/stores/postgres-store.js.map +1 -1
  193. package/dist/plugins/users/types.d.ts +74 -6
  194. package/dist/plugins/users/types.d.ts.map +1 -1
  195. package/dist/plugins/users/users-plugin.d.ts +15 -1
  196. package/dist/plugins/users/users-plugin.d.ts.map +1 -1
  197. package/dist/plugins/users/users-plugin.js +29 -0
  198. package/dist/plugins/users/users-plugin.js.map +1 -1
  199. package/dist-ui/assets/index-CynOqPkb.js +469 -0
  200. package/dist-ui/assets/index-CynOqPkb.js.map +1 -0
  201. package/dist-ui/index.html +1 -1
  202. package/dist-ui-lib/api/controlPanelApi.d.ts +46 -0
  203. package/dist-ui-lib/components/StatCard.d.ts +16 -0
  204. package/dist-ui-lib/dashboard/widgets/NotificationsStatsWidget.d.ts +12 -0
  205. package/dist-ui-lib/dashboard/widgets/index.d.ts +1 -0
  206. package/dist-ui-lib/index.js +1822 -1611
  207. package/dist-ui-lib/index.js.map +1 -1
  208. package/dist-ui-lib/pages/NotificationsPage.d.ts +9 -0
  209. package/dist-ui-lib/utils/formatters.d.ts +19 -0
  210. package/package.json +1 -1
  211. package/src/index.ts +178 -0
  212. package/src/plugins/bans/bans-plugin.ts +15 -3
  213. package/src/plugins/devices/__tests__/devices-plugin.test.ts +551 -0
  214. package/src/plugins/devices/__tests__/token-utils.test.ts +264 -0
  215. package/src/plugins/devices/adapters/compute-adapter.ts +139 -0
  216. package/src/plugins/devices/adapters/index.ts +13 -0
  217. package/src/plugins/devices/adapters/mobile-adapter.ts +179 -0
  218. package/src/plugins/devices/devices-plugin.ts +538 -0
  219. package/src/plugins/devices/index.ts +69 -0
  220. package/src/plugins/devices/stores/index.ts +9 -0
  221. package/src/plugins/devices/stores/postgres-store.ts +304 -0
  222. package/src/plugins/devices/token-utils.ts +213 -0
  223. package/src/plugins/devices/types.ts +351 -0
  224. package/src/plugins/index.ts +218 -0
  225. package/src/plugins/notifications/__tests__/notifications-manager.test.ts +637 -0
  226. package/src/plugins/notifications/index.ts +91 -0
  227. package/src/plugins/notifications/notifications-manager.ts +773 -0
  228. package/src/plugins/notifications/notifications-plugin.ts +398 -0
  229. package/src/plugins/notifications/types.ts +207 -0
  230. package/src/plugins/parental/__tests__/parental-plugin.test.ts +465 -0
  231. package/src/plugins/parental/adapters/index.ts +8 -0
  232. package/src/plugins/parental/adapters/kids-adapter.ts +206 -0
  233. package/src/plugins/parental/index.ts +55 -0
  234. package/src/plugins/parental/parental-plugin.ts +759 -0
  235. package/src/plugins/parental/stores/index.ts +7 -0
  236. package/src/plugins/parental/stores/postgres-store.ts +304 -0
  237. package/src/plugins/parental/types.ts +180 -0
  238. package/src/plugins/profiles/__tests__/profiles-plugin.test.ts +321 -0
  239. package/src/plugins/profiles/index.ts +49 -0
  240. package/src/plugins/profiles/profiles-plugin.ts +546 -0
  241. package/src/plugins/profiles/stores/index.ts +9 -0
  242. package/src/plugins/profiles/stores/postgres-store.ts +439 -0
  243. package/src/plugins/profiles/types.ts +338 -0
  244. package/src/plugins/subscriptions/__tests__/subscriptions-plugin.test.ts +404 -0
  245. package/src/plugins/subscriptions/index.ts +51 -0
  246. package/src/plugins/subscriptions/stores/index.ts +9 -0
  247. package/src/plugins/subscriptions/stores/postgres-store.ts +482 -0
  248. package/src/plugins/subscriptions/subscriptions-plugin.ts +530 -0
  249. package/src/plugins/subscriptions/types.ts +355 -0
  250. package/src/plugins/usage/__tests__/usage-plugin.test.ts +288 -0
  251. package/src/plugins/usage/index.ts +39 -0
  252. package/src/plugins/usage/stores/index.ts +9 -0
  253. package/src/plugins/usage/stores/postgres-store.ts +213 -0
  254. package/src/plugins/usage/types.ts +222 -0
  255. package/src/plugins/usage/usage-plugin.ts +484 -0
  256. package/src/plugins/users/__tests__/postgres-store.test.ts +326 -0
  257. package/src/plugins/users/__tests__/users-plugin.test.ts +3 -0
  258. package/src/plugins/users/index.ts +6 -0
  259. package/src/plugins/users/stores/postgres-store.ts +104 -0
  260. package/src/plugins/users/types.ts +82 -6
  261. package/src/plugins/users/users-plugin.ts +37 -0
  262. package/ui/src/App.tsx +5 -1
  263. package/ui/src/api/controlPanelApi.ts +103 -6
  264. package/ui/src/components/StatCard.tsx +58 -0
  265. package/ui/src/dashboard/builtInWidgets.tsx +3 -1
  266. package/ui/src/dashboard/widgets/NotificationsStatsWidget.tsx +167 -0
  267. package/ui/src/dashboard/widgets/index.ts +1 -0
  268. package/ui/src/pages/NotificationsPage.tsx +417 -0
  269. package/ui/src/utils/formatters.ts +33 -0
  270. package/dist-ui/assets/index-D7DoZ9rL.js +0 -478
  271. package/dist-ui/assets/index-D7DoZ9rL.js.map +0 -1
@@ -347,6 +347,51 @@ export interface TestProviderResponse {
347
347
  };
348
348
  }
349
349
 
350
+ // ==================
351
+ // Notifications API Types
352
+ // ==================
353
+
354
+ export interface NotificationsConnectionHealth {
355
+ isConnected: boolean;
356
+ isHealthy: boolean;
357
+ lastEventAt: string | null;
358
+ timeSinceLastEvent: number;
359
+ channelCount: number;
360
+ isReconnecting: boolean;
361
+ reconnectAttempts: number;
362
+ }
363
+
364
+ export interface NotificationsStatsResponse {
365
+ totalConnections: number;
366
+ currentConnections: number;
367
+ eventsProcessed: number;
368
+ eventsRouted: number;
369
+ eventsParseFailed: number;
370
+ eventsDroppedNoClients: number;
371
+ reconnectionAttempts: number;
372
+ lastReconnectionAt?: string;
373
+ clientsByType: {
374
+ device: number;
375
+ user: number;
376
+ };
377
+ connectionHealth: NotificationsConnectionHealth;
378
+ channels: string[];
379
+ lastEventAt?: string;
380
+ }
381
+
382
+ export interface NotificationsClient {
383
+ id: string;
384
+ deviceId?: string;
385
+ userId?: string;
386
+ connectedAt: string;
387
+ durationMs: number;
388
+ }
389
+
390
+ export interface NotificationsClientsResponse {
391
+ clients: NotificationsClient[];
392
+ total: number;
393
+ }
394
+
350
395
  // ==================
351
396
  // Rate Limit Config Types
352
397
  // ==================
@@ -470,7 +515,7 @@ class ControlPanelApi {
470
515
  const params = new URLSearchParams();
471
516
  if (options.limit) params.set('limit', options.limit.toString());
472
517
  if (options.page) params.set('page', options.page.toString());
473
- if (options.search) params.set('search', options.search);
518
+ if (options.search) params.set('q', options.search);
474
519
 
475
520
  const response = await fetch(`${this.baseUrl}/api/users?${params}`);
476
521
  if (!response.ok) {
@@ -500,10 +545,18 @@ class ControlPanelApi {
500
545
  }
501
546
 
502
547
  async banUser(email: string, reason: string, expiresAt?: string): Promise<void> {
503
- const response = await fetch(`${this.baseUrl}/api/bans`, {
548
+ // Convert expiresAt datetime to duration in seconds
549
+ let duration: number | undefined;
550
+ if (expiresAt) {
551
+ const expiresDate = new Date(expiresAt);
552
+ const now = new Date();
553
+ duration = Math.max(0, Math.floor((expiresDate.getTime() - now.getTime()) / 1000));
554
+ }
555
+
556
+ const response = await fetch(`${this.baseUrl}/api/bans/email/${encodeURIComponent(email)}`, {
504
557
  method: 'POST',
505
558
  headers: { 'Content-Type': 'application/json' },
506
- body: JSON.stringify({ email, reason, expiresAt }),
559
+ body: JSON.stringify({ reason, duration }),
507
560
  });
508
561
  if (!response.ok) {
509
562
  const error = await response.json().catch(() => ({}));
@@ -512,7 +565,7 @@ class ControlPanelApi {
512
565
  }
513
566
 
514
567
  async unbanUser(email: string): Promise<void> {
515
- const response = await fetch(`${this.baseUrl}/api/bans/${encodeURIComponent(email)}`, {
568
+ const response = await fetch(`${this.baseUrl}/api/bans/email/${encodeURIComponent(email)}`, {
516
569
  method: 'DELETE',
517
570
  });
518
571
  if (!response.ok) {
@@ -521,11 +574,13 @@ class ControlPanelApi {
521
574
  }
522
575
 
523
576
  async checkBan(email: string): Promise<{ banned: boolean; ban?: Ban }> {
524
- const response = await fetch(`${this.baseUrl}/api/bans/check/${encodeURIComponent(email)}`);
577
+ const response = await fetch(`${this.baseUrl}/api/bans/email/${encodeURIComponent(email)}`);
525
578
  if (!response.ok) {
526
579
  throw new Error(`Ban check failed: ${response.statusText}`);
527
580
  }
528
- return response.json();
581
+ const data = await response.json();
582
+ // Backend returns { email, isBanned }, transform to expected shape
583
+ return { banned: data.isBanned };
529
584
  }
530
585
 
531
586
  // ==================
@@ -821,6 +876,48 @@ class ControlPanelApi {
821
876
  }
822
877
  return response.json();
823
878
  }
879
+
880
+ // ==================
881
+ // Notifications API
882
+ // ==================
883
+
884
+ async getNotificationsStats(): Promise<NotificationsStatsResponse> {
885
+ const response = await fetch(`${this.baseUrl}/api/notifications/stats`);
886
+ if (!response.ok) {
887
+ throw new Error(`Notifications stats request failed: ${response.statusText}`);
888
+ }
889
+ return response.json();
890
+ }
891
+
892
+ async getNotificationsClients(): Promise<NotificationsClientsResponse> {
893
+ const response = await fetch(`${this.baseUrl}/api/notifications/clients`);
894
+ if (!response.ok) {
895
+ throw new Error(`Notifications clients request failed: ${response.statusText}`);
896
+ }
897
+ return response.json();
898
+ }
899
+
900
+ async disconnectNotificationsClient(clientId: string): Promise<{ success: boolean }> {
901
+ const response = await fetch(`${this.baseUrl}/api/notifications/clients/${encodeURIComponent(clientId)}`, {
902
+ method: 'DELETE',
903
+ });
904
+ if (!response.ok) {
905
+ const error = await response.json().catch(() => ({}));
906
+ throw new Error(error.error || `Disconnect client failed: ${response.statusText}`);
907
+ }
908
+ return response.json();
909
+ }
910
+
911
+ async forceNotificationsReconnect(): Promise<{ success: boolean; message: string }> {
912
+ const response = await fetch(`${this.baseUrl}/api/notifications/reconnect`, {
913
+ method: 'POST',
914
+ });
915
+ if (!response.ok) {
916
+ const error = await response.json().catch(() => ({}));
917
+ throw new Error(error.error || `Force reconnect failed: ${response.statusText}`);
918
+ }
919
+ return response.json();
920
+ }
824
921
  }
825
922
 
826
923
  export const api = new ControlPanelApi();
@@ -0,0 +1,58 @@
1
+ /**
2
+ * StatCard Component
3
+ *
4
+ * A reusable card component for displaying statistics with an icon,
5
+ * value, label, and optional sub-value.
6
+ *
7
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
+ */
9
+
10
+ import { Box, Card, CardContent } from '@mui/material';
11
+ import { Text } from '@qwickapps/react-framework';
12
+
13
+ export interface StatCardProps {
14
+ icon: React.ReactNode;
15
+ label: string;
16
+ value: string | number;
17
+ subValue?: string;
18
+ color?: string;
19
+ }
20
+
21
+ export function StatCard({
22
+ icon,
23
+ label,
24
+ value,
25
+ subValue,
26
+ color = 'var(--theme-primary)',
27
+ }: StatCardProps) {
28
+ return (
29
+ <Card sx={{ bgcolor: 'var(--theme-surface)', height: '100%' }}>
30
+ <CardContent sx={{ py: 1.5, '&:last-child': { pb: 1.5 } }}>
31
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
32
+ <Box sx={{ color }}>{icon}</Box>
33
+ <Box sx={{ flex: 1, minWidth: 0 }}>
34
+ <Text
35
+ variant="h4"
36
+ content={String(value)}
37
+ customColor="var(--theme-text-primary)"
38
+ fontWeight="600"
39
+ />
40
+ <Text
41
+ variant="caption"
42
+ content={label}
43
+ customColor="var(--theme-text-secondary)"
44
+ />
45
+ {subValue && (
46
+ <Text
47
+ variant="caption"
48
+ content={subValue}
49
+ customColor="var(--theme-text-secondary)"
50
+ sx={{ display: 'block', mt: 0.25 }}
51
+ />
52
+ )}
53
+ </Box>
54
+ </Box>
55
+ </CardContent>
56
+ </Card>
57
+ );
58
+ }
@@ -10,7 +10,7 @@
10
10
  * Copyright (c) 2025 QwickApps.com. All rights reserved.
11
11
  */
12
12
 
13
- import { ServiceHealthWidget, IntegrationStatusWidget, AuthStatusWidget } from './widgets';
13
+ import { ServiceHealthWidget, IntegrationStatusWidget, AuthStatusWidget, NotificationsStatsWidget } from './widgets';
14
14
  import type { WidgetComponent } from './WidgetComponentRegistry';
15
15
 
16
16
  /**
@@ -21,6 +21,7 @@ export const builtInWidgetComponents: Record<string, React.ComponentType> = {
21
21
  ServiceHealthWidget: ServiceHealthWidget,
22
22
  IntegrationStatusWidget: IntegrationStatusWidget,
23
23
  AuthStatusWidget: AuthStatusWidget,
24
+ NotificationsStatsWidget: NotificationsStatsWidget,
24
25
  };
25
26
 
26
27
  /**
@@ -35,5 +36,6 @@ export function getBuiltInWidgetComponents(): WidgetComponent[] {
35
36
  { name: 'ServiceHealthWidget', component: ServiceHealthWidget },
36
37
  { name: 'IntegrationStatusWidget', component: IntegrationStatusWidget },
37
38
  { name: 'AuthStatusWidget', component: AuthStatusWidget },
39
+ { name: 'NotificationsStatsWidget', component: NotificationsStatsWidget },
38
40
  ];
39
41
  }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Notifications Stats Widget
3
+ *
4
+ * Displays realtime notifications connection statistics on the Control Panel dashboard.
5
+ * Shows active clients, events processed, and connection health.
6
+ *
7
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
+ */
9
+
10
+ import { useState, useEffect } from 'react';
11
+ import { Box, Card, CardContent, Chip, LinearProgress } from '@mui/material';
12
+ import { GridLayout, Text } from '@qwickapps/react-framework';
13
+ import WifiIcon from '@mui/icons-material/Wifi';
14
+ import WifiOffIcon from '@mui/icons-material/WifiOff';
15
+ import DevicesIcon from '@mui/icons-material/Devices';
16
+ import PersonIcon from '@mui/icons-material/Person';
17
+ import SendIcon from '@mui/icons-material/Send';
18
+ import ErrorIcon from '@mui/icons-material/Error';
19
+ import { api } from '../../api/controlPanelApi';
20
+ import type { NotificationsStatsResponse } from '../../api/controlPanelApi';
21
+ import { StatCard } from '../../components/StatCard';
22
+ import { formatNumber, formatDuration } from '../../utils/formatters';
23
+
24
+ /**
25
+ * Notifications Stats Widget Component
26
+ */
27
+ export function NotificationsStatsWidget() {
28
+ const [stats, setStats] = useState<NotificationsStatsResponse | null>(null);
29
+ const [error, setError] = useState<string | null>(null);
30
+ const [loading, setLoading] = useState(true);
31
+
32
+ useEffect(() => {
33
+ const fetchStats = async () => {
34
+ try {
35
+ const data = await api.getNotificationsStats();
36
+ setStats(data);
37
+ setError(null);
38
+ } catch (err) {
39
+ // Check if it's a 404 (plugin not enabled)
40
+ if (err instanceof Error && err.message.includes('404')) {
41
+ setError('Notifications plugin not enabled');
42
+ } else {
43
+ setError(err instanceof Error ? err.message : 'Failed to fetch stats');
44
+ }
45
+ } finally {
46
+ setLoading(false);
47
+ }
48
+ };
49
+
50
+ fetchStats();
51
+ const interval = setInterval(fetchStats, 5000); // Refresh every 5 seconds
52
+ return () => clearInterval(interval);
53
+ }, []);
54
+
55
+ if (loading) {
56
+ return (
57
+ <Card sx={{ bgcolor: 'var(--theme-surface)' }}>
58
+ <CardContent>
59
+ <LinearProgress />
60
+ </CardContent>
61
+ </Card>
62
+ );
63
+ }
64
+
65
+ if (error) {
66
+ return (
67
+ <Card sx={{ bgcolor: 'var(--theme-surface)', border: '1px solid var(--theme-border)' }}>
68
+ <CardContent>
69
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
70
+ <WifiOffIcon sx={{ color: 'var(--theme-text-secondary)' }} />
71
+ <Text variant="body2" customColor="var(--theme-text-secondary)" content={error} />
72
+ </Box>
73
+ </CardContent>
74
+ </Card>
75
+ );
76
+ }
77
+
78
+ if (!stats) {
79
+ return null;
80
+ }
81
+
82
+ const isHealthy = stats.connectionHealth.isHealthy;
83
+ const healthColor = isHealthy ? 'var(--theme-success)' : 'var(--theme-warning)';
84
+
85
+ return (
86
+ <Box>
87
+ {/* Connection Status Bar */}
88
+ <Card sx={{ bgcolor: 'var(--theme-surface)', mb: 2 }}>
89
+ <CardContent sx={{ py: 1, '&:last-child': { pb: 1 } }}>
90
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
91
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
92
+ {isHealthy ? (
93
+ <WifiIcon sx={{ color: healthColor, fontSize: 20 }} />
94
+ ) : (
95
+ <WifiOffIcon sx={{ color: healthColor, fontSize: 20 }} />
96
+ )}
97
+ <Text
98
+ variant="body2"
99
+ content={isHealthy ? 'Connected' : 'Reconnecting...'}
100
+ customColor={healthColor}
101
+ fontWeight="500"
102
+ />
103
+ {stats.connectionHealth.isReconnecting && (
104
+ <Chip
105
+ label={`Attempt ${stats.connectionHealth.reconnectAttempts}`}
106
+ size="small"
107
+ sx={{
108
+ bgcolor: 'var(--theme-warning)20',
109
+ color: 'var(--theme-warning)',
110
+ fontSize: '0.7rem',
111
+ height: 18,
112
+ }}
113
+ />
114
+ )}
115
+ </Box>
116
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
117
+ <Text
118
+ variant="caption"
119
+ content={`${stats.channels.length} channel${stats.channels.length !== 1 ? 's' : ''}`}
120
+ customColor="var(--theme-text-secondary)"
121
+ />
122
+ {stats.lastEventAt && (
123
+ <Text
124
+ variant="caption"
125
+ content={`Last event: ${formatDuration(stats.connectionHealth.timeSinceLastEvent)} ago`}
126
+ customColor="var(--theme-text-secondary)"
127
+ />
128
+ )}
129
+ </Box>
130
+ </Box>
131
+ </CardContent>
132
+ </Card>
133
+
134
+ {/* Stats Grid */}
135
+ <GridLayout columns={4} spacing="small" equalHeight>
136
+ <StatCard
137
+ icon={<DevicesIcon sx={{ fontSize: 28 }} />}
138
+ label="Active Clients"
139
+ value={stats.currentConnections}
140
+ subValue={`${stats.totalConnections} total`}
141
+ color="var(--theme-primary)"
142
+ />
143
+ <StatCard
144
+ icon={<PersonIcon sx={{ fontSize: 28 }} />}
145
+ label="By Device"
146
+ value={stats.clientsByType.device}
147
+ subValue={`${stats.clientsByType.user} by user`}
148
+ color="var(--theme-info)"
149
+ />
150
+ <StatCard
151
+ icon={<SendIcon sx={{ fontSize: 28 }} />}
152
+ label="Events Routed"
153
+ value={formatNumber(stats.eventsRouted)}
154
+ subValue={`${formatNumber(stats.eventsProcessed)} processed`}
155
+ color="var(--theme-success)"
156
+ />
157
+ <StatCard
158
+ icon={<ErrorIcon sx={{ fontSize: 28 }} />}
159
+ label="Dropped"
160
+ value={formatNumber(stats.eventsDroppedNoClients)}
161
+ subValue={`${stats.eventsParseFailed} parse errors`}
162
+ color={stats.eventsDroppedNoClients > 0 ? 'var(--theme-warning)' : 'var(--theme-text-secondary)'}
163
+ />
164
+ </GridLayout>
165
+ </Box>
166
+ );
167
+ }
@@ -7,3 +7,4 @@
7
7
  export { ServiceHealthWidget } from './ServiceHealthWidget';
8
8
  export { IntegrationStatusWidget } from './IntegrationStatusWidget';
9
9
  export { AuthStatusWidget } from './AuthStatusWidget';
10
+ export { NotificationsStatsWidget } from './NotificationsStatsWidget';