@creezio/observability 0.22.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 (209) hide show
  1. package/README.md +328 -0
  2. package/dist/.creezio-src-hash.json +4 -0
  3. package/dist/api-endpoints/http-routes.d.ts +11 -0
  4. package/dist/api-endpoints/http-routes.d.ts.map +1 -0
  5. package/dist/api-endpoints/http-routes.js +11 -0
  6. package/dist/api-endpoints/http-routes.js.map +1 -0
  7. package/dist/api-endpoints/index.d.ts +4 -0
  8. package/dist/api-endpoints/index.d.ts.map +1 -0
  9. package/dist/api-endpoints/index.js +3 -0
  10. package/dist/api-endpoints/index.js.map +1 -0
  11. package/dist/api-endpoints/registry.d.ts +66 -0
  12. package/dist/api-endpoints/registry.d.ts.map +1 -0
  13. package/dist/api-endpoints/registry.js +102 -0
  14. package/dist/api-endpoints/registry.js.map +1 -0
  15. package/dist/api-mount.d.ts +7 -0
  16. package/dist/api-mount.d.ts.map +1 -0
  17. package/dist/api-mount.js +114 -0
  18. package/dist/api-mount.js.map +1 -0
  19. package/dist/helpers.d.ts +23 -0
  20. package/dist/helpers.d.ts.map +1 -0
  21. package/dist/helpers.js +36 -0
  22. package/dist/helpers.js.map +1 -0
  23. package/dist/index.d.ts +35 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +24 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/memory-store.d.ts +3 -0
  28. package/dist/memory-store.d.ts.map +1 -0
  29. package/dist/memory-store.js +113 -0
  30. package/dist/memory-store.js.map +1 -0
  31. package/dist/ops/emit.d.ts +8 -0
  32. package/dist/ops/emit.d.ts.map +1 -0
  33. package/dist/ops/emit.js +15 -0
  34. package/dist/ops/emit.js.map +1 -0
  35. package/dist/ops/fleet-activity.d.ts +50 -0
  36. package/dist/ops/fleet-activity.d.ts.map +1 -0
  37. package/dist/ops/fleet-activity.js +123 -0
  38. package/dist/ops/fleet-activity.js.map +1 -0
  39. package/dist/ops/fleet-agent.d.ts +102 -0
  40. package/dist/ops/fleet-agent.d.ts.map +1 -0
  41. package/dist/ops/fleet-agent.js +352 -0
  42. package/dist/ops/fleet-agent.js.map +1 -0
  43. package/dist/ops/fleet-samples.d.ts +33 -0
  44. package/dist/ops/fleet-samples.d.ts.map +1 -0
  45. package/dist/ops/fleet-samples.js +223 -0
  46. package/dist/ops/fleet-samples.js.map +1 -0
  47. package/dist/ops/journal.d.ts +56 -0
  48. package/dist/ops/journal.d.ts.map +1 -0
  49. package/dist/ops/journal.js +307 -0
  50. package/dist/ops/journal.js.map +1 -0
  51. package/dist/ops/rules.d.ts +16 -0
  52. package/dist/ops/rules.d.ts.map +1 -0
  53. package/dist/ops/rules.js +98 -0
  54. package/dist/ops/rules.js.map +1 -0
  55. package/dist/ops/types.d.ts +69 -0
  56. package/dist/ops/types.d.ts.map +1 -0
  57. package/dist/ops/types.js +119 -0
  58. package/dist/ops/types.js.map +1 -0
  59. package/dist/request-logs/config.d.ts +19 -0
  60. package/dist/request-logs/config.d.ts.map +1 -0
  61. package/dist/request-logs/config.js +36 -0
  62. package/dist/request-logs/config.js.map +1 -0
  63. package/dist/request-logs/http-routes.d.ts +8 -0
  64. package/dist/request-logs/http-routes.d.ts.map +1 -0
  65. package/dist/request-logs/http-routes.js +29 -0
  66. package/dist/request-logs/http-routes.js.map +1 -0
  67. package/dist/request-logs/index.d.ts +6 -0
  68. package/dist/request-logs/index.d.ts.map +1 -0
  69. package/dist/request-logs/index.js +5 -0
  70. package/dist/request-logs/index.js.map +1 -0
  71. package/dist/request-logs/middleware.d.ts +11 -0
  72. package/dist/request-logs/middleware.d.ts.map +1 -0
  73. package/dist/request-logs/middleware.js +198 -0
  74. package/dist/request-logs/middleware.js.map +1 -0
  75. package/dist/request-logs/request-logs.d.ts +60 -0
  76. package/dist/request-logs/request-logs.d.ts.map +1 -0
  77. package/dist/request-logs/request-logs.js +260 -0
  78. package/dist/request-logs/request-logs.js.map +1 -0
  79. package/dist/schema.d.ts +3 -0
  80. package/dist/schema.d.ts.map +1 -0
  81. package/dist/schema.js +21 -0
  82. package/dist/schema.js.map +1 -0
  83. package/dist/sqlite-driver.d.ts +16 -0
  84. package/dist/sqlite-driver.d.ts.map +1 -0
  85. package/dist/sqlite-driver.js +15 -0
  86. package/dist/sqlite-driver.js.map +1 -0
  87. package/dist/sqlite-store.d.ts +12 -0
  88. package/dist/sqlite-store.d.ts.map +1 -0
  89. package/dist/sqlite-store.js +176 -0
  90. package/dist/sqlite-store.js.map +1 -0
  91. package/dist/types.d.ts +67 -0
  92. package/dist/types.d.ts.map +1 -0
  93. package/dist/types.js +9 -0
  94. package/dist/types.js.map +1 -0
  95. package/dist/usage/adapters.d.ts +31 -0
  96. package/dist/usage/adapters.d.ts.map +1 -0
  97. package/dist/usage/adapters.js +37 -0
  98. package/dist/usage/adapters.js.map +1 -0
  99. package/dist/usage/http-routes.d.ts +23 -0
  100. package/dist/usage/http-routes.d.ts.map +1 -0
  101. package/dist/usage/http-routes.js +150 -0
  102. package/dist/usage/http-routes.js.map +1 -0
  103. package/dist/usage/index.d.ts +11 -0
  104. package/dist/usage/index.d.ts.map +1 -0
  105. package/dist/usage/index.js +7 -0
  106. package/dist/usage/index.js.map +1 -0
  107. package/dist/usage/ui-brand.d.ts +23 -0
  108. package/dist/usage/ui-brand.d.ts.map +1 -0
  109. package/dist/usage/ui-brand.js +18 -0
  110. package/dist/usage/ui-brand.js.map +1 -0
  111. package/dist/usage/usage-analytics-productivity.d.ts +73 -0
  112. package/dist/usage/usage-analytics-productivity.d.ts.map +1 -0
  113. package/dist/usage/usage-analytics-productivity.js +317 -0
  114. package/dist/usage/usage-analytics-productivity.js.map +1 -0
  115. package/dist/usage/usage-analytics-shared.d.ts +5 -0
  116. package/dist/usage/usage-analytics-shared.d.ts.map +1 -0
  117. package/dist/usage/usage-analytics-shared.js +20 -0
  118. package/dist/usage/usage-analytics-shared.js.map +1 -0
  119. package/dist/usage/usage-analytics.d.ts +127 -0
  120. package/dist/usage/usage-analytics.d.ts.map +1 -0
  121. package/dist/usage/usage-analytics.js +338 -0
  122. package/dist/usage/usage-analytics.js.map +1 -0
  123. package/dist-cjs/api-endpoints/http-routes.js +14 -0
  124. package/dist-cjs/api-endpoints/http-routes.js.map +1 -0
  125. package/dist-cjs/api-endpoints/index.js +10 -0
  126. package/dist-cjs/api-endpoints/index.js.map +1 -0
  127. package/dist-cjs/api-endpoints/registry.js +107 -0
  128. package/dist-cjs/api-endpoints/registry.js.map +1 -0
  129. package/dist-cjs/api-mount.js +117 -0
  130. package/dist-cjs/api-mount.js.map +1 -0
  131. package/dist-cjs/helpers.js +41 -0
  132. package/dist-cjs/helpers.js.map +1 -0
  133. package/dist-cjs/index.js +118 -0
  134. package/dist-cjs/index.js.map +1 -0
  135. package/dist-cjs/memory-store.js +119 -0
  136. package/dist-cjs/memory-store.js.map +1 -0
  137. package/dist-cjs/ops/emit.js +18 -0
  138. package/dist-cjs/ops/emit.js.map +1 -0
  139. package/dist-cjs/ops/fleet-activity.js +131 -0
  140. package/dist-cjs/ops/fleet-activity.js.map +1 -0
  141. package/dist-cjs/ops/fleet-agent.js +358 -0
  142. package/dist-cjs/ops/fleet-agent.js.map +1 -0
  143. package/dist-cjs/ops/fleet-samples.js +229 -0
  144. package/dist-cjs/ops/fleet-samples.js.map +1 -0
  145. package/dist-cjs/ops/journal.js +326 -0
  146. package/dist-cjs/ops/journal.js.map +1 -0
  147. package/dist-cjs/ops/rules.js +102 -0
  148. package/dist-cjs/ops/rules.js.map +1 -0
  149. package/dist-cjs/ops/types.js +126 -0
  150. package/dist-cjs/ops/types.js.map +1 -0
  151. package/dist-cjs/package.json +3 -0
  152. package/dist-cjs/request-logs/config.js +42 -0
  153. package/dist-cjs/request-logs/config.js.map +1 -0
  154. package/dist-cjs/request-logs/http-routes.js +32 -0
  155. package/dist-cjs/request-logs/http-routes.js.map +1 -0
  156. package/dist-cjs/request-logs/index.js +27 -0
  157. package/dist-cjs/request-logs/index.js.map +1 -0
  158. package/dist-cjs/request-logs/middleware.js +203 -0
  159. package/dist-cjs/request-logs/middleware.js.map +1 -0
  160. package/dist-cjs/request-logs/request-logs.js +277 -0
  161. package/dist-cjs/request-logs/request-logs.js.map +1 -0
  162. package/dist-cjs/schema.js +24 -0
  163. package/dist-cjs/schema.js.map +1 -0
  164. package/dist-cjs/sqlite-driver.js +18 -0
  165. package/dist-cjs/sqlite-driver.js.map +1 -0
  166. package/dist-cjs/sqlite-store.js +182 -0
  167. package/dist-cjs/sqlite-store.js.map +1 -0
  168. package/dist-cjs/types.js +12 -0
  169. package/dist-cjs/types.js.map +1 -0
  170. package/dist-cjs/usage/adapters.js +47 -0
  171. package/dist-cjs/usage/adapters.js.map +1 -0
  172. package/dist-cjs/usage/http-routes.js +154 -0
  173. package/dist-cjs/usage/http-routes.js.map +1 -0
  174. package/dist-cjs/usage/index.js +34 -0
  175. package/dist-cjs/usage/index.js.map +1 -0
  176. package/dist-cjs/usage/ui-brand.js +23 -0
  177. package/dist-cjs/usage/ui-brand.js.map +1 -0
  178. package/dist-cjs/usage/usage-analytics-productivity.js +321 -0
  179. package/dist-cjs/usage/usage-analytics-productivity.js.map +1 -0
  180. package/dist-cjs/usage/usage-analytics-shared.js +23 -0
  181. package/dist-cjs/usage/usage-analytics-shared.js.map +1 -0
  182. package/dist-cjs/usage/usage-analytics.js +353 -0
  183. package/dist-cjs/usage/usage-analytics.js.map +1 -0
  184. package/fleet-collector/AGENTS.md +7 -0
  185. package/fleet-collector/README.md +83 -0
  186. package/fleet-collector/configure-fleet-npm.sh +108 -0
  187. package/fleet-collector/env.mjs +174 -0
  188. package/fleet-collector/ops-api.mjs +455 -0
  189. package/fleet-collector/public/admin.html +696 -0
  190. package/fleet-collector/public/index.html +941 -0
  191. package/fleet-collector/server.mjs +564 -0
  192. package/fleet-collector/test-fleet-collector.mjs +413 -0
  193. package/fleet-collector/test-server-admin.mjs +475 -0
  194. package/package.json +109 -0
  195. package/scripts/test-api-endpoints-registry.mjs +60 -0
  196. package/ui/analytics-client.tsx +834 -0
  197. package/ui/analytics-productivity-panel.tsx +513 -0
  198. package/ui/api-endpoints-client.tsx +228 -0
  199. package/ui/index.ts +39 -0
  200. package/ui/primitives/badge.tsx +37 -0
  201. package/ui/primitives/button.tsx +44 -0
  202. package/ui/primitives/card.tsx +44 -0
  203. package/ui/primitives/cn.ts +6 -0
  204. package/ui/primitives/input.tsx +19 -0
  205. package/ui/primitives/tabs.tsx +51 -0
  206. package/ui/request-logs-client.tsx +327 -0
  207. package/ui/session-usage-analytics-provider.tsx +28 -0
  208. package/ui/usage-analytics-client.ts +479 -0
  209. package/ui/usage-analytics-provider.tsx +84 -0
@@ -0,0 +1,513 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import {
5
+ Bar,
6
+ BarChart,
7
+ CartesianGrid,
8
+ ResponsiveContainer,
9
+ Tooltip,
10
+ XAxis,
11
+ YAxis,
12
+ } from "recharts";
13
+ import { Coffee, Flame, Gauge, Timer } from "lucide-react";
14
+ import { Badge } from "./primitives/badge";
15
+ import { cn } from "./primitives/cn";
16
+ import { formatDuration } from "../dist/usage/usage-analytics-shared.js";
17
+
18
+ export type ProductivityPayload = {
19
+ summary: {
20
+ userId: string;
21
+ username: string;
22
+ kind: string;
23
+ activeMs: number;
24
+ idleMs: number;
25
+ breakMs: number;
26
+ avgActivePerDayMs: number;
27
+ avgBreakMs: number;
28
+ breaksCount: number;
29
+ focusBlocksCount: number;
30
+ avgFocusBlockMs: number;
31
+ peakHour: number | null;
32
+ peakWeekday: number | null;
33
+ eventsPerActiveHour: number;
34
+ focusRatio: number;
35
+ productivityScore: number;
36
+ daysActive: number;
37
+ events: number;
38
+ clicks: number;
39
+ pageViews: number;
40
+ } | null;
41
+ heatmap: Array<{ weekday: number; hour: number; events: number; activeMs: number }>;
42
+ daily: Array<{
43
+ date: string;
44
+ activeMs: number;
45
+ idleMs: number;
46
+ breakMs: number;
47
+ breaksCount: number;
48
+ events: number;
49
+ clicks: number;
50
+ pageViews: number;
51
+ productivityScore: number;
52
+ firstSeen: string | null;
53
+ lastSeen: string | null;
54
+ }>;
55
+ breaks: Array<{
56
+ startedAt: string;
57
+ endedAt: string;
58
+ durationMs: number;
59
+ day: string;
60
+ }>;
61
+ focusBlocks: Array<{
62
+ startedAt: string;
63
+ endedAt: string;
64
+ durationMs: number;
65
+ events: number;
66
+ }>;
67
+ leaderboard: Array<{
68
+ userId: string;
69
+ username: string;
70
+ kind: string;
71
+ activeMs: number;
72
+ breakMs: number;
73
+ breaksCount: number;
74
+ productivityScore: number;
75
+ avgActivePerDayMs: number;
76
+ focusRatio: number;
77
+ events: number;
78
+ }>;
79
+ };
80
+
81
+ const WEEKDAYS = ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam"];
82
+ const HOURS = Array.from({ length: 24 }, (_, i) => i);
83
+
84
+ function scoreTone(score: number): string {
85
+ if (score >= 75) return "text-emerald-700 bg-emerald-50 border-emerald-200";
86
+ if (score >= 45) return "text-amber-700 bg-amber-50 border-amber-200";
87
+ return "text-rose-700 bg-rose-50 border-rose-200";
88
+ }
89
+
90
+ function heatColor(intensity: number, max: number): string {
91
+ if (max <= 0 || intensity <= 0) return "bg-slate-50";
92
+ const t = intensity / max;
93
+ if (t < 0.15) return "bg-sky-100";
94
+ if (t < 0.35) return "bg-sky-200";
95
+ if (t < 0.55) return "bg-sky-300";
96
+ if (t < 0.75) return "bg-sky-400";
97
+ if (t < 0.9) return "bg-sky-500";
98
+ return "bg-sky-600";
99
+ }
100
+
101
+ function formatHour(h: number): string {
102
+ return `${String(h).padStart(2, "0")}h`;
103
+ }
104
+
105
+ function formatTs(iso: string): string {
106
+ try {
107
+ return new Date(iso).toLocaleString("fr-FR", {
108
+ day: "2-digit",
109
+ month: "short",
110
+ hour: "2-digit",
111
+ minute: "2-digit",
112
+ });
113
+ } catch {
114
+ return iso;
115
+ }
116
+ }
117
+
118
+ export function AnalyticsProductivityPanel({
119
+ data,
120
+ selectedUserId,
121
+ onSelectUser,
122
+ }: {
123
+ data: ProductivityPayload | null;
124
+ selectedUserId: string;
125
+ onSelectUser: (userId: string) => void;
126
+ }) {
127
+ const summary = data?.summary || null;
128
+
129
+ const heatMax = useMemo(() => {
130
+ let m = 0;
131
+ for (const c of data?.heatmap || []) {
132
+ const v = c.activeMs > 0 ? c.activeMs : c.events * 30_000;
133
+ if (v > m) m = v;
134
+ }
135
+ return m;
136
+ }, [data?.heatmap]);
137
+
138
+ const heatLookup = useMemo(() => {
139
+ const map = new Map<string, { events: number; activeMs: number }>();
140
+ for (const c of data?.heatmap || []) {
141
+ map.set(`${c.weekday}:${c.hour}`, { events: c.events, activeMs: c.activeMs });
142
+ }
143
+ return map;
144
+ }, [data?.heatmap]);
145
+
146
+ const dailyChart = useMemo(
147
+ () =>
148
+ (data?.daily || []).map((d) => ({
149
+ date: d.date.slice(5),
150
+ activeH: Math.round((d.activeMs / 3_600_000) * 100) / 100,
151
+ breakH: Math.round((d.breakMs / 3_600_000) * 100) / 100,
152
+ idleH: Math.round((d.idleMs / 3_600_000) * 100) / 100,
153
+ score: d.productivityScore,
154
+ })),
155
+ [data?.daily],
156
+ );
157
+
158
+ if (!data) {
159
+ return (
160
+ <p className="rounded-2xl border border-dashed border-slate-200 bg-white py-16 text-center text-sm text-slate-400">
161
+ Chargement productivité…
162
+ </p>
163
+ );
164
+ }
165
+
166
+ return (
167
+ <div className="space-y-4">
168
+ {/* Explication méthodo */}
169
+ <div className="rounded-2xl border border-sky-100 bg-gradient-to-r from-sky-50/80 to-white px-4 py-3 text-xs leading-relaxed text-slate-600">
170
+ <strong className="text-slate-800">Comment c’est calculé — </strong>
171
+ présence active via heartbeats (60s) + interactions ; idle après 2 min sans
172
+ activité ; pauses = trous d’activité entre 5 min et 4 h ; score 0–100 =
173
+ couverture horaire × focus (actif vs idle) × densité d’actions. Aucune frappe
174
+ clavier n’est enregistrée.
175
+ </div>
176
+
177
+ {/* Leaderboard */}
178
+ <div className="rounded-2xl border border-slate-200/80 bg-white p-4 shadow-sm">
179
+ <div className="mb-3 flex items-center justify-between gap-2">
180
+ <div>
181
+ <h2 className="text-sm font-semibold text-slate-900">Classement collaborateurs</h2>
182
+ <p className="text-xs text-slate-500">
183
+ Cliquez un employé pour ouvrir sa heatmap et ses pauses
184
+ </p>
185
+ </div>
186
+ <Gauge className="h-4 w-4 text-slate-400" />
187
+ </div>
188
+ <div className="overflow-auto">
189
+ <table className="w-full text-left text-sm">
190
+ <thead className="text-xs uppercase tracking-wide text-slate-500">
191
+ <tr>
192
+ <th className="px-2 py-2 font-medium">#</th>
193
+ <th className="px-2 py-2 font-medium">Collaborateur</th>
194
+ <th className="px-2 py-2 font-medium">Score</th>
195
+ <th className="px-2 py-2 font-medium">Actif / jour</th>
196
+ <th className="px-2 py-2 font-medium">Pauses</th>
197
+ <th className="px-2 py-2 font-medium">Focus</th>
198
+ </tr>
199
+ </thead>
200
+ <tbody>
201
+ {(data.leaderboard || []).length === 0 ? (
202
+ <tr>
203
+ <td colSpan={6} className="px-2 py-8 text-center text-slate-400">
204
+ Pas encore assez de données de présence — naviguez dans l’app quelques
205
+ minutes (heartbeats).
206
+ </td>
207
+ </tr>
208
+ ) : (
209
+ data.leaderboard.map((u, i) => (
210
+ <tr
211
+ key={u.userId}
212
+ className={cn(
213
+ "cursor-pointer border-t border-slate-100 transition-colors hover:bg-sky-50/60",
214
+ selectedUserId === u.userId && "bg-sky-50",
215
+ )}
216
+ onClick={() =>
217
+ onSelectUser(selectedUserId === u.userId ? "" : u.userId)
218
+ }
219
+ >
220
+ <td className="px-2 py-2 text-xs text-slate-400">{i + 1}</td>
221
+ <td className="px-2 py-2">
222
+ <div className="flex items-center gap-2">
223
+ <span className="font-medium text-slate-800">{u.username}</span>
224
+ <Badge variant={u.kind === "ai" ? "warning" : "success"}>
225
+ {u.kind === "ai" ? "IA" : "Humain"}
226
+ </Badge>
227
+ </div>
228
+ </td>
229
+ <td className="px-2 py-2">
230
+ <span
231
+ className={cn(
232
+ "inline-flex rounded-md border px-2 py-0.5 text-xs font-semibold tabular-nums",
233
+ scoreTone(u.productivityScore),
234
+ )}
235
+ >
236
+ {u.productivityScore}
237
+ </span>
238
+ </td>
239
+ <td className="px-2 py-2 tabular-nums text-slate-700">
240
+ {formatDuration(u.avgActivePerDayMs)}
241
+ </td>
242
+ <td className="px-2 py-2 text-slate-700">
243
+ {u.breaksCount}{" "}
244
+ <span className="text-xs text-slate-400">
245
+ ({formatDuration(u.breakMs)})
246
+ </span>
247
+ </td>
248
+ <td className="px-2 py-2 tabular-nums text-slate-700">
249
+ {Math.round((u.focusRatio || 0) * 100)}%
250
+ </td>
251
+ </tr>
252
+ ))
253
+ )}
254
+ </tbody>
255
+ </table>
256
+ </div>
257
+ </div>
258
+
259
+ {summary ? (
260
+ <>
261
+ <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
262
+ <MiniKpi
263
+ icon={Gauge}
264
+ label={`Score · ${summary.username}`}
265
+ value={String(summary.productivityScore)}
266
+ hint={`${summary.daysActive} jour${summary.daysActive > 1 ? "s" : ""} actif${summary.daysActive > 1 ? "s" : ""}`}
267
+ accent="bg-emerald-500"
268
+ />
269
+ <MiniKpi
270
+ icon={Timer}
271
+ label="Temps actif / jour"
272
+ value={formatDuration(summary.avgActivePerDayMs)}
273
+ hint={`Total ${formatDuration(summary.activeMs)}`}
274
+ accent="bg-sky-500"
275
+ />
276
+ <MiniKpi
277
+ icon={Coffee}
278
+ label="Pauses"
279
+ value={String(summary.breaksCount)}
280
+ hint={`Moy. ${formatDuration(summary.avgBreakMs)} · total ${formatDuration(summary.breakMs)}`}
281
+ accent="bg-amber-500"
282
+ />
283
+ <MiniKpi
284
+ icon={Flame}
285
+ label="Pic d'activité"
286
+ value={
287
+ summary.peakHour != null && summary.peakWeekday != null
288
+ ? `${WEEKDAYS[summary.peakWeekday]} ${formatHour(summary.peakHour)}`
289
+ : "—"
290
+ }
291
+ hint={`${summary.eventsPerActiveHour} evt/h actif · focus ${Math.round(summary.focusRatio * 100)}%`}
292
+ accent="bg-violet-500"
293
+ />
294
+ </div>
295
+
296
+ {/* Heatmap */}
297
+ <div className="rounded-2xl border border-slate-200/80 bg-white p-4 shadow-sm">
298
+ <div className="mb-3">
299
+ <h2 className="text-sm font-semibold text-slate-900">
300
+ Heatmap d’activité — {summary.username}
301
+ </h2>
302
+ <p className="text-xs text-slate-500">
303
+ Intensité par jour de la semaine × heure (UTC). Plus c’est foncé, plus
304
+ l’employé est actif.
305
+ </p>
306
+ </div>
307
+ <div className="overflow-x-auto">
308
+ <div className="inline-block min-w-full">
309
+ <div
310
+ className="grid gap-1"
311
+ style={{ gridTemplateColumns: `48px repeat(24, minmax(18px, 1fr))` }}
312
+ >
313
+ <div />
314
+ {HOURS.map((h) => (
315
+ <div
316
+ key={h}
317
+ className="text-center text-[9px] tabular-nums text-slate-400"
318
+ >
319
+ {h % 3 === 0 ? h : ""}
320
+ </div>
321
+ ))}
322
+ {WEEKDAYS.map((label, weekday) => (
323
+ <div key={label} className="contents">
324
+ <div className="flex items-center text-[11px] font-medium text-slate-500">
325
+ {label}
326
+ </div>
327
+ {HOURS.map((hour) => {
328
+ const cell = heatLookup.get(`${weekday}:${hour}`);
329
+ const intensity = cell
330
+ ? cell.activeMs > 0
331
+ ? cell.activeMs
332
+ : cell.events * 30_000
333
+ : 0;
334
+ const title = cell
335
+ ? `${label} ${formatHour(hour)} — ${cell.events} evt · ${formatDuration(cell.activeMs)}`
336
+ : `${label} ${formatHour(hour)} — aucune activité`;
337
+ return (
338
+ <div
339
+ key={`${weekday}-${hour}`}
340
+ title={title}
341
+ className={cn(
342
+ "aspect-square rounded-sm transition-colors",
343
+ heatColor(intensity, heatMax),
344
+ )}
345
+ />
346
+ );
347
+ })}
348
+ </div>
349
+ ))}
350
+ </div>
351
+ <div className="mt-3 flex items-center gap-2 text-[10px] text-slate-400">
352
+ <span>Faible</span>
353
+ <div className="flex gap-0.5">
354
+ {["bg-slate-50", "bg-sky-100", "bg-sky-300", "bg-sky-500", "bg-sky-600"].map(
355
+ (c) => (
356
+ <div key={c} className={cn("h-3 w-4 rounded-sm", c)} />
357
+ ),
358
+ )}
359
+ </div>
360
+ <span>Fort</span>
361
+ </div>
362
+ </div>
363
+ </div>
364
+ </div>
365
+
366
+ <div className="grid gap-4 xl:grid-cols-2">
367
+ <div className="rounded-2xl border border-slate-200/80 bg-white p-4 shadow-sm">
368
+ <h2 className="text-sm font-semibold text-slate-900">Heures actives / jour</h2>
369
+ <p className="mb-3 text-xs text-slate-500">
370
+ Temps actif vs pauses détectées (heures)
371
+ </p>
372
+ <div className="h-56">
373
+ {dailyChart.length < 1 ? (
374
+ <p className="flex h-full items-center justify-center text-sm text-slate-400">
375
+ Pas de série quotidienne
376
+ </p>
377
+ ) : (
378
+ <ResponsiveContainer width="100%" height="100%">
379
+ <BarChart data={dailyChart}>
380
+ <CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
381
+ <XAxis dataKey="date" tick={{ fontSize: 11 }} stroke="#94a3b8" />
382
+ <YAxis tick={{ fontSize: 11 }} stroke="#94a3b8" width={28} />
383
+ <Tooltip
384
+ contentStyle={{ fontSize: 12, borderRadius: 12 }}
385
+ formatter={(v: number, name: string) => [
386
+ `${v} h`,
387
+ name === "activeH"
388
+ ? "Actif"
389
+ : name === "breakH"
390
+ ? "Pauses"
391
+ : "Idle",
392
+ ]}
393
+ />
394
+ <Bar dataKey="activeH" name="activeH" fill="#0ea5e9" radius={[4, 4, 0, 0]} />
395
+ <Bar dataKey="breakH" name="breakH" fill="#f59e0b" radius={[4, 4, 0, 0]} />
396
+ </BarChart>
397
+ </ResponsiveContainer>
398
+ )}
399
+ </div>
400
+ </div>
401
+
402
+ <div className="rounded-2xl border border-slate-200/80 bg-white p-4 shadow-sm">
403
+ <h2 className="text-sm font-semibold text-slate-900">Pauses détectées</h2>
404
+ <p className="mb-3 text-xs text-slate-500">
405
+ Gaps d’activité 5 min – 4 h (au-delà = coupure de journée)
406
+ </p>
407
+ <div className="max-h-56 space-y-2 overflow-auto">
408
+ {(data.breaks || []).length === 0 ? (
409
+ <p className="py-8 text-center text-sm text-slate-400">
410
+ Aucune pause détectée sur la période
411
+ </p>
412
+ ) : (
413
+ data.breaks.map((b, i) => (
414
+ <div
415
+ key={`${b.startedAt}-${i}`}
416
+ className="flex items-center justify-between gap-3 rounded-xl border border-slate-100 bg-slate-50/70 px-3 py-2"
417
+ >
418
+ <div className="min-w-0">
419
+ <p className="truncate text-sm font-medium text-slate-800">
420
+ {formatTs(b.startedAt)} → {formatTs(b.endedAt)}
421
+ </p>
422
+ <p className="text-[11px] text-slate-400">{b.day}</p>
423
+ </div>
424
+ <span className="shrink-0 rounded-md bg-amber-100 px-2 py-0.5 text-xs font-semibold tabular-nums text-amber-800">
425
+ {formatDuration(b.durationMs)}
426
+ </span>
427
+ </div>
428
+ ))
429
+ )}
430
+ </div>
431
+ </div>
432
+ </div>
433
+
434
+ <div className="rounded-2xl border border-slate-200/80 bg-white p-4 shadow-sm">
435
+ <h2 className="text-sm font-semibold text-slate-900">Blocs de focus</h2>
436
+ <p className="mb-3 text-xs text-slate-500">
437
+ Plus longs segments d’activité continue (sans pause ≥ 5 min)
438
+ </p>
439
+ <div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
440
+ {(data.focusBlocks || []).length === 0 ? (
441
+ <p className="col-span-full py-6 text-center text-sm text-slate-400">
442
+ Pas encore de blocs de focus
443
+ </p>
444
+ ) : (
445
+ data.focusBlocks.map((f, i) => (
446
+ <div
447
+ key={`${f.startedAt}-${i}`}
448
+ className="rounded-xl border border-slate-100 bg-gradient-to-br from-white to-sky-50/40 px-3 py-2.5"
449
+ >
450
+ <p className="text-sm font-semibold tabular-nums text-slate-900">
451
+ {formatDuration(f.durationMs)}
452
+ </p>
453
+ <p className="mt-0.5 text-[11px] text-slate-500">
454
+ {formatTs(f.startedAt)} · {f.events} événements
455
+ </p>
456
+ </div>
457
+ ))
458
+ )}
459
+ </div>
460
+ </div>
461
+ </>
462
+ ) : (
463
+ <p className="rounded-2xl border border-dashed border-slate-200 bg-white py-10 text-center text-sm text-slate-400">
464
+ Sélectionnez un collaborateur dans le classement pour voir sa heatmap détaillée.
465
+ </p>
466
+ )}
467
+ </div>
468
+ );
469
+ }
470
+
471
+ function MiniKpi({
472
+ label,
473
+ value,
474
+ hint,
475
+ icon: Icon,
476
+ accent,
477
+ }: {
478
+ label: string;
479
+ value: string;
480
+ hint?: string;
481
+ icon: React.ComponentType<{ className?: string }>;
482
+ accent: string;
483
+ }) {
484
+ return (
485
+ <div className="relative overflow-hidden rounded-2xl border border-slate-200/80 bg-white p-4 shadow-sm">
486
+ <div
487
+ className={cn(
488
+ "pointer-events-none absolute -right-6 -top-6 h-24 w-24 rounded-full opacity-[0.12]",
489
+ accent,
490
+ )}
491
+ />
492
+ <div className="flex items-start justify-between gap-3">
493
+ <div>
494
+ <p className="text-xs font-medium uppercase tracking-wide text-slate-500">
495
+ {label}
496
+ </p>
497
+ <p className="mt-1 text-2xl font-semibold tabular-nums tracking-tight text-slate-900">
498
+ {value}
499
+ </p>
500
+ {hint ? <p className="mt-1 text-xs text-slate-500">{hint}</p> : null}
501
+ </div>
502
+ <div
503
+ className={cn(
504
+ "flex h-10 w-10 items-center justify-center rounded-xl text-white shadow-sm",
505
+ accent,
506
+ )}
507
+ >
508
+ <Icon className="h-5 w-5" />
509
+ </div>
510
+ </div>
511
+ </div>
512
+ );
513
+ }