@qwickapps/server 1.4.0 → 1.5.1

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 (273) hide show
  1. package/CHANGELOG.md +507 -0
  2. package/README.md +9 -0
  3. package/dist/index.d.ts +2 -2
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +12 -2
  6. package/dist/index.js.map +1 -1
  7. package/dist/plugins/bans/bans-plugin.d.ts.map +1 -1
  8. package/dist/plugins/bans/bans-plugin.js +12 -3
  9. package/dist/plugins/bans/bans-plugin.js.map +1 -1
  10. package/dist/plugins/devices/__tests__/devices-plugin.test.d.ts +11 -0
  11. package/dist/plugins/devices/__tests__/devices-plugin.test.d.ts.map +1 -0
  12. package/dist/plugins/devices/__tests__/devices-plugin.test.js +410 -0
  13. package/dist/plugins/devices/__tests__/devices-plugin.test.js.map +1 -0
  14. package/dist/plugins/devices/__tests__/token-utils.test.d.ts +7 -0
  15. package/dist/plugins/devices/__tests__/token-utils.test.d.ts.map +1 -0
  16. package/dist/plugins/devices/__tests__/token-utils.test.js +197 -0
  17. package/dist/plugins/devices/__tests__/token-utils.test.js.map +1 -0
  18. package/dist/plugins/devices/adapters/compute-adapter.d.ts +36 -0
  19. package/dist/plugins/devices/adapters/compute-adapter.d.ts.map +1 -0
  20. package/dist/plugins/devices/adapters/compute-adapter.js +100 -0
  21. package/dist/plugins/devices/adapters/compute-adapter.js.map +1 -0
  22. package/dist/plugins/devices/adapters/index.d.ts +12 -0
  23. package/dist/plugins/devices/adapters/index.d.ts.map +1 -0
  24. package/dist/plugins/devices/adapters/index.js +10 -0
  25. package/dist/plugins/devices/adapters/index.js.map +1 -0
  26. package/dist/plugins/devices/adapters/mobile-adapter.d.ts +41 -0
  27. package/dist/plugins/devices/adapters/mobile-adapter.d.ts.map +1 -0
  28. package/dist/plugins/devices/adapters/mobile-adapter.js +131 -0
  29. package/dist/plugins/devices/adapters/mobile-adapter.js.map +1 -0
  30. package/dist/plugins/devices/devices-plugin.d.ts +70 -0
  31. package/dist/plugins/devices/devices-plugin.d.ts.map +1 -0
  32. package/dist/plugins/devices/devices-plugin.js +453 -0
  33. package/dist/plugins/devices/devices-plugin.js.map +1 -0
  34. package/dist/plugins/devices/index.d.ts +18 -0
  35. package/dist/plugins/devices/index.d.ts.map +1 -0
  36. package/dist/plugins/devices/index.js +18 -0
  37. package/dist/plugins/devices/index.js.map +1 -0
  38. package/dist/plugins/devices/stores/index.d.ts +9 -0
  39. package/dist/plugins/devices/stores/index.d.ts.map +1 -0
  40. package/dist/plugins/devices/stores/index.js +9 -0
  41. package/dist/plugins/devices/stores/index.js.map +1 -0
  42. package/dist/plugins/devices/stores/postgres-store.d.ts +26 -0
  43. package/dist/plugins/devices/stores/postgres-store.d.ts.map +1 -0
  44. package/dist/plugins/devices/stores/postgres-store.js +199 -0
  45. package/dist/plugins/devices/stores/postgres-store.js.map +1 -0
  46. package/dist/plugins/devices/token-utils.d.ts +100 -0
  47. package/dist/plugins/devices/token-utils.d.ts.map +1 -0
  48. package/dist/plugins/devices/token-utils.js +162 -0
  49. package/dist/plugins/devices/token-utils.js.map +1 -0
  50. package/dist/plugins/devices/types.d.ts +307 -0
  51. package/dist/plugins/devices/types.d.ts.map +1 -0
  52. package/dist/plugins/devices/types.js +10 -0
  53. package/dist/plugins/devices/types.js.map +1 -0
  54. package/dist/plugins/index.d.ts +14 -2
  55. package/dist/plugins/index.d.ts.map +1 -1
  56. package/dist/plugins/index.js +13 -1
  57. package/dist/plugins/index.js.map +1 -1
  58. package/dist/plugins/notifications/__tests__/notifications-manager.test.d.ts +5 -0
  59. package/dist/plugins/notifications/__tests__/notifications-manager.test.d.ts.map +1 -0
  60. package/dist/plugins/notifications/__tests__/notifications-manager.test.js +470 -0
  61. package/dist/plugins/notifications/__tests__/notifications-manager.test.js.map +1 -0
  62. package/dist/plugins/notifications/index.d.ts +71 -0
  63. package/dist/plugins/notifications/index.d.ts.map +1 -0
  64. package/dist/plugins/notifications/index.js +72 -0
  65. package/dist/plugins/notifications/index.js.map +1 -0
  66. package/dist/plugins/notifications/notifications-manager.d.ts +182 -0
  67. package/dist/plugins/notifications/notifications-manager.d.ts.map +1 -0
  68. package/dist/plugins/notifications/notifications-manager.js +610 -0
  69. package/dist/plugins/notifications/notifications-manager.js.map +1 -0
  70. package/dist/plugins/notifications/notifications-plugin.d.ts +83 -0
  71. package/dist/plugins/notifications/notifications-plugin.d.ts.map +1 -0
  72. package/dist/plugins/notifications/notifications-plugin.js +337 -0
  73. package/dist/plugins/notifications/notifications-plugin.js.map +1 -0
  74. package/dist/plugins/notifications/types.d.ts +164 -0
  75. package/dist/plugins/notifications/types.d.ts.map +1 -0
  76. package/dist/plugins/notifications/types.js +9 -0
  77. package/dist/plugins/notifications/types.js.map +1 -0
  78. package/dist/plugins/parental/__tests__/parental-plugin.test.d.ts +12 -0
  79. package/dist/plugins/parental/__tests__/parental-plugin.test.d.ts.map +1 -0
  80. package/dist/plugins/parental/__tests__/parental-plugin.test.js +349 -0
  81. package/dist/plugins/parental/__tests__/parental-plugin.test.js.map +1 -0
  82. package/dist/plugins/parental/adapters/index.d.ts +8 -0
  83. package/dist/plugins/parental/adapters/index.d.ts.map +1 -0
  84. package/dist/plugins/parental/adapters/index.js +7 -0
  85. package/dist/plugins/parental/adapters/index.js.map +1 -0
  86. package/dist/plugins/parental/adapters/kids-adapter.d.ts +24 -0
  87. package/dist/plugins/parental/adapters/kids-adapter.d.ts.map +1 -0
  88. package/dist/plugins/parental/adapters/kids-adapter.js +174 -0
  89. package/dist/plugins/parental/adapters/kids-adapter.js.map +1 -0
  90. package/dist/plugins/parental/index.d.ts +14 -0
  91. package/dist/plugins/parental/index.d.ts.map +1 -0
  92. package/dist/plugins/parental/index.js +15 -0
  93. package/dist/plugins/parental/index.js.map +1 -0
  94. package/dist/plugins/parental/parental-plugin.d.ts +88 -0
  95. package/dist/plugins/parental/parental-plugin.d.ts.map +1 -0
  96. package/dist/plugins/parental/parental-plugin.js +666 -0
  97. package/dist/plugins/parental/parental-plugin.js.map +1 -0
  98. package/dist/plugins/parental/stores/index.d.ts +7 -0
  99. package/dist/plugins/parental/stores/index.d.ts.map +1 -0
  100. package/dist/plugins/parental/stores/index.js +7 -0
  101. package/dist/plugins/parental/stores/index.js.map +1 -0
  102. package/dist/plugins/parental/stores/postgres-store.d.ts +10 -0
  103. package/dist/plugins/parental/stores/postgres-store.d.ts.map +1 -0
  104. package/dist/plugins/parental/stores/postgres-store.js +209 -0
  105. package/dist/plugins/parental/stores/postgres-store.js.map +1 -0
  106. package/dist/plugins/parental/types.d.ts +154 -0
  107. package/dist/plugins/parental/types.d.ts.map +1 -0
  108. package/dist/plugins/parental/types.js +10 -0
  109. package/dist/plugins/parental/types.js.map +1 -0
  110. package/dist/plugins/profiles/__tests__/profiles-plugin.test.d.ts +11 -0
  111. package/dist/plugins/profiles/__tests__/profiles-plugin.test.d.ts.map +1 -0
  112. package/dist/plugins/profiles/__tests__/profiles-plugin.test.js +243 -0
  113. package/dist/plugins/profiles/__tests__/profiles-plugin.test.js.map +1 -0
  114. package/dist/plugins/profiles/index.d.ts +12 -0
  115. package/dist/plugins/profiles/index.d.ts.map +1 -0
  116. package/dist/plugins/profiles/index.js +13 -0
  117. package/dist/plugins/profiles/index.js.map +1 -0
  118. package/dist/plugins/profiles/profiles-plugin.d.ts +71 -0
  119. package/dist/plugins/profiles/profiles-plugin.d.ts.map +1 -0
  120. package/dist/plugins/profiles/profiles-plugin.js +481 -0
  121. package/dist/plugins/profiles/profiles-plugin.js.map +1 -0
  122. package/dist/plugins/profiles/stores/index.d.ts +9 -0
  123. package/dist/plugins/profiles/stores/index.d.ts.map +1 -0
  124. package/dist/plugins/profiles/stores/index.js +9 -0
  125. package/dist/plugins/profiles/stores/index.js.map +1 -0
  126. package/dist/plugins/profiles/stores/postgres-store.d.ts +18 -0
  127. package/dist/plugins/profiles/stores/postgres-store.d.ts.map +1 -0
  128. package/dist/plugins/profiles/stores/postgres-store.js +310 -0
  129. package/dist/plugins/profiles/stores/postgres-store.js.map +1 -0
  130. package/dist/plugins/profiles/types.d.ts +289 -0
  131. package/dist/plugins/profiles/types.d.ts.map +1 -0
  132. package/dist/plugins/profiles/types.js +10 -0
  133. package/dist/plugins/profiles/types.js.map +1 -0
  134. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.d.ts +11 -0
  135. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.d.ts.map +1 -0
  136. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.js +305 -0
  137. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.js.map +1 -0
  138. package/dist/plugins/subscriptions/index.d.ts +12 -0
  139. package/dist/plugins/subscriptions/index.d.ts.map +1 -0
  140. package/dist/plugins/subscriptions/index.js +13 -0
  141. package/dist/plugins/subscriptions/index.js.map +1 -0
  142. package/dist/plugins/subscriptions/stores/index.d.ts +9 -0
  143. package/dist/plugins/subscriptions/stores/index.d.ts.map +1 -0
  144. package/dist/plugins/subscriptions/stores/index.js +9 -0
  145. package/dist/plugins/subscriptions/stores/index.js.map +1 -0
  146. package/dist/plugins/subscriptions/stores/postgres-store.d.ts +14 -0
  147. package/dist/plugins/subscriptions/stores/postgres-store.d.ts.map +1 -0
  148. package/dist/plugins/subscriptions/stores/postgres-store.js +359 -0
  149. package/dist/plugins/subscriptions/stores/postgres-store.js.map +1 -0
  150. package/dist/plugins/subscriptions/subscriptions-plugin.d.ts +82 -0
  151. package/dist/plugins/subscriptions/subscriptions-plugin.d.ts.map +1 -0
  152. package/dist/plugins/subscriptions/subscriptions-plugin.js +449 -0
  153. package/dist/plugins/subscriptions/subscriptions-plugin.js.map +1 -0
  154. package/dist/plugins/subscriptions/types.d.ts +308 -0
  155. package/dist/plugins/subscriptions/types.d.ts.map +1 -0
  156. package/dist/plugins/subscriptions/types.js +10 -0
  157. package/dist/plugins/subscriptions/types.js.map +1 -0
  158. package/dist/plugins/usage/__tests__/usage-plugin.test.d.ts +11 -0
  159. package/dist/plugins/usage/__tests__/usage-plugin.test.d.ts.map +1 -0
  160. package/dist/plugins/usage/__tests__/usage-plugin.test.js +218 -0
  161. package/dist/plugins/usage/__tests__/usage-plugin.test.js.map +1 -0
  162. package/dist/plugins/usage/index.d.ts +12 -0
  163. package/dist/plugins/usage/index.d.ts.map +1 -0
  164. package/dist/plugins/usage/index.js +13 -0
  165. package/dist/plugins/usage/index.js.map +1 -0
  166. package/dist/plugins/usage/stores/index.d.ts +9 -0
  167. package/dist/plugins/usage/stores/index.d.ts.map +1 -0
  168. package/dist/plugins/usage/stores/index.js +9 -0
  169. package/dist/plugins/usage/stores/index.js.map +1 -0
  170. package/dist/plugins/usage/stores/postgres-store.d.ts +14 -0
  171. package/dist/plugins/usage/stores/postgres-store.d.ts.map +1 -0
  172. package/dist/plugins/usage/stores/postgres-store.js +146 -0
  173. package/dist/plugins/usage/stores/postgres-store.js.map +1 -0
  174. package/dist/plugins/usage/types.d.ts +195 -0
  175. package/dist/plugins/usage/types.d.ts.map +1 -0
  176. package/dist/plugins/usage/types.js +10 -0
  177. package/dist/plugins/usage/types.js.map +1 -0
  178. package/dist/plugins/usage/usage-plugin.d.ts +51 -0
  179. package/dist/plugins/usage/usage-plugin.d.ts.map +1 -0
  180. package/dist/plugins/usage/usage-plugin.js +412 -0
  181. package/dist/plugins/usage/usage-plugin.js.map +1 -0
  182. package/dist/plugins/users/__tests__/postgres-store.test.d.ts +10 -0
  183. package/dist/plugins/users/__tests__/postgres-store.test.d.ts.map +1 -0
  184. package/dist/plugins/users/__tests__/postgres-store.test.js +229 -0
  185. package/dist/plugins/users/__tests__/postgres-store.test.js.map +1 -0
  186. package/dist/plugins/users/__tests__/users-plugin.test.js +3 -0
  187. package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -1
  188. package/dist/plugins/users/index.d.ts +2 -2
  189. package/dist/plugins/users/index.d.ts.map +1 -1
  190. package/dist/plugins/users/index.js +1 -1
  191. package/dist/plugins/users/index.js.map +1 -1
  192. package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -1
  193. package/dist/plugins/users/stores/postgres-store.js +76 -0
  194. package/dist/plugins/users/stores/postgres-store.js.map +1 -1
  195. package/dist/plugins/users/types.d.ts +74 -6
  196. package/dist/plugins/users/types.d.ts.map +1 -1
  197. package/dist/plugins/users/users-plugin.d.ts +15 -1
  198. package/dist/plugins/users/users-plugin.d.ts.map +1 -1
  199. package/dist/plugins/users/users-plugin.js +29 -0
  200. package/dist/plugins/users/users-plugin.js.map +1 -1
  201. package/dist-ui/assets/index-CynOqPkb.js +469 -0
  202. package/dist-ui/assets/index-CynOqPkb.js.map +1 -0
  203. package/dist-ui/index.html +1 -1
  204. package/dist-ui-lib/api/controlPanelApi.d.ts +46 -0
  205. package/dist-ui-lib/components/StatCard.d.ts +16 -0
  206. package/dist-ui-lib/dashboard/widgets/NotificationsStatsWidget.d.ts +12 -0
  207. package/dist-ui-lib/dashboard/widgets/index.d.ts +1 -0
  208. package/dist-ui-lib/index.js +1822 -1611
  209. package/dist-ui-lib/index.js.map +1 -1
  210. package/dist-ui-lib/pages/NotificationsPage.d.ts +9 -0
  211. package/dist-ui-lib/utils/formatters.d.ts +19 -0
  212. package/package.json +3 -2
  213. package/src/index.ts +178 -0
  214. package/src/plugins/bans/bans-plugin.ts +15 -3
  215. package/src/plugins/devices/__tests__/devices-plugin.test.ts +551 -0
  216. package/src/plugins/devices/__tests__/token-utils.test.ts +264 -0
  217. package/src/plugins/devices/adapters/compute-adapter.ts +139 -0
  218. package/src/plugins/devices/adapters/index.ts +13 -0
  219. package/src/plugins/devices/adapters/mobile-adapter.ts +179 -0
  220. package/src/plugins/devices/devices-plugin.ts +538 -0
  221. package/src/plugins/devices/index.ts +69 -0
  222. package/src/plugins/devices/stores/index.ts +9 -0
  223. package/src/plugins/devices/stores/postgres-store.ts +304 -0
  224. package/src/plugins/devices/token-utils.ts +213 -0
  225. package/src/plugins/devices/types.ts +351 -0
  226. package/src/plugins/index.ts +218 -0
  227. package/src/plugins/notifications/__tests__/notifications-manager.test.ts +637 -0
  228. package/src/plugins/notifications/index.ts +91 -0
  229. package/src/plugins/notifications/notifications-manager.ts +773 -0
  230. package/src/plugins/notifications/notifications-plugin.ts +398 -0
  231. package/src/plugins/notifications/types.ts +207 -0
  232. package/src/plugins/parental/__tests__/parental-plugin.test.ts +465 -0
  233. package/src/plugins/parental/adapters/index.ts +8 -0
  234. package/src/plugins/parental/adapters/kids-adapter.ts +206 -0
  235. package/src/plugins/parental/index.ts +55 -0
  236. package/src/plugins/parental/parental-plugin.ts +759 -0
  237. package/src/plugins/parental/stores/index.ts +7 -0
  238. package/src/plugins/parental/stores/postgres-store.ts +304 -0
  239. package/src/plugins/parental/types.ts +180 -0
  240. package/src/plugins/profiles/__tests__/profiles-plugin.test.ts +321 -0
  241. package/src/plugins/profiles/index.ts +49 -0
  242. package/src/plugins/profiles/profiles-plugin.ts +546 -0
  243. package/src/plugins/profiles/stores/index.ts +9 -0
  244. package/src/plugins/profiles/stores/postgres-store.ts +439 -0
  245. package/src/plugins/profiles/types.ts +338 -0
  246. package/src/plugins/subscriptions/__tests__/subscriptions-plugin.test.ts +404 -0
  247. package/src/plugins/subscriptions/index.ts +51 -0
  248. package/src/plugins/subscriptions/stores/index.ts +9 -0
  249. package/src/plugins/subscriptions/stores/postgres-store.ts +482 -0
  250. package/src/plugins/subscriptions/subscriptions-plugin.ts +530 -0
  251. package/src/plugins/subscriptions/types.ts +355 -0
  252. package/src/plugins/usage/__tests__/usage-plugin.test.ts +288 -0
  253. package/src/plugins/usage/index.ts +39 -0
  254. package/src/plugins/usage/stores/index.ts +9 -0
  255. package/src/plugins/usage/stores/postgres-store.ts +213 -0
  256. package/src/plugins/usage/types.ts +222 -0
  257. package/src/plugins/usage/usage-plugin.ts +484 -0
  258. package/src/plugins/users/__tests__/postgres-store.test.ts +326 -0
  259. package/src/plugins/users/__tests__/users-plugin.test.ts +3 -0
  260. package/src/plugins/users/index.ts +6 -0
  261. package/src/plugins/users/stores/postgres-store.ts +104 -0
  262. package/src/plugins/users/types.ts +82 -6
  263. package/src/plugins/users/users-plugin.ts +37 -0
  264. package/ui/src/App.tsx +5 -1
  265. package/ui/src/api/controlPanelApi.ts +103 -6
  266. package/ui/src/components/StatCard.tsx +58 -0
  267. package/ui/src/dashboard/builtInWidgets.tsx +3 -1
  268. package/ui/src/dashboard/widgets/NotificationsStatsWidget.tsx +167 -0
  269. package/ui/src/dashboard/widgets/index.ts +1 -0
  270. package/ui/src/pages/NotificationsPage.tsx +417 -0
  271. package/ui/src/utils/formatters.ts +33 -0
  272. package/dist-ui/assets/index-D7DoZ9rL.js +0 -478
  273. package/dist-ui/assets/index-D7DoZ9rL.js.map +0 -1
@@ -0,0 +1,417 @@
1
+ /**
2
+ * Notifications Page
3
+ *
4
+ * Full management page for the Notifications Plugin.
5
+ * Shows connection stats, connected clients table, and management actions.
6
+ *
7
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
+ */
9
+
10
+ import { useState, useEffect, useRef, useCallback } from 'react';
11
+ import {
12
+ Box,
13
+ Card,
14
+ CardContent,
15
+ Typography,
16
+ Table,
17
+ TableBody,
18
+ TableCell,
19
+ TableContainer,
20
+ TableHead,
21
+ TableRow,
22
+ Paper,
23
+ Button,
24
+ IconButton,
25
+ Tooltip,
26
+ Chip,
27
+ Dialog,
28
+ DialogTitle,
29
+ DialogContent,
30
+ DialogContentText,
31
+ DialogActions,
32
+ CircularProgress,
33
+ Alert,
34
+ } from '@mui/material';
35
+ import RefreshIcon from '@mui/icons-material/Refresh';
36
+ import WifiIcon from '@mui/icons-material/Wifi';
37
+ import WifiOffIcon from '@mui/icons-material/WifiOff';
38
+ import DevicesIcon from '@mui/icons-material/Devices';
39
+ import PersonIcon from '@mui/icons-material/Person';
40
+ import SendIcon from '@mui/icons-material/Send';
41
+ import ErrorIcon from '@mui/icons-material/Error';
42
+ import NotificationsIcon from '@mui/icons-material/Notifications';
43
+ import DisconnectIcon from '@mui/icons-material/LinkOff';
44
+ import RestartAltIcon from '@mui/icons-material/RestartAlt';
45
+ import { GridLayout } from '@qwickapps/react-framework';
46
+ import {
47
+ api,
48
+ type NotificationsStatsResponse,
49
+ type NotificationsClient,
50
+ } from '../api/controlPanelApi';
51
+ import { StatCard } from '../components/StatCard';
52
+ import { formatDuration, formatNumber, truncateId } from '../utils/formatters';
53
+
54
+ export function NotificationsPage() {
55
+ const [stats, setStats] = useState<NotificationsStatsResponse | null>(null);
56
+ const [clients, setClients] = useState<NotificationsClient[]>([]);
57
+ const [loading, setLoading] = useState(true);
58
+ const [error, setError] = useState<string | null>(null);
59
+ const [success, setSuccess] = useState<string | null>(null);
60
+ const [disconnectingId, setDisconnectingId] = useState<string | null>(null);
61
+ const [reconnecting, setReconnecting] = useState(false);
62
+ const [confirmDisconnect, setConfirmDisconnect] = useState<NotificationsClient | null>(null);
63
+
64
+ // Track mounted state to prevent state updates after unmount
65
+ const isMountedRef = useRef(true);
66
+
67
+ const fetchData = useCallback(async () => {
68
+ try {
69
+ const [statsData, clientsData] = await Promise.all([
70
+ api.getNotificationsStats(),
71
+ api.getNotificationsClients(),
72
+ ]);
73
+ // Only update state if component is still mounted
74
+ if (isMountedRef.current) {
75
+ setStats(statsData);
76
+ setClients(clientsData.clients);
77
+ setError(null);
78
+ }
79
+ } catch (err) {
80
+ if (isMountedRef.current) {
81
+ if (err instanceof Error && err.message.includes('404')) {
82
+ setError('Notifications plugin not enabled');
83
+ } else {
84
+ setError(err instanceof Error ? err.message : 'Failed to fetch data');
85
+ }
86
+ }
87
+ } finally {
88
+ if (isMountedRef.current) {
89
+ setLoading(false);
90
+ }
91
+ }
92
+ }, []);
93
+
94
+ useEffect(() => {
95
+ isMountedRef.current = true;
96
+ fetchData();
97
+ const interval = setInterval(fetchData, 5000);
98
+ return () => {
99
+ isMountedRef.current = false;
100
+ clearInterval(interval);
101
+ };
102
+ }, [fetchData]);
103
+
104
+ const handleDisconnect = async (client: NotificationsClient) => {
105
+ setDisconnectingId(client.id);
106
+ setConfirmDisconnect(null);
107
+ try {
108
+ await api.disconnectNotificationsClient(client.id);
109
+ if (isMountedRef.current) {
110
+ setSuccess(`Client ${truncateId(client.id)} disconnected`);
111
+ setTimeout(() => {
112
+ if (isMountedRef.current) setSuccess(null);
113
+ }, 3000);
114
+ await fetchData();
115
+ }
116
+ } catch (err) {
117
+ if (isMountedRef.current) {
118
+ setError(err instanceof Error ? err.message : 'Failed to disconnect client');
119
+ }
120
+ } finally {
121
+ if (isMountedRef.current) {
122
+ setDisconnectingId(null);
123
+ }
124
+ }
125
+ };
126
+
127
+ const handleForceReconnect = async () => {
128
+ setReconnecting(true);
129
+ try {
130
+ const result = await api.forceNotificationsReconnect();
131
+ if (isMountedRef.current) {
132
+ setSuccess(result.message);
133
+ setTimeout(() => {
134
+ if (isMountedRef.current) setSuccess(null);
135
+ }, 3000);
136
+ await fetchData();
137
+ }
138
+ } catch (err) {
139
+ if (isMountedRef.current) {
140
+ setError(err instanceof Error ? err.message : 'Failed to force reconnect');
141
+ }
142
+ } finally {
143
+ if (isMountedRef.current) {
144
+ setReconnecting(false);
145
+ }
146
+ }
147
+ };
148
+
149
+ if (loading) {
150
+ return (
151
+ <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '50vh' }}>
152
+ <CircularProgress />
153
+ </Box>
154
+ );
155
+ }
156
+
157
+ if (error && !stats) {
158
+ return (
159
+ <Card sx={{ bgcolor: 'var(--theme-surface)', border: '1px solid var(--theme-border)' }}>
160
+ <CardContent>
161
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
162
+ <WifiOffIcon sx={{ color: 'var(--theme-text-secondary)' }} />
163
+ <Typography sx={{ color: 'var(--theme-text-secondary)' }}>{error}</Typography>
164
+ </Box>
165
+ </CardContent>
166
+ </Card>
167
+ );
168
+ }
169
+
170
+ const isHealthy = stats?.connectionHealth.isHealthy ?? false;
171
+ const healthColor = isHealthy ? 'var(--theme-success)' : 'var(--theme-warning)';
172
+
173
+ return (
174
+ <Box>
175
+ {/* Page Header */}
176
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
177
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
178
+ <NotificationsIcon sx={{ color: 'var(--theme-primary)', fontSize: 32 }} />
179
+ <Typography variant="h4" sx={{ color: 'var(--theme-text-primary)' }}>
180
+ Notifications
181
+ </Typography>
182
+ </Box>
183
+ <Box sx={{ display: 'flex', gap: 1 }}>
184
+ <Tooltip title="Refresh">
185
+ <IconButton onClick={fetchData} sx={{ color: 'var(--theme-text-secondary)' }}>
186
+ <RefreshIcon />
187
+ </IconButton>
188
+ </Tooltip>
189
+ <Button
190
+ variant="outlined"
191
+ startIcon={reconnecting ? <CircularProgress size={16} /> : <RestartAltIcon />}
192
+ onClick={handleForceReconnect}
193
+ disabled={reconnecting}
194
+ sx={{ borderColor: 'var(--theme-border)' }}
195
+ >
196
+ {reconnecting ? 'Reconnecting...' : 'Force Reconnect'}
197
+ </Button>
198
+ </Box>
199
+ </Box>
200
+ <Typography variant="body2" sx={{ mb: 4, color: 'var(--theme-text-secondary)' }}>
201
+ Manage realtime notification connections and SSE clients
202
+ </Typography>
203
+
204
+ {/* Success/Error messages */}
205
+ {success && <Alert severity="success" sx={{ mb: 3 }}>{success}</Alert>}
206
+ {error && stats && <Alert severity="error" sx={{ mb: 3 }}>{error}</Alert>}
207
+
208
+ {/* Connection Status Bar */}
209
+ <Card sx={{ bgcolor: 'var(--theme-surface)', mb: 3 }}>
210
+ <CardContent sx={{ py: 1.5, '&:last-child': { pb: 1.5 } }}>
211
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
212
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
213
+ {isHealthy ? (
214
+ <WifiIcon sx={{ color: healthColor, fontSize: 20 }} />
215
+ ) : (
216
+ <WifiOffIcon sx={{ color: healthColor, fontSize: 20 }} />
217
+ )}
218
+ <Typography sx={{ color: healthColor, fontWeight: 500 }}>
219
+ {isHealthy ? 'Connected' : 'Reconnecting...'}
220
+ </Typography>
221
+ {stats?.connectionHealth.isReconnecting && (
222
+ <Chip
223
+ label={`Attempt ${stats.connectionHealth.reconnectAttempts}`}
224
+ size="small"
225
+ sx={{
226
+ bgcolor: 'var(--theme-warning)20',
227
+ color: 'var(--theme-warning)',
228
+ fontSize: '0.7rem',
229
+ height: 18,
230
+ }}
231
+ />
232
+ )}
233
+ </Box>
234
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
235
+ <Typography variant="caption" sx={{ color: 'var(--theme-text-secondary)' }}>
236
+ {stats?.channels.length} channel{stats?.channels.length !== 1 ? 's' : ''}
237
+ </Typography>
238
+ {stats?.lastEventAt && (
239
+ <Typography variant="caption" sx={{ color: 'var(--theme-text-secondary)' }}>
240
+ Last event: {formatDuration(stats.connectionHealth.timeSinceLastEvent)} ago
241
+ </Typography>
242
+ )}
243
+ </Box>
244
+ </Box>
245
+ </CardContent>
246
+ </Card>
247
+
248
+ {/* Stats Grid */}
249
+ <GridLayout columns={4} spacing="small" equalHeight>
250
+ <StatCard
251
+ icon={<DevicesIcon sx={{ fontSize: 28 }} />}
252
+ label="Active Clients"
253
+ value={stats?.currentConnections ?? 0}
254
+ subValue={`${stats?.totalConnections ?? 0} total`}
255
+ color="var(--theme-primary)"
256
+ />
257
+ <StatCard
258
+ icon={<PersonIcon sx={{ fontSize: 28 }} />}
259
+ label="By Device"
260
+ value={stats?.clientsByType.device ?? 0}
261
+ subValue={`${stats?.clientsByType.user ?? 0} by user`}
262
+ color="var(--theme-info)"
263
+ />
264
+ <StatCard
265
+ icon={<SendIcon sx={{ fontSize: 28 }} />}
266
+ label="Events Routed"
267
+ value={formatNumber(stats?.eventsRouted ?? 0)}
268
+ subValue={`${formatNumber(stats?.eventsProcessed ?? 0)} processed`}
269
+ color="var(--theme-success)"
270
+ />
271
+ <StatCard
272
+ icon={<ErrorIcon sx={{ fontSize: 28 }} />}
273
+ label="Dropped"
274
+ value={formatNumber(stats?.eventsDroppedNoClients ?? 0)}
275
+ subValue={`${stats?.eventsParseFailed ?? 0} parse errors`}
276
+ color={(stats?.eventsDroppedNoClients ?? 0) > 0 ? 'var(--theme-warning)' : 'var(--theme-text-secondary)'}
277
+ />
278
+ </GridLayout>
279
+
280
+ {/* Clients Table */}
281
+ <Card sx={{ bgcolor: 'var(--theme-surface)', mt: 3 }}>
282
+ <CardContent>
283
+ <Typography variant="h6" sx={{ color: 'var(--theme-text-primary)', mb: 2 }}>
284
+ Connected Clients
285
+ </Typography>
286
+
287
+ {clients.length === 0 ? (
288
+ <Typography sx={{ color: 'var(--theme-text-secondary)', py: 4, textAlign: 'center' }}>
289
+ No clients currently connected
290
+ </Typography>
291
+ ) : (
292
+ <TableContainer component={Paper} sx={{ bgcolor: 'transparent', boxShadow: 'none' }}>
293
+ <Table size="small">
294
+ <TableHead>
295
+ <TableRow>
296
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
297
+ Client ID
298
+ </TableCell>
299
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
300
+ Device ID
301
+ </TableCell>
302
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
303
+ User ID
304
+ </TableCell>
305
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
306
+ Connected
307
+ </TableCell>
308
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
309
+ Duration
310
+ </TableCell>
311
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }} align="right">
312
+ Actions
313
+ </TableCell>
314
+ </TableRow>
315
+ </TableHead>
316
+ <TableBody>
317
+ {clients.map((client) => (
318
+ <TableRow key={client.id} hover>
319
+ <TableCell sx={{ color: 'var(--theme-text-primary)', borderColor: 'var(--theme-border)' }}>
320
+ <Tooltip title={client.id}>
321
+ <code style={{ fontSize: '0.85em' }}>{truncateId(client.id)}</code>
322
+ </Tooltip>
323
+ </TableCell>
324
+ <TableCell sx={{ color: 'var(--theme-text-primary)', borderColor: 'var(--theme-border)' }}>
325
+ {client.deviceId ? (
326
+ <Tooltip title={client.deviceId}>
327
+ <Chip
328
+ size="small"
329
+ icon={<DevicesIcon sx={{ fontSize: 14 }} />}
330
+ label={truncateId(client.deviceId)}
331
+ sx={{ bgcolor: 'var(--theme-primary)20', color: 'var(--theme-primary)' }}
332
+ />
333
+ </Tooltip>
334
+ ) : (
335
+ <Typography variant="caption" sx={{ color: 'var(--theme-text-secondary)' }}>—</Typography>
336
+ )}
337
+ </TableCell>
338
+ <TableCell sx={{ color: 'var(--theme-text-primary)', borderColor: 'var(--theme-border)' }}>
339
+ {client.userId ? (
340
+ <Tooltip title={client.userId}>
341
+ <Chip
342
+ size="small"
343
+ icon={<PersonIcon sx={{ fontSize: 14 }} />}
344
+ label={truncateId(client.userId)}
345
+ sx={{ bgcolor: 'var(--theme-info)20', color: 'var(--theme-info)' }}
346
+ />
347
+ </Tooltip>
348
+ ) : (
349
+ <Typography variant="caption" sx={{ color: 'var(--theme-text-secondary)' }}>—</Typography>
350
+ )}
351
+ </TableCell>
352
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
353
+ {new Date(client.connectedAt).toLocaleTimeString()}
354
+ </TableCell>
355
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
356
+ {formatDuration(client.durationMs)}
357
+ </TableCell>
358
+ <TableCell sx={{ borderColor: 'var(--theme-border)' }} align="right">
359
+ <Tooltip title="Disconnect client">
360
+ <IconButton
361
+ size="small"
362
+ onClick={() => setConfirmDisconnect(client)}
363
+ disabled={disconnectingId === client.id}
364
+ sx={{ color: 'var(--theme-error)' }}
365
+ >
366
+ {disconnectingId === client.id ? (
367
+ <CircularProgress size={18} />
368
+ ) : (
369
+ <DisconnectIcon fontSize="small" />
370
+ )}
371
+ </IconButton>
372
+ </Tooltip>
373
+ </TableCell>
374
+ </TableRow>
375
+ ))}
376
+ </TableBody>
377
+ </Table>
378
+ </TableContainer>
379
+ )}
380
+ </CardContent>
381
+ </Card>
382
+
383
+ {/* Confirmation Dialog */}
384
+ <Dialog
385
+ open={!!confirmDisconnect}
386
+ onClose={() => setConfirmDisconnect(null)}
387
+ aria-labelledby="disconnect-dialog-title"
388
+ >
389
+ <DialogTitle id="disconnect-dialog-title">Disconnect Client?</DialogTitle>
390
+ <DialogContent>
391
+ <DialogContentText>
392
+ Are you sure you want to disconnect this client?
393
+ {confirmDisconnect?.deviceId && (
394
+ <><br /><strong>Device:</strong> {truncateId(confirmDisconnect.deviceId)}</>
395
+ )}
396
+ {confirmDisconnect?.userId && (
397
+ <><br /><strong>User:</strong> {truncateId(confirmDisconnect.userId)}</>
398
+ )}
399
+ <br /><br />
400
+ The client will receive a disconnect event and the connection will be closed.
401
+ The client may automatically reconnect.
402
+ </DialogContentText>
403
+ </DialogContent>
404
+ <DialogActions>
405
+ <Button onClick={() => setConfirmDisconnect(null)}>Cancel</Button>
406
+ <Button
407
+ onClick={() => confirmDisconnect && handleDisconnect(confirmDisconnect)}
408
+ color="error"
409
+ autoFocus
410
+ >
411
+ Disconnect
412
+ </Button>
413
+ </DialogActions>
414
+ </Dialog>
415
+ </Box>
416
+ );
417
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Formatting Utilities
3
+ *
4
+ * Common formatting functions for numbers, durations, and strings.
5
+ *
6
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
7
+ */
8
+
9
+ /**
10
+ * Format a number with K/M suffix for large values
11
+ */
12
+ export function formatNumber(num: number): string {
13
+ if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
14
+ if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
15
+ return num.toString();
16
+ }
17
+
18
+ /**
19
+ * Format milliseconds to human-readable duration
20
+ */
21
+ export function formatDuration(ms: number): string {
22
+ if (ms < 1000) return `${ms}ms`;
23
+ if (ms < 60000) return `${(ms / 1000).toFixed(0)}s`;
24
+ if (ms < 3600000) return `${(ms / 60000).toFixed(0)}m`;
25
+ return `${(ms / 3600000).toFixed(1)}h`;
26
+ }
27
+
28
+ /**
29
+ * Truncate a string ID for display
30
+ */
31
+ export function truncateId(id: string, maxLength = 12): string {
32
+ return id.length > maxLength ? `${id.substring(0, maxLength)}...` : id;
33
+ }