@strapi-community/plugin-io 5.2.0 → 5.3.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.
@@ -0,0 +1,339 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useState, useRef, useCallback, useEffect } from "react";
3
+ import { Typography, Flex, Badge, Box } from "@strapi/design-system";
4
+ import { Pencil, User, Clock } from "@strapi/icons";
5
+ import { useFetchClient } from "@strapi/strapi/admin";
6
+ import styled, { keyframes } from "styled-components";
7
+ import { io } from "socket.io-client";
8
+ import { P as PLUGIN_ID } from "./index-Bw7WjN5H.mjs";
9
+ const pulse = keyframes`
10
+ 0%, 100% { opacity: 1; }
11
+ 50% { opacity: 0.5; }
12
+ `;
13
+ const WidgetContainer = styled(Box)`
14
+ padding: 0;
15
+ position: relative;
16
+ `;
17
+ const HeaderContainer = styled(Flex)`
18
+ justify-content: space-between;
19
+ align-items: center;
20
+ margin-bottom: ${({ theme }) => theme.spaces[3]};
21
+ padding-bottom: ${({ theme }) => theme.spaces[2]};
22
+ border-bottom: 1px solid ${({ theme }) => theme.colors.neutral150};
23
+ `;
24
+ const LiveDot = styled.span`
25
+ display: inline-block;
26
+ width: 8px;
27
+ height: 8px;
28
+ border-radius: 50%;
29
+ background: ${({ theme, $connected }) => $connected ? theme.colors.success500 : theme.colors.neutral400};
30
+ margin-right: ${({ theme }) => theme.spaces[2]};
31
+ animation: ${({ $connected }) => $connected ? pulse : "none"} 2s ease-in-out infinite;
32
+ `;
33
+ const CountBadge = styled.span`
34
+ display: inline-flex;
35
+ align-items: center;
36
+ justify-content: center;
37
+ min-width: 24px;
38
+ height: 24px;
39
+ padding: 0 8px;
40
+ background: ${({ theme, $active }) => $active ? theme.colors.primary100 : theme.colors.neutral100};
41
+ color: ${({ theme, $active }) => $active ? theme.colors.primary700 : theme.colors.neutral600};
42
+ border-radius: 12px;
43
+ font-size: 12px;
44
+ font-weight: 600;
45
+ `;
46
+ const UserList = styled.div`
47
+ display: flex;
48
+ flex-direction: column;
49
+ gap: ${({ theme }) => theme.spaces[2]};
50
+ max-height: 280px;
51
+ overflow-y: auto;
52
+ `;
53
+ const UserCard = styled.div`
54
+ display: flex;
55
+ align-items: flex-start;
56
+ gap: ${({ theme }) => theme.spaces[3]};
57
+ padding: ${({ theme }) => theme.spaces[3]};
58
+ background: ${({ theme }) => theme.colors.neutral0};
59
+ border: 1px solid ${({ theme }) => theme.colors.neutral150};
60
+ border-radius: ${({ theme }) => theme.borderRadius};
61
+ transition: all 0.2s ease;
62
+
63
+ &:hover {
64
+ border-color: ${({ theme }) => theme.colors.primary200};
65
+ box-shadow: 0 2px 8px rgba(73, 69, 255, 0.08);
66
+ }
67
+ `;
68
+ const AVATAR_COLORS = [
69
+ "linear-gradient(135deg, #4945ff 0%, #7b79ff 100%)",
70
+ "linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%)",
71
+ "linear-gradient(135deg, #10b981 0%, #34d399 100%)",
72
+ "linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%)",
73
+ "linear-gradient(135deg, #ef4444 0%, #f87171 100%)",
74
+ "linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%)"
75
+ ];
76
+ const UserAvatar = styled.div`
77
+ width: 40px;
78
+ height: 40px;
79
+ border-radius: 50%;
80
+ background: ${({ $colorIndex }) => AVATAR_COLORS[$colorIndex % AVATAR_COLORS.length]};
81
+ color: white;
82
+ font-size: 14px;
83
+ font-weight: 700;
84
+ display: flex;
85
+ align-items: center;
86
+ justify-content: center;
87
+ flex-shrink: 0;
88
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
89
+ `;
90
+ const UserInfo = styled.div`
91
+ flex: 1;
92
+ min-width: 0;
93
+ `;
94
+ const UserName = styled.div`
95
+ font-size: 14px;
96
+ font-weight: 600;
97
+ color: ${({ theme }) => theme.colors.neutral800};
98
+ white-space: nowrap;
99
+ overflow: hidden;
100
+ text-overflow: ellipsis;
101
+ `;
102
+ const UserMeta = styled.div`
103
+ font-size: 12px;
104
+ color: ${({ theme }) => theme.colors.neutral500};
105
+ display: flex;
106
+ align-items: center;
107
+ gap: ${({ theme }) => theme.spaces[2]};
108
+ margin-top: 2px;
109
+ `;
110
+ const EditingBadge = styled.a`
111
+ display: inline-flex;
112
+ align-items: center;
113
+ gap: 4px;
114
+ font-size: 11px;
115
+ font-weight: 500;
116
+ color: ${({ theme }) => theme.colors.success700};
117
+ background: ${({ theme }) => theme.colors.success100};
118
+ padding: 4px 10px;
119
+ border-radius: 10px;
120
+ margin-top: ${({ theme }) => theme.spaces[1]};
121
+ word-break: break-all;
122
+ max-width: 100%;
123
+ text-decoration: none;
124
+ cursor: pointer;
125
+ transition: all 0.15s ease;
126
+
127
+ &:hover {
128
+ background: ${({ theme }) => theme.colors.success200};
129
+ color: ${({ theme }) => theme.colors.success800};
130
+ transform: translateY(-1px);
131
+ }
132
+ `;
133
+ const IdleBadge = styled.span`
134
+ display: inline-flex;
135
+ align-items: center;
136
+ gap: 4px;
137
+ font-size: 11px;
138
+ font-weight: 500;
139
+ color: ${({ theme }) => theme.colors.neutral600};
140
+ background: ${({ theme }) => theme.colors.neutral100};
141
+ padding: 2px 8px;
142
+ border-radius: 10px;
143
+ margin-top: ${({ theme }) => theme.spaces[1]};
144
+ `;
145
+ const EmptyState = styled.div`
146
+ display: flex;
147
+ flex-direction: column;
148
+ align-items: center;
149
+ justify-content: center;
150
+ text-align: center;
151
+ padding: ${({ theme }) => theme.spaces[8]} ${({ theme }) => theme.spaces[4]};
152
+ color: ${({ theme }) => theme.colors.neutral500};
153
+ min-height: 180px;
154
+ `;
155
+ const EmptyIcon = styled.div`
156
+ width: 64px;
157
+ height: 64px;
158
+ border-radius: 50%;
159
+ background: ${({ theme }) => theme.colors.neutral100};
160
+ display: flex;
161
+ align-items: center;
162
+ justify-content: center;
163
+ margin-bottom: ${({ theme }) => theme.spaces[3]};
164
+ `;
165
+ const LoadingContainer = styled.div`
166
+ display: flex;
167
+ align-items: center;
168
+ justify-content: center;
169
+ padding: ${({ theme }) => theme.spaces[6]};
170
+ `;
171
+ const FooterLink = styled.a`
172
+ font-size: 12px;
173
+ color: ${({ theme }) => theme.colors.primary600};
174
+ text-decoration: none;
175
+
176
+ &:hover {
177
+ text-decoration: underline;
178
+ }
179
+ `;
180
+ const getInitials = (user) => {
181
+ const first = (user.firstname?.[0] || user.username?.[0] || user.email?.[0] || "?").toUpperCase();
182
+ const last = (user.lastname?.[0] || "").toUpperCase();
183
+ return `${first}${last}`.trim() || "?";
184
+ };
185
+ const getDisplayName = (user) => {
186
+ if (user.firstname) {
187
+ return `${user.firstname} ${user.lastname || ""}`.trim();
188
+ }
189
+ return user.username || user.email || "Unknown";
190
+ };
191
+ const formatDuration = (seconds) => {
192
+ if (seconds < 60) return "just now";
193
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
194
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
195
+ return `${Math.floor(seconds / 86400)}d`;
196
+ };
197
+ const OnlineEditorsWidget = () => {
198
+ const { get, post } = useFetchClient();
199
+ const [data, setData] = useState(null);
200
+ const [loading, setLoading] = useState(true);
201
+ const [error, setError] = useState(null);
202
+ const [connected, setConnected] = useState(false);
203
+ const socketRef = useRef(null);
204
+ const fetchOnlineUsers = useCallback(async () => {
205
+ try {
206
+ const response = await get(`/${PLUGIN_ID}/online-users`);
207
+ setData(response.data?.data || response.data);
208
+ setError(null);
209
+ setLoading(false);
210
+ } catch (err) {
211
+ console.error("[plugin-io] Failed to fetch online users:", err);
212
+ setError(err.message);
213
+ setLoading(false);
214
+ }
215
+ }, [get]);
216
+ useEffect(() => {
217
+ let cancelled = false;
218
+ let socket = null;
219
+ const connectSocket = async () => {
220
+ try {
221
+ const { data: sessionData } = await post(`/${PLUGIN_ID}/presence/session`, {});
222
+ if (cancelled || !sessionData?.token) return;
223
+ const socketUrl = sessionData.wsUrl || `${window.location.protocol}//${window.location.host}`;
224
+ socket = io(socketUrl, {
225
+ path: sessionData.wsPath || "/socket.io",
226
+ transports: ["websocket", "polling"],
227
+ auth: {
228
+ token: sessionData.token,
229
+ strategy: "admin-jwt",
230
+ isAdmin: true
231
+ },
232
+ reconnection: true,
233
+ reconnectionAttempts: 3
234
+ });
235
+ socketRef.current = socket;
236
+ socket.on("connect", () => {
237
+ if (!cancelled) {
238
+ setConnected(true);
239
+ console.log(`[${PLUGIN_ID}] Dashboard presence connected`);
240
+ fetchOnlineUsers();
241
+ }
242
+ });
243
+ socket.on("disconnect", () => {
244
+ if (!cancelled) {
245
+ setConnected(false);
246
+ }
247
+ });
248
+ socket.on("connect_error", (err) => {
249
+ console.warn(`[${PLUGIN_ID}] Dashboard socket error:`, err.message);
250
+ });
251
+ socket.on("presence:update", () => {
252
+ fetchOnlineUsers();
253
+ });
254
+ } catch (err) {
255
+ console.error("[plugin-io] Failed to connect dashboard socket:", err);
256
+ }
257
+ };
258
+ connectSocket();
259
+ return () => {
260
+ cancelled = true;
261
+ if (socket) {
262
+ socket.disconnect();
263
+ socketRef.current = null;
264
+ }
265
+ };
266
+ }, [post, fetchOnlineUsers]);
267
+ useEffect(() => {
268
+ const interval = setInterval(fetchOnlineUsers, 15e3);
269
+ return () => clearInterval(interval);
270
+ }, [fetchOnlineUsers]);
271
+ if (loading) {
272
+ return /* @__PURE__ */ jsx(LoadingContainer, { children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Loading..." }) });
273
+ }
274
+ if (error) {
275
+ return /* @__PURE__ */ jsx(EmptyState, { children: /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "danger600", children: [
276
+ "Failed to load: ",
277
+ error
278
+ ] }) });
279
+ }
280
+ const users = data?.users || [];
281
+ const counts = data?.counts || { total: 0, editing: 0 };
282
+ return /* @__PURE__ */ jsxs(WidgetContainer, { children: [
283
+ /* @__PURE__ */ jsxs(HeaderContainer, { children: [
284
+ /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
285
+ /* @__PURE__ */ jsx(LiveDot, { $connected: connected }),
286
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "bold", textColor: "neutral800", children: "Who's Online" })
287
+ ] }),
288
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
289
+ /* @__PURE__ */ jsxs(CountBadge, { $active: counts.editing > 0, title: "Users editing", children: [
290
+ /* @__PURE__ */ jsx(Pencil, { width: "12", height: "12", style: { marginRight: 4 } }),
291
+ counts.editing
292
+ ] }),
293
+ /* @__PURE__ */ jsxs(CountBadge, { title: "Total online", children: [
294
+ /* @__PURE__ */ jsx(User, { width: "12", height: "12", style: { marginRight: 4 } }),
295
+ counts.total
296
+ ] })
297
+ ] })
298
+ ] }),
299
+ users.length === 0 ? /* @__PURE__ */ jsxs(EmptyState, { children: [
300
+ /* @__PURE__ */ jsx(EmptyIcon, { children: /* @__PURE__ */ jsx(User, { width: "28", height: "28", fill: "#a5a5ba" }) }),
301
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", textColor: "neutral600", children: "No one else is online" }),
302
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", style: { marginTop: 4 }, children: "You're the only one here right now" })
303
+ ] }) : /* @__PURE__ */ jsx(UserList, { children: users.map((userData, index) => /* @__PURE__ */ jsxs(UserCard, { children: [
304
+ /* @__PURE__ */ jsx(UserAvatar, { $colorIndex: index, children: getInitials(userData.user) }),
305
+ /* @__PURE__ */ jsxs(UserInfo, { children: [
306
+ /* @__PURE__ */ jsxs(UserName, { children: [
307
+ getDisplayName(userData.user),
308
+ userData.user.isAdmin && /* @__PURE__ */ jsx(Badge, { size: "S", style: { marginLeft: 8 }, children: "Admin" })
309
+ ] }),
310
+ /* @__PURE__ */ jsxs(UserMeta, { children: [
311
+ /* @__PURE__ */ jsx(Clock, { width: "12", height: "12" }),
312
+ "Online ",
313
+ formatDuration(userData.onlineFor)
314
+ ] }),
315
+ userData.isEditing ? userData.editingEntities.map((entity, idx) => /* @__PURE__ */ jsxs(
316
+ EditingBadge,
317
+ {
318
+ href: `/admin/content-manager/collection-types/${entity.uid}/${entity.documentId}`,
319
+ target: "_blank",
320
+ rel: "noopener noreferrer",
321
+ title: "Open in new tab",
322
+ children: [
323
+ /* @__PURE__ */ jsx(Pencil, { width: "10", height: "10" }),
324
+ entity.contentTypeName,
325
+ " - ",
326
+ entity.documentId
327
+ ]
328
+ },
329
+ idx
330
+ )) : /* @__PURE__ */ jsx(IdleBadge, { children: "Idle" })
331
+ ] })
332
+ ] }, userData.socketId)) }),
333
+ /* @__PURE__ */ jsx(Flex, { justifyContent: "flex-end", marginTop: 3, children: /* @__PURE__ */ jsx(FooterLink, { href: "/admin/settings/io/monitoring", children: "View All Activity" }) })
334
+ ] });
335
+ };
336
+ export {
337
+ OnlineEditorsWidget,
338
+ OnlineEditorsWidget as default
339
+ };
@@ -7,7 +7,7 @@ const icons = require("@strapi/icons");
7
7
  const admin = require("@strapi/strapi/admin");
8
8
  const index = require("./index-DkTxsEqL.js");
9
9
  const styled = require("styled-components");
10
- const index$1 = require("./index-BEZDDgvZ.js");
10
+ const index$1 = require("./index-DVNfszio.js");
11
11
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
12
12
  const styled__default = /* @__PURE__ */ _interopDefault(styled);
13
13
  const ResponsiveMain = styled__default.default(designSystem.Main)`
@@ -5,7 +5,7 @@ import { Download, Upload, Check } from "@strapi/icons";
5
5
  import { useFetchClient, useNotification } from "@strapi/strapi/admin";
6
6
  import { u as useIntl } from "./index-CEh8vkxY.mjs";
7
7
  import styled from "styled-components";
8
- import { P as PLUGIN_ID } from "./index-Dof_eA3e.mjs";
8
+ import { P as PLUGIN_ID } from "./index-Bw7WjN5H.mjs";
9
9
  const ResponsiveMain = styled(Main)`
10
10
  & > div {
11
11
  padding: 16px !important;
@@ -50,7 +50,7 @@ const index = {
50
50
  },
51
51
  id: `${PLUGIN_ID}-settings`,
52
52
  to: `${PLUGIN_ID}/settings`,
53
- Component: () => import("./SettingsPage-Btz_5MuC.mjs").then((mod) => ({ default: mod.SettingsPage }))
53
+ Component: () => import("./SettingsPage-Qi0iMaWc.mjs").then((mod) => ({ default: mod.SettingsPage }))
54
54
  },
55
55
  {
56
56
  intlLabel: {
@@ -59,7 +59,7 @@ const index = {
59
59
  },
60
60
  id: `${PLUGIN_ID}-monitoring`,
61
61
  to: `${PLUGIN_ID}/monitoring`,
62
- Component: () => import("./MonitoringPage-Bbkoh6ih.mjs").then((mod) => ({ default: mod.MonitoringPage }))
62
+ Component: () => import("./MonitoringPage-DKfhYUgU.mjs").then((mod) => ({ default: mod.MonitoringPage }))
63
63
  }
64
64
  ]
65
65
  );
@@ -77,13 +77,26 @@ const index = {
77
77
  id: "socket-io-stats-widget",
78
78
  pluginId: PLUGIN_ID
79
79
  });
80
- console.log(`[${PLUGIN_ID}] [SUCCESS] Socket.IO Stats Widget registered`);
80
+ app.widgets.register({
81
+ icon: PluginIcon,
82
+ title: {
83
+ id: `${PLUGIN_ID}.widget.online-editors.title`,
84
+ defaultMessage: "Who's Online"
85
+ },
86
+ component: async () => {
87
+ const component = await import("./OnlineEditorsWidget-RcYLxQke.mjs");
88
+ return component.OnlineEditorsWidget;
89
+ },
90
+ id: "socket-io-online-editors-widget",
91
+ pluginId: PLUGIN_ID
92
+ });
93
+ console.log(`[${PLUGIN_ID}] [SUCCESS] Dashboard widgets registered`);
81
94
  }
82
95
  },
83
96
  async bootstrap(app) {
84
97
  console.log(`[${PLUGIN_ID}] [INFO] Bootstrapping plugin...`);
85
98
  try {
86
- const { default: LivePresencePanel } = await import("./LivePresencePanel-CIFG_05s.mjs");
99
+ const { default: LivePresencePanel } = await import("./LivePresencePanel-D_vzQr4B.mjs");
87
100
  const contentManagerPlugin = app.getPlugin("content-manager");
88
101
  if (contentManagerPlugin && contentManagerPlugin.apis) {
89
102
  contentManagerPlugin.apis.addEditViewSidePanel([LivePresencePanel]);
@@ -51,7 +51,7 @@ const index = {
51
51
  },
52
52
  id: `${PLUGIN_ID}-settings`,
53
53
  to: `${PLUGIN_ID}/settings`,
54
- Component: () => Promise.resolve().then(() => require("./SettingsPage-CsRazf0j.js")).then((mod) => ({ default: mod.SettingsPage }))
54
+ Component: () => Promise.resolve().then(() => require("./SettingsPage-0k9qPAJZ.js")).then((mod) => ({ default: mod.SettingsPage }))
55
55
  },
56
56
  {
57
57
  intlLabel: {
@@ -60,7 +60,7 @@ const index = {
60
60
  },
61
61
  id: `${PLUGIN_ID}-monitoring`,
62
62
  to: `${PLUGIN_ID}/monitoring`,
63
- Component: () => Promise.resolve().then(() => require("./MonitoringPage-9f4Gzd2X.js")).then((mod) => ({ default: mod.MonitoringPage }))
63
+ Component: () => Promise.resolve().then(() => require("./MonitoringPage-CYGqkzva.js")).then((mod) => ({ default: mod.MonitoringPage }))
64
64
  }
65
65
  ]
66
66
  );
@@ -78,13 +78,26 @@ const index = {
78
78
  id: "socket-io-stats-widget",
79
79
  pluginId: PLUGIN_ID
80
80
  });
81
- console.log(`[${PLUGIN_ID}] [SUCCESS] Socket.IO Stats Widget registered`);
81
+ app.widgets.register({
82
+ icon: PluginIcon,
83
+ title: {
84
+ id: `${PLUGIN_ID}.widget.online-editors.title`,
85
+ defaultMessage: "Who's Online"
86
+ },
87
+ component: async () => {
88
+ const component = await Promise.resolve().then(() => require("./OnlineEditorsWidget-Bf8hfVha.js"));
89
+ return component.OnlineEditorsWidget;
90
+ },
91
+ id: "socket-io-online-editors-widget",
92
+ pluginId: PLUGIN_ID
93
+ });
94
+ console.log(`[${PLUGIN_ID}] [SUCCESS] Dashboard widgets registered`);
82
95
  }
83
96
  },
84
97
  async bootstrap(app) {
85
98
  console.log(`[${PLUGIN_ID}] [INFO] Bootstrapping plugin...`);
86
99
  try {
87
- const { default: LivePresencePanel } = await Promise.resolve().then(() => require("./LivePresencePanel-2U3I3yL0.js"));
100
+ const { default: LivePresencePanel } = await Promise.resolve().then(() => require("./LivePresencePanel-BkeWL4kq.js"));
88
101
  const contentManagerPlugin = app.getPlugin("content-manager");
89
102
  if (contentManagerPlugin && contentManagerPlugin.apis) {
90
103
  contentManagerPlugin.apis.addEditViewSidePanel([LivePresencePanel]);
@@ -1,3 +1,3 @@
1
1
  "use strict";
2
- const index = require("../_chunks/index-BEZDDgvZ.js");
2
+ const index = require("../_chunks/index-DVNfszio.js");
3
3
  module.exports = index.index;
@@ -1,4 +1,4 @@
1
- import { i } from "../_chunks/index-Dof_eA3e.mjs";
1
+ import { i } from "../_chunks/index-Bw7WjN5H.mjs";
2
2
  export {
3
3
  i as default
4
4
  };
@@ -666,7 +666,9 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
666
666
  });
667
667
  socket.on("get-entity-subscriptions", (callback) => {
668
668
  const rooms = Array.from(socket.rooms).filter((r) => r !== socket.id && r.includes(":")).map((room) => {
669
- const [uid, id] = room.split(":");
669
+ const lastColonIndex = room.lastIndexOf(":");
670
+ const uid = room.substring(0, lastColonIndex);
671
+ const id = room.substring(lastColonIndex + 1);
670
672
  return { uid, id, room };
671
673
  });
672
674
  if (callback) callback({ success: true, subscriptions: rooms });
@@ -1348,7 +1350,7 @@ const sessionTokens = /* @__PURE__ */ new Map();
1348
1350
  const activeSockets = /* @__PURE__ */ new Map();
1349
1351
  const refreshThrottle = /* @__PURE__ */ new Map();
1350
1352
  const SESSION_TTL = 10 * 60 * 1e3;
1351
- const REFRESH_COOLDOWN = 30 * 1e3;
1353
+ const REFRESH_COOLDOWN = 3 * 1e3;
1352
1354
  const CLEANUP_INTERVAL = 2 * 60 * 1e3;
1353
1355
  const hashToken = (token) => {
1354
1356
  return createHash("sha256").update(token).digest("hex");
@@ -1399,7 +1401,7 @@ var presence$3 = ({ strapi: strapi2 }) => ({
1399
1401
  userId: adminUser.id,
1400
1402
  user: {
1401
1403
  id: adminUser.id,
1402
- // Only store minimal user data needed for display
1404
+ email: adminUser.email,
1403
1405
  firstname: adminUser.firstname,
1404
1406
  lastname: adminUser.lastname
1405
1407
  },
@@ -1562,6 +1564,32 @@ var presence$3 = ({ strapi: strapi2 }) => ({
1562
1564
  strapi2.log.error("[plugin-io] Failed to invalidate user sessions:", error2);
1563
1565
  return ctx.internalServerError("Failed to invalidate sessions");
1564
1566
  }
1567
+ },
1568
+ /**
1569
+ * HTTP Handler: Gets all online users with their editing info
1570
+ * Used for the "Who's Online" dashboard widget
1571
+ * @param {object} ctx - Koa context
1572
+ */
1573
+ async getOnlineUsers(ctx) {
1574
+ const adminUser = ctx.state.user;
1575
+ if (!adminUser) {
1576
+ return ctx.unauthorized("Admin authentication required");
1577
+ }
1578
+ try {
1579
+ const presenceService = strapi2.plugin("io").service("presence");
1580
+ const onlineUsers = presenceService.getOnlineUsers();
1581
+ const counts = presenceService.getOnlineCounts();
1582
+ ctx.body = {
1583
+ data: {
1584
+ users: onlineUsers,
1585
+ counts,
1586
+ timestamp: Date.now()
1587
+ }
1588
+ };
1589
+ } catch (error2) {
1590
+ strapi2.log.error("[plugin-io] Failed to get online users:", error2);
1591
+ return ctx.internalServerError("Failed to get online users");
1592
+ }
1565
1593
  }
1566
1594
  });
1567
1595
  const settings$2 = settings$3;
@@ -1671,6 +1699,15 @@ var admin$1 = {
1671
1699
  config: {
1672
1700
  policies: ["admin::isAuthenticatedAdmin"]
1673
1701
  }
1702
+ },
1703
+ // Who's Online: Get all online users with editing info
1704
+ {
1705
+ method: "GET",
1706
+ path: "/online-users",
1707
+ handler: "presence.getOnlineUsers",
1708
+ config: {
1709
+ policies: ["admin::isAuthenticatedAdmin"]
1710
+ }
1674
1711
  }
1675
1712
  ]
1676
1713
  };
@@ -11346,6 +11383,7 @@ const getNonVisibleAttributes = (model) => {
11346
11383
  return ___default.uniq([
11347
11384
  ID_ATTRIBUTE$4,
11348
11385
  DOC_ID_ATTRIBUTE$4,
11386
+ PUBLISHED_AT_ATTRIBUTE$1,
11349
11387
  ...getTimestamps(model),
11350
11388
  ...nonVisibleAttributes
11351
11389
  ]);
@@ -30550,7 +30588,10 @@ var presence$1 = ({ strapi: strapi2 }) => {
30550
30588
  */
30551
30589
  registerConnection(socketId, user = null) {
30552
30590
  const settings2 = getPresenceSettings();
30553
- if (!settings2.enabled) return;
30591
+ if (!settings2.enabled) {
30592
+ strapi2.log.warn(`socket.io: Presence disabled, skipping registration for ${socketId}`);
30593
+ return;
30594
+ }
30554
30595
  activeConnections.set(socketId, {
30555
30596
  user,
30556
30597
  entities: /* @__PURE__ */ new Map(),
@@ -30558,7 +30599,8 @@ var presence$1 = ({ strapi: strapi2 }) => {
30558
30599
  lastSeen: Date.now(),
30559
30600
  connectedAt: Date.now()
30560
30601
  });
30561
- strapi2.log.debug(`socket.io: Presence registered for socket ${socketId}`);
30602
+ const username = user?.username || user?.firstname || "anonymous";
30603
+ strapi2.log.info(`socket.io: Presence registered for ${username} (socket: ${socketId}, total: ${activeConnections.size})`);
30562
30604
  },
30563
30605
  /**
30564
30606
  * Unregisters a socket connection and cleans up all entity presence
@@ -30569,7 +30611,9 @@ var presence$1 = ({ strapi: strapi2 }) => {
30569
30611
  if (!connection) return;
30570
30612
  if (connection.entities) {
30571
30613
  for (const entityKey of connection.entities.keys()) {
30572
- const [uid, documentId] = entityKey.split(":");
30614
+ const lastColonIndex = entityKey.lastIndexOf(":");
30615
+ const uid = entityKey.substring(0, lastColonIndex);
30616
+ const documentId = entityKey.substring(lastColonIndex + 1);
30573
30617
  await this.leaveEntity(socketId, uid, documentId, false);
30574
30618
  }
30575
30619
  }
@@ -30804,6 +30848,90 @@ var presence$1 = ({ strapi: strapi2 }) => {
30804
30848
  timestamp: Date.now()
30805
30849
  });
30806
30850
  }
30851
+ },
30852
+ /**
30853
+ * Gets all online users with their currently editing entities
30854
+ * Used for the "Who's Online" dashboard widget
30855
+ * @returns {Array} List of online users with their editing info
30856
+ */
30857
+ getOnlineUsers() {
30858
+ const users = [];
30859
+ const now = Date.now();
30860
+ for (const [socketId, connection] of activeConnections) {
30861
+ if (!connection.user) continue;
30862
+ const editingEntities = [];
30863
+ if (connection.entities) {
30864
+ for (const [entityKey, joinedAt] of connection.entities) {
30865
+ const lastColonIndex = entityKey.lastIndexOf(":");
30866
+ const uid = entityKey.substring(0, lastColonIndex);
30867
+ const documentId = entityKey.substring(lastColonIndex + 1);
30868
+ let contentTypeName = uid;
30869
+ try {
30870
+ const contentType = strapi2.contentTypes[uid];
30871
+ if (contentType?.info?.displayName) {
30872
+ contentTypeName = contentType.info.displayName;
30873
+ } else if (contentType?.info?.singularName) {
30874
+ contentTypeName = contentType.info.singularName;
30875
+ }
30876
+ } catch (e) {
30877
+ }
30878
+ editingEntities.push({
30879
+ uid,
30880
+ documentId,
30881
+ contentTypeName,
30882
+ joinedAt,
30883
+ editingFor: Math.floor((now - joinedAt) / 1e3)
30884
+ // seconds
30885
+ });
30886
+ }
30887
+ }
30888
+ users.push({
30889
+ socketId,
30890
+ user: {
30891
+ id: connection.user.id,
30892
+ username: connection.user.username,
30893
+ email: connection.user.email,
30894
+ firstname: connection.user.firstname,
30895
+ lastname: connection.user.lastname,
30896
+ isAdmin: connection.user.isAdmin || false
30897
+ },
30898
+ connectedAt: connection.connectedAt,
30899
+ lastSeen: connection.lastSeen,
30900
+ onlineFor: Math.floor((now - connection.connectedAt) / 1e3),
30901
+ // seconds
30902
+ editingEntities,
30903
+ isEditing: editingEntities.length > 0
30904
+ });
30905
+ }
30906
+ users.sort((a, b) => {
30907
+ if (a.isEditing && !b.isEditing) return -1;
30908
+ if (!a.isEditing && b.isEditing) return 1;
30909
+ return b.connectedAt - a.connectedAt;
30910
+ });
30911
+ return users;
30912
+ },
30913
+ /**
30914
+ * Gets count of online users
30915
+ * @returns {object} Online user counts
30916
+ */
30917
+ getOnlineCounts() {
30918
+ let total = 0;
30919
+ let admins = 0;
30920
+ let users = 0;
30921
+ let editing = 0;
30922
+ for (const connection of activeConnections.values()) {
30923
+ if (!connection.user) continue;
30924
+ total++;
30925
+ if (connection.user.isAdmin) {
30926
+ admins++;
30927
+ } else {
30928
+ users++;
30929
+ }
30930
+ if (connection.entities?.size > 0) {
30931
+ editing++;
30932
+ }
30933
+ }
30934
+ return { total, admins, users, editing };
30807
30935
  }
30808
30936
  };
30809
30937
  };
@@ -31020,7 +31148,9 @@ var preview$1 = ({ strapi: strapi2 }) => {
31020
31148
  getActivePreviewEntities() {
31021
31149
  const entities = [];
31022
31150
  for (const [entityKey, subscribers] of previewSubscribers) {
31023
- const [uid, documentId] = entityKey.split(":");
31151
+ const lastColonIndex = entityKey.lastIndexOf(":");
31152
+ const uid = entityKey.substring(0, lastColonIndex);
31153
+ const documentId = entityKey.substring(lastColonIndex + 1);
31024
31154
  entities.push({
31025
31155
  uid,
31026
31156
  documentId,