nodefony 7.0.2 → 10.0.0-alpha.2

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 (903) hide show
  1. package/.ai/symbols.json +36135 -0
  2. package/LICENSE +544 -0
  3. package/README.md +316 -694
  4. package/bin/nodefony +2183 -85
  5. package/dist/client/Container.js +367 -0
  6. package/dist/client/Event.js +198 -0
  7. package/dist/client/Service.js +344 -0
  8. package/dist/client/Tools.js +162 -0
  9. package/dist/client/client/ClientKernel.js +276 -0
  10. package/dist/client/client/angular/index.js +352 -0
  11. package/dist/client/client/announce.js +212 -0
  12. package/dist/client/client/debugbar/DebugBar.js +1624 -0
  13. package/dist/client/client/debugbar/format.js +103 -0
  14. package/dist/client/client/debugbar/hmr.js +37 -0
  15. package/dist/client/client/debugbar/index.js +29 -0
  16. package/dist/client/client/debugbar/model.js +133 -0
  17. package/dist/client/client/debugbar/network.js +173 -0
  18. package/dist/client/client/debugbar/profile.js +114 -0
  19. package/dist/client/client/index.js +34 -0
  20. package/dist/client/client/react/index.js +268 -0
  21. package/dist/client/client/realtime/AdaptiveRate.js +240 -0
  22. package/dist/client/client/realtime/BrowserWsTransport.js +57 -0
  23. package/dist/client/client/realtime/RealtimeClient.js +898 -0
  24. package/dist/client/client/realtime/localEvents.js +56 -0
  25. package/dist/client/client/realtime/notice.js +125 -0
  26. package/dist/client/client/realtime/observe.js +280 -0
  27. package/dist/client/client/roles/index.js +3 -0
  28. package/dist/client/client/roles/registry.js +97 -0
  29. package/dist/client/client/roles/roles.js +91 -0
  30. package/dist/client/client/shim/events.js +88 -0
  31. package/dist/client/client/shim/util.js +20 -0
  32. package/dist/client/client/svelte/index.js +319 -0
  33. package/dist/client/client/syslog/context.js +122 -0
  34. package/dist/client/client/syslog/errors.js +49 -0
  35. package/dist/client/client/syslog/uplink.js +106 -0
  36. package/dist/client/client/vue/index.js +321 -0
  37. package/dist/client/colors.js +63 -0
  38. package/dist/client/debugbar.standalone.js +4274 -0
  39. package/dist/client/realtime/IRealtimeTransport.js +32 -0
  40. package/dist/client/realtime/JsonRpcPeer.js +251 -0
  41. package/dist/client/realtime/channelRate.js +39 -0
  42. package/dist/client/realtime/platformChannels.js +155 -0
  43. package/dist/client/syslog/Pdu.js +224 -0
  44. package/dist/client/syslog/Syslog.js +1159 -0
  45. package/dist/client/syslog/drivers/pduFlow.js +106 -0
  46. package/dist/client/syslog/drivers/pduProtocol.js +28 -0
  47. package/dist/client/syslog/logColor.js +80 -0
  48. package/dist/client/types/src/Container.d.ts +221 -0
  49. package/dist/client/types/src/Event.d.ts +171 -0
  50. package/dist/client/types/src/FileClass.d.ts +184 -0
  51. package/dist/client/types/src/Service.d.ts +165 -0
  52. package/dist/client/types/src/Tools.d.ts +116 -0
  53. package/dist/client/types/src/client/ClientKernel.d.ts +90 -0
  54. package/dist/client/types/src/client/IClientKernel.d.ts +195 -0
  55. package/dist/client/types/src/client/angular/index.d.ts +273 -0
  56. package/dist/client/types/src/client/announce.d.ts +74 -0
  57. package/dist/client/types/src/client/debugbar/DebugBar.d.ts +279 -0
  58. package/dist/client/types/src/client/debugbar/format.d.ts +48 -0
  59. package/dist/client/types/src/client/debugbar/hmr.d.ts +16 -0
  60. package/dist/client/types/src/client/debugbar/index.d.ts +30 -0
  61. package/dist/client/types/src/client/debugbar/model.d.ts +158 -0
  62. package/dist/client/types/src/client/debugbar/network.d.ts +55 -0
  63. package/dist/client/types/src/client/debugbar/profile.d.ts +99 -0
  64. package/dist/client/types/src/client/index.d.ts +47 -0
  65. package/dist/client/types/src/client/react/index.d.ts +207 -0
  66. package/dist/client/types/src/client/realtime/AdaptiveRate.d.ts +138 -0
  67. package/dist/client/types/src/client/realtime/BrowserWsTransport.d.ts +27 -0
  68. package/dist/client/types/src/client/realtime/RealtimeClient.d.ts +573 -0
  69. package/dist/client/types/src/client/realtime/localEvents.d.ts +53 -0
  70. package/dist/client/types/src/client/realtime/notice.d.ts +70 -0
  71. package/dist/client/types/src/client/realtime/observe.d.ts +284 -0
  72. package/dist/client/types/src/client/roles/index.d.ts +15 -0
  73. package/dist/client/types/src/client/roles/registry.d.ts +72 -0
  74. package/dist/client/types/src/client/roles/roles.d.ts +68 -0
  75. package/dist/client/types/src/client/shim/cli-color.d.ts +2 -0
  76. package/dist/client/types/src/client/shim/events.d.ts +39 -0
  77. package/dist/client/types/src/client/shim/util.d.ts +14 -0
  78. package/dist/client/types/src/client/svelte/index.d.ts +216 -0
  79. package/dist/client/types/src/client/syslog/context.d.ts +76 -0
  80. package/dist/client/types/src/client/syslog/errors.d.ts +46 -0
  81. package/dist/client/types/src/client/syslog/uplink.d.ts +89 -0
  82. package/dist/client/types/src/client/transport/websocket.d.ts +5 -0
  83. package/dist/client/types/src/client/vue/index.d.ts +234 -0
  84. package/dist/client/types/src/colors.d.ts +32 -0
  85. package/dist/client/types/src/config/infra.d.ts +171 -0
  86. package/dist/client/types/src/kernel/bootReport.d.ts +112 -0
  87. package/dist/client/types/src/kernel/readinessRegistry.d.ts +104 -0
  88. package/dist/client/types/src/realtime/IRealtimeSocket.d.ts +142 -0
  89. package/dist/client/types/src/realtime/IRealtimeTransport.d.ts +50 -0
  90. package/dist/client/types/src/realtime/JsonRpcPeer.d.ts +251 -0
  91. package/dist/client/types/src/realtime/RealtimeEventMap.d.ts +269 -0
  92. package/dist/client/types/src/realtime/channelRate.d.ts +55 -0
  93. package/dist/client/types/src/realtime/platformChannels.d.ts +147 -0
  94. package/dist/client/types/src/syslog/Pdu.d.ts +134 -0
  95. package/dist/client/types/src/syslog/Syslog.d.ts +482 -0
  96. package/dist/client/types/src/syslog/drivers/ILogDriver.d.ts +177 -0
  97. package/dist/client/types/src/syslog/drivers/pduFlow.d.ts +53 -0
  98. package/dist/client/types/src/syslog/drivers/pduProtocol.d.ts +20 -0
  99. package/dist/client/types/src/syslog/logColor.d.ts +59 -0
  100. package/dist/client/types/src/types/ICliKernel.d.ts +28 -0
  101. package/dist/client/types/src/types/ICommand.d.ts +14 -0
  102. package/dist/client/types/src/types/IContainer.d.ts +35 -0
  103. package/dist/client/types/src/types/IKernel.d.ts +138 -0
  104. package/dist/client/types/src/types/IMcpTool.d.ts +157 -0
  105. package/dist/client/types/src/types/IModule.d.ts +73 -0
  106. package/dist/client/types/src/types/IPage.d.ts +119 -0
  107. package/dist/client/types/src/types/IService.d.ts +62 -0
  108. package/dist/client/types/src/types/ISyslog.d.ts +58 -0
  109. package/dist/client/types/src/types/ITransport.d.ts +5 -0
  110. package/dist/client/types/src/types/globals.d.ts +47 -0
  111. package/dist/node/Cli.js +750 -0
  112. package/dist/node/Container.js +367 -0
  113. package/dist/node/Error.js +309 -0
  114. package/dist/node/Event.js +198 -0
  115. package/dist/node/FileClass.js +308 -0
  116. package/dist/node/Nodefony.js +78 -0
  117. package/dist/node/Service.js +344 -0
  118. package/dist/node/Tools.js +205 -0
  119. package/dist/node/bin/resolveLocalCli.js +10 -0
  120. package/dist/node/bundler/index.js +103 -0
  121. package/dist/node/cli/agentTargets.js +461 -0
  122. package/dist/node/cli/aiMcp.js +553 -0
  123. package/dist/node/cli/aiMcpReport.js +129 -0
  124. package/dist/node/cli/aiSync.js +323 -0
  125. package/dist/node/cli/aiSyncReport.js +142 -0
  126. package/dist/node/cli/card.js +186 -0
  127. package/dist/node/cli/cardReport.js +110 -0
  128. package/dist/node/cli/completion.js +415 -0
  129. package/dist/node/cli/create.js +805 -0
  130. package/dist/node/cli/env.js +332 -0
  131. package/dist/node/cli/envReport.js +159 -0
  132. package/dist/node/cli/execPortable.js +60 -0
  133. package/dist/node/cli/gitHooks.js +191 -0
  134. package/dist/node/cli/gitHooksReport.js +119 -0
  135. package/dist/node/cli/globalFlags.js +67 -0
  136. package/dist/node/cli/helpReport.js +288 -0
  137. package/dist/node/cli/manPage.js +165 -0
  138. package/dist/node/cli/nodefonyBin.js +61 -0
  139. package/dist/node/cli/outdated.js +144 -0
  140. package/dist/node/cli/progress.js +555 -0
  141. package/dist/node/cli/projectRoot.js +32 -0
  142. package/dist/node/cli/promptPassword.js +44 -0
  143. package/dist/node/cli/prompts.js +86 -0
  144. package/dist/node/cli/scaffold/destination.js +92 -0
  145. package/dist/node/cli/scaffold/engine.js +2156 -0
  146. package/dist/node/cli/scaffold/entityFields.js +589 -0
  147. package/dist/node/cli/scaffold/format.js +243 -0
  148. package/dist/node/cli/scaffold/interactive.js +124 -0
  149. package/dist/node/cli/scaffold/moduleLayout.js +38 -0
  150. package/dist/node/cli/scaffold/reservedEntities.js +74 -0
  151. package/dist/node/cli/scaffold/spec.js +704 -0
  152. package/dist/node/cli/scaffold/steps.js +40 -0
  153. package/dist/node/cli/scaffold/userContractSource.js +133 -0
  154. package/dist/node/cli/scaffold/versions.js +57 -0
  155. package/dist/node/cli/scaffold/writer.js +172 -0
  156. package/dist/node/cli/startMenu.js +496 -0
  157. package/dist/node/cli/symbols.js +205 -0
  158. package/dist/node/cli/sysexits.js +34 -0
  159. package/dist/node/cli/tableReport.js +184 -0
  160. package/dist/node/cli/usageReport.js +144 -0
  161. package/dist/node/colors.js +63 -0
  162. package/dist/node/command/Builder.js +153 -0
  163. package/dist/node/command/Command.js +348 -0
  164. package/dist/node/config/configProvenance.js +101 -0
  165. package/dist/node/config/defaults.js +39 -0
  166. package/dist/node/config/defineConfig.js +129 -0
  167. package/dist/node/config/defineEnv.js +264 -0
  168. package/dist/node/config/envExample.js +41 -0
  169. package/dist/node/config/envOverride.js +423 -0
  170. package/dist/node/config/infra.js +163 -0
  171. package/dist/node/config/reactivity.js +23 -0
  172. package/dist/node/config/reservedEnv.js +118 -0
  173. package/dist/node/config/schema.js +134 -0
  174. package/dist/node/config/use.js +24 -0
  175. package/dist/node/finder/File.js +49 -0
  176. package/dist/node/finder/FileResult.js +86 -0
  177. package/dist/node/finder/Finder.js +201 -0
  178. package/dist/node/finder/Result.js +45 -0
  179. package/dist/node/index.js +121 -0
  180. package/dist/node/kernel/BootConfigurationError.js +54 -0
  181. package/dist/node/kernel/CliKernel.js +630 -0
  182. package/dist/node/kernel/Kernel.js +2198 -0
  183. package/dist/node/kernel/Module.js +453 -0
  184. package/dist/node/kernel/adminPlane/adminCaller.js +98 -0
  185. package/dist/node/kernel/adminPlane/adminRbac.js +51 -0
  186. package/dist/node/kernel/adminPlane/catalog.js +158 -0
  187. package/dist/node/kernel/adminPlane/executeAdmin.js +78 -0
  188. package/dist/node/kernel/checks/deep.js +335 -0
  189. package/dist/node/kernel/checks/freshness.js +139 -0
  190. package/dist/node/kernel/checks/gating.js +231 -0
  191. package/dist/node/kernel/checks/guards.js +232 -0
  192. package/dist/node/kernel/checks/lastBoot.js +136 -0
  193. package/dist/node/kernel/checks/live.js +330 -0
  194. package/dist/node/kernel/checks/packageDeps.js +292 -0
  195. package/dist/node/kernel/checks/readiness.js +153 -0
  196. package/dist/node/kernel/checks/renderReport.js +780 -0
  197. package/dist/node/kernel/checks/report.js +421 -0
  198. package/dist/node/kernel/checks/runDoctor.js +731 -0
  199. package/dist/node/kernel/checks/surface.js +364 -0
  200. package/dist/node/kernel/checks/walk.js +100 -0
  201. package/dist/node/kernel/checks/wiring.js +450 -0
  202. package/dist/node/kernel/commands/AiMcpCommand.js +65 -0
  203. package/dist/node/kernel/commands/AiSyncCommand.js +48 -0
  204. package/dist/node/kernel/commands/BuildCommand.js +49 -0
  205. package/dist/node/kernel/commands/CardCommand.js +50 -0
  206. package/dist/node/kernel/commands/ClusterCommand.js +58 -0
  207. package/dist/node/kernel/commands/CompletionCommand.js +31 -0
  208. package/dist/node/kernel/commands/CreateCommand.js +45 -0
  209. package/dist/node/kernel/commands/DevCommand.js +99 -0
  210. package/dist/node/kernel/commands/DoctorCommand.js +115 -0
  211. package/dist/node/kernel/commands/EnvCommand.js +52 -0
  212. package/dist/node/kernel/commands/GitHooksCommand.js +47 -0
  213. package/dist/node/kernel/commands/InspectCommand.js +153 -0
  214. package/dist/node/kernel/commands/InstallCommand.js +20 -0
  215. package/dist/node/kernel/commands/MenuCommand.js +261 -0
  216. package/dist/node/kernel/commands/OutdatedCommand.js +164 -0
  217. package/dist/node/kernel/commands/ProdCommand.js +60 -0
  218. package/dist/node/kernel/commands/StatusCommand.js +30 -0
  219. package/dist/node/kernel/commands/StopCommand.js +35 -0
  220. package/dist/node/kernel/commands/SymbolsCommand.js +45 -0
  221. package/dist/node/kernel/commands/runtimeLauncher.js +95 -0
  222. package/dist/node/kernel/decorators/kernelDecorator.js +110 -0
  223. package/dist/node/kernel/injector/injector.js +137 -0
  224. package/dist/node/kernel/injector/serviceOrder.js +106 -0
  225. package/dist/node/kernel/inspect/adminSubjects.js +194 -0
  226. package/dist/node/kernel/inspect/docOutline.js +86 -0
  227. package/dist/node/kernel/lifecycleTags.js +76 -0
  228. package/dist/node/kernel/moduleConfig.js +75 -0
  229. package/dist/node/kernel/moduleGating.js +68 -0
  230. package/dist/node/kernel/readinessRegistry.js +98 -0
  231. package/dist/node/kernel/resolveModuleEntry.js +61 -0
  232. package/dist/node/mcp/caller.js +40 -0
  233. package/dist/node/mcp/guard.js +51 -0
  234. package/dist/node/mcp/protocol.js +157 -0
  235. package/dist/node/mcp/server.js +216 -0
  236. package/dist/node/mcp/tools.js +861 -0
  237. package/dist/node/oauth/authorizationServer.js +221 -0
  238. package/dist/node/oauth/protectedResource.js +293 -0
  239. package/dist/node/package.js +4 -0
  240. package/dist/node/realtime/IRealtimeTransport.js +32 -0
  241. package/dist/node/realtime/JsonRpcPeer.js +251 -0
  242. package/dist/node/realtime/RealtimeEventMap.js +15 -0
  243. package/dist/node/realtime/channelRate.js +39 -0
  244. package/dist/node/realtime/platformChannels.js +155 -0
  245. package/dist/node/runtime/GcScheduler.js +92 -0
  246. package/dist/node/runtime/RequestContext.js +76 -0
  247. package/dist/node/runtime/bearer.js +84 -0
  248. package/dist/node/runtime/engineEnvironment.js +79 -0
  249. package/dist/node/runtime/loadEnv.js +105 -0
  250. package/dist/node/runtime/pageFacets.js +85 -0
  251. package/dist/node/runtime/pageFilters.js +103 -0
  252. package/dist/node/runtime/pageGuard.js +84 -0
  253. package/dist/node/runtime/pageQuery.js +160 -0
  254. package/dist/node/runtime/pageSort.js +114 -0
  255. package/dist/node/runtime/redact.js +43 -0
  256. package/dist/node/runtime/withTimeout.js +71 -0
  257. package/dist/node/service/cluster/ClusterManager.js +202 -0
  258. package/dist/node/service/cluster/ClusterProbeAggregator.js +120 -0
  259. package/dist/node/service/cluster/ClusterRelay.js +74 -0
  260. package/dist/node/service/cluster/clusterMaster.js +65 -0
  261. package/dist/node/service/cluster/clusterMessage.js +70 -0
  262. package/dist/node/service/cluster/cpuQuota.js +59 -0
  263. package/dist/node/service/cluster/instanceProbe.js +52 -0
  264. package/dist/node/service/cluster/podEnvironment.js +48 -0
  265. package/dist/node/service/cluster/processProbe.js +70 -0
  266. package/dist/node/service/cluster/richProcessProbe.js +115 -0
  267. package/dist/node/service/cluster/topology.js +71 -0
  268. package/dist/node/service/dev/BootReporter.js +490 -0
  269. package/dist/node/service/dev/DevSupervisor.js +1004 -0
  270. package/dist/node/service/dev/bootVerdict.js +158 -0
  271. package/dist/node/service/dev/detachedStart.js +372 -0
  272. package/dist/node/service/dev/devProcess.js +1137 -0
  273. package/dist/node/service/dev/devProjects.js +138 -0
  274. package/dist/node/service/dev/devStatusReport.js +395 -0
  275. package/dist/node/service/dev/devStop.js +263 -0
  276. package/dist/node/service/fetchService.js +35 -0
  277. package/dist/node/service/gitService.js +62 -0
  278. package/dist/node/syslog/Pdu.js +224 -0
  279. package/dist/node/syslog/Syslog.js +1159 -0
  280. package/dist/node/syslog/drivers/ClusterFileLogDriver.js +114 -0
  281. package/dist/node/syslog/drivers/FileLogDriver.js +127 -0
  282. package/dist/node/syslog/drivers/ILogDriver.js +26 -0
  283. package/dist/node/syslog/drivers/LokiLogDriver.js +144 -0
  284. package/dist/node/syslog/drivers/MemoryLogDriver.js +33 -0
  285. package/dist/node/syslog/drivers/OpenSearchLogDriver.js +159 -0
  286. package/dist/node/syslog/drivers/builtinLogDrivers.js +114 -0
  287. package/dist/node/syslog/drivers/filterPdus.js +71 -0
  288. package/dist/node/syslog/drivers/logDriverRegistry.js +83 -0
  289. package/dist/node/syslog/drivers/opensearchShared.js +17 -0
  290. package/dist/node/syslog/drivers/pduFlow.js +106 -0
  291. package/dist/node/syslog/drivers/pduProtocol.js +28 -0
  292. package/dist/node/syslog/httpFetch.js +43 -0
  293. package/dist/node/syslog/logColor.js +80 -0
  294. package/dist/node/syslog/sinks/FileSink.js +142 -0
  295. package/dist/node/syslog/transports/BatchingHttpTransport.js +98 -0
  296. package/dist/node/syslog/transports/ConsoleTransport.js +14 -0
  297. package/dist/node/syslog/transports/FileTransport.js +17 -0
  298. package/dist/node/syslog/transports/HttpTransport.js +44 -0
  299. package/dist/node/syslog/transports/LokiTransport.js +80 -0
  300. package/dist/node/syslog/transports/OpenSearchTransport.js +53 -0
  301. package/dist/node/syslog/transports/SyslogTransport.js +13 -0
  302. package/dist/node/syslog/transports/index.js +8 -0
  303. package/dist/node/testing/index.js +190 -0
  304. package/dist/types/Cli.d.ts +177 -0
  305. package/dist/types/Container.d.ts +221 -0
  306. package/dist/types/Error.d.ts +162 -0
  307. package/dist/types/Event.d.ts +171 -0
  308. package/dist/types/FileClass.d.ts +184 -0
  309. package/dist/types/Nodefony.d.ts +66 -0
  310. package/dist/types/Service.d.ts +165 -0
  311. package/dist/types/Tools.d.ts +116 -0
  312. package/dist/types/bin/resolveLocalCli.d.ts +49 -0
  313. package/dist/types/bundler/index.d.ts +66 -0
  314. package/dist/types/cli/agentTargets.d.ts +378 -0
  315. package/dist/types/cli/aiMcp.d.ts +239 -0
  316. package/dist/types/cli/aiMcpReport.d.ts +113 -0
  317. package/dist/types/cli/aiSync.d.ts +68 -0
  318. package/dist/types/cli/aiSyncReport.d.ts +149 -0
  319. package/dist/types/cli/card.d.ts +49 -0
  320. package/dist/types/cli/cardReport.d.ts +127 -0
  321. package/dist/types/cli/completion.d.ts +154 -0
  322. package/dist/types/cli/create.d.ts +177 -0
  323. package/dist/types/cli/env.d.ts +103 -0
  324. package/dist/types/cli/envReport.d.ts +153 -0
  325. package/dist/types/cli/execPortable.d.ts +54 -0
  326. package/dist/types/cli/gitHooks.d.ts +48 -0
  327. package/dist/types/cli/gitHooksReport.d.ts +101 -0
  328. package/dist/types/cli/globalFlags.d.ts +48 -0
  329. package/dist/types/cli/helpReport.d.ts +110 -0
  330. package/dist/types/cli/manPage.d.ts +21 -0
  331. package/dist/types/cli/nodefonyBin.d.ts +45 -0
  332. package/dist/types/cli/outdated.d.ts +119 -0
  333. package/dist/types/cli/progress.d.ts +349 -0
  334. package/dist/types/cli/projectRoot.d.ts +19 -0
  335. package/dist/types/cli/promptPassword.d.ts +22 -0
  336. package/dist/types/cli/prompts.d.ts +102 -0
  337. package/dist/types/cli/scaffold/destination.d.ts +70 -0
  338. package/dist/types/cli/scaffold/engine.d.ts +545 -0
  339. package/dist/types/cli/scaffold/entityFields.d.ts +238 -0
  340. package/dist/types/cli/scaffold/format.d.ts +78 -0
  341. package/dist/types/cli/scaffold/interactive.d.ts +11 -0
  342. package/dist/types/cli/scaffold/moduleLayout.d.ts +69 -0
  343. package/dist/types/cli/scaffold/reservedEntities.d.ts +49 -0
  344. package/dist/types/cli/scaffold/spec.d.ts +159 -0
  345. package/dist/types/cli/scaffold/steps.d.ts +24 -0
  346. package/dist/types/cli/scaffold/userContractSource.d.ts +86 -0
  347. package/dist/types/cli/scaffold/versions.d.ts +14 -0
  348. package/dist/types/cli/scaffold/writer.d.ts +100 -0
  349. package/dist/types/cli/startMenu.d.ts +195 -0
  350. package/dist/types/cli/symbols.d.ts +84 -0
  351. package/dist/types/cli/sysexits.d.ts +31 -0
  352. package/dist/types/cli/tableReport.d.ts +44 -0
  353. package/dist/types/cli/usageReport.d.ts +108 -0
  354. package/dist/types/client/ClientKernel.d.ts +90 -0
  355. package/dist/types/client/IClientKernel.d.ts +195 -0
  356. package/dist/types/client/angular/index.d.ts +273 -0
  357. package/dist/types/client/announce.d.ts +74 -0
  358. package/dist/types/client/debugbar/DebugBar.d.ts +279 -0
  359. package/dist/types/client/debugbar/format.d.ts +48 -0
  360. package/dist/types/client/debugbar/hmr.d.ts +16 -0
  361. package/dist/types/client/debugbar/index.d.ts +30 -0
  362. package/dist/types/client/debugbar/model.d.ts +158 -0
  363. package/dist/types/client/debugbar/network.d.ts +55 -0
  364. package/dist/types/client/debugbar/profile.d.ts +99 -0
  365. package/dist/types/client/index.d.ts +47 -0
  366. package/dist/types/client/react/index.d.ts +207 -0
  367. package/dist/types/client/realtime/AdaptiveRate.d.ts +138 -0
  368. package/dist/types/client/realtime/BrowserWsTransport.d.ts +27 -0
  369. package/dist/types/client/realtime/RealtimeClient.d.ts +573 -0
  370. package/dist/types/client/realtime/localEvents.d.ts +53 -0
  371. package/dist/types/client/realtime/notice.d.ts +70 -0
  372. package/dist/types/client/realtime/observe.d.ts +284 -0
  373. package/dist/types/client/roles/index.d.ts +15 -0
  374. package/dist/types/client/roles/registry.d.ts +72 -0
  375. package/dist/types/client/roles/roles.d.ts +68 -0
  376. package/dist/types/client/shim/cli-color.d.ts +2 -0
  377. package/dist/types/client/shim/events.d.ts +39 -0
  378. package/dist/types/client/shim/util.d.ts +14 -0
  379. package/dist/types/client/svelte/index.d.ts +216 -0
  380. package/dist/types/client/syslog/context.d.ts +76 -0
  381. package/dist/types/client/syslog/errors.d.ts +46 -0
  382. package/dist/types/client/syslog/uplink.d.ts +89 -0
  383. package/dist/types/client/transport/websocket.d.ts +5 -0
  384. package/dist/types/client/vue/index.d.ts +234 -0
  385. package/dist/types/colors.d.ts +32 -0
  386. package/dist/types/command/Builder.d.ts +47 -0
  387. package/dist/types/command/Command.d.ts +268 -0
  388. package/dist/types/config/configMeta.d.ts +53 -0
  389. package/dist/types/config/configProvenance.d.ts +58 -0
  390. package/dist/types/config/defaults.d.ts +33 -0
  391. package/dist/types/config/defineConfig.d.ts +70 -0
  392. package/dist/types/config/defineEnv.d.ts +170 -0
  393. package/dist/types/config/envExample.d.ts +23 -0
  394. package/dist/types/config/envOverride.d.ts +236 -0
  395. package/dist/types/config/index.d.ts +23 -0
  396. package/dist/types/config/infra.d.ts +171 -0
  397. package/dist/types/config/reactivity.d.ts +36 -0
  398. package/dist/types/config/reservedEnv.d.ts +145 -0
  399. package/dist/types/config/schema.d.ts +113 -0
  400. package/dist/types/config/types.d.ts +374 -0
  401. package/dist/types/config/use.d.ts +80 -0
  402. package/dist/types/finder/File.d.ts +26 -0
  403. package/dist/types/finder/FileResult.d.ts +14 -0
  404. package/dist/types/finder/Finder.d.ts +51 -0
  405. package/dist/types/finder/Result.d.ts +10 -0
  406. package/dist/types/index.d.ts +205 -0
  407. package/dist/types/kernel/BootConfigurationError.d.ts +50 -0
  408. package/dist/types/kernel/CliKernel.d.ts +289 -0
  409. package/dist/types/kernel/Kernel.d.ts +1255 -0
  410. package/dist/types/kernel/Module.d.ts +326 -0
  411. package/dist/types/kernel/adminPlane/adminCaller.d.ts +105 -0
  412. package/dist/types/kernel/adminPlane/adminRbac.d.ts +44 -0
  413. package/dist/types/kernel/adminPlane/catalog.d.ts +122 -0
  414. package/dist/types/kernel/adminPlane/executeAdmin.d.ts +99 -0
  415. package/dist/types/kernel/bootReport.d.ts +112 -0
  416. package/dist/types/kernel/checks/deep.d.ts +122 -0
  417. package/dist/types/kernel/checks/freshness.d.ts +49 -0
  418. package/dist/types/kernel/checks/gating.d.ts +134 -0
  419. package/dist/types/kernel/checks/guards.d.ts +57 -0
  420. package/dist/types/kernel/checks/lastBoot.d.ts +189 -0
  421. package/dist/types/kernel/checks/live.d.ts +110 -0
  422. package/dist/types/kernel/checks/packageDeps.d.ts +44 -0
  423. package/dist/types/kernel/checks/readiness.d.ts +97 -0
  424. package/dist/types/kernel/checks/renderReport.d.ts +107 -0
  425. package/dist/types/kernel/checks/report.d.ts +414 -0
  426. package/dist/types/kernel/checks/runDoctor.d.ts +361 -0
  427. package/dist/types/kernel/checks/surface.d.ts +148 -0
  428. package/dist/types/kernel/checks/walk.d.ts +45 -0
  429. package/dist/types/kernel/checks/wiring.d.ts +54 -0
  430. package/dist/types/kernel/commands/AiMcpCommand.d.ts +39 -0
  431. package/dist/types/kernel/commands/AiSyncCommand.d.ts +28 -0
  432. package/dist/types/kernel/commands/BuildCommand.d.ts +13 -0
  433. package/dist/types/kernel/commands/CardCommand.d.ts +34 -0
  434. package/dist/types/kernel/commands/ClusterCommand.d.ts +29 -0
  435. package/dist/types/kernel/commands/CompletionCommand.d.ts +17 -0
  436. package/dist/types/kernel/commands/CreateCommand.d.ts +16 -0
  437. package/dist/types/kernel/commands/DevCommand.d.ts +40 -0
  438. package/dist/types/kernel/commands/DoctorCommand.d.ts +55 -0
  439. package/dist/types/kernel/commands/EnvCommand.d.ts +35 -0
  440. package/dist/types/kernel/commands/GitHooksCommand.d.ts +27 -0
  441. package/dist/types/kernel/commands/InspectCommand.d.ts +59 -0
  442. package/dist/types/kernel/commands/InstallCommand.d.ts +7 -0
  443. package/dist/types/kernel/commands/MenuCommand.d.ts +58 -0
  444. package/dist/types/kernel/commands/OutdatedCommand.d.ts +47 -0
  445. package/dist/types/kernel/commands/ProdCommand.d.ts +29 -0
  446. package/dist/types/kernel/commands/StatusCommand.d.ts +17 -0
  447. package/dist/types/kernel/commands/StopCommand.d.ts +17 -0
  448. package/dist/types/kernel/commands/SymbolsCommand.d.ts +29 -0
  449. package/dist/types/kernel/commands/runtimeLauncher.d.ts +76 -0
  450. package/dist/types/kernel/decorators/kernelDecorator.d.ts +76 -0
  451. package/dist/types/kernel/injector/injector.d.ts +47 -0
  452. package/dist/types/kernel/injector/serviceOrder.d.ts +29 -0
  453. package/dist/types/kernel/inspect/adminSubjects.d.ts +169 -0
  454. package/dist/types/kernel/inspect/docOutline.d.ts +53 -0
  455. package/dist/types/kernel/lifecycleTags.d.ts +79 -0
  456. package/dist/types/kernel/moduleConfig.d.ts +35 -0
  457. package/dist/types/kernel/moduleGating.d.ts +78 -0
  458. package/dist/types/kernel/readinessRegistry.d.ts +104 -0
  459. package/dist/types/kernel/resolveModuleEntry.d.ts +46 -0
  460. package/dist/types/mcp/caller.d.ts +55 -0
  461. package/dist/types/mcp/guard.d.ts +66 -0
  462. package/dist/types/mcp/protocol.d.ts +163 -0
  463. package/dist/types/mcp/server.d.ts +70 -0
  464. package/dist/types/mcp/tools.d.ts +209 -0
  465. package/dist/types/oauth/authorizationServer.d.ts +204 -0
  466. package/dist/types/oauth/protectedResource.d.ts +339 -0
  467. package/dist/types/realtime/IRealtimeSocket.d.ts +142 -0
  468. package/dist/types/realtime/IRealtimeTransport.d.ts +50 -0
  469. package/dist/types/realtime/JsonRpcPeer.d.ts +251 -0
  470. package/dist/types/realtime/RealtimeEventMap.d.ts +269 -0
  471. package/dist/types/realtime/channelRate.d.ts +55 -0
  472. package/dist/types/realtime/platformChannels.d.ts +147 -0
  473. package/dist/types/runtime/GcScheduler.d.ts +96 -0
  474. package/dist/types/runtime/RequestContext.d.ts +152 -0
  475. package/dist/types/runtime/bearer.d.ts +88 -0
  476. package/dist/types/runtime/engineEnvironment.d.ts +115 -0
  477. package/dist/types/runtime/loadEnv.d.ts +86 -0
  478. package/dist/types/runtime/pageFacets.d.ts +104 -0
  479. package/dist/types/runtime/pageFilters.d.ts +139 -0
  480. package/dist/types/runtime/pageGuard.d.ts +78 -0
  481. package/dist/types/runtime/pageQuery.d.ts +138 -0
  482. package/dist/types/runtime/pageSort.d.ts +86 -0
  483. package/dist/types/runtime/redact.d.ts +27 -0
  484. package/dist/types/runtime/withTimeout.d.ts +51 -0
  485. package/dist/types/service/cluster/ClusterManager.d.ts +112 -0
  486. package/dist/types/service/cluster/ClusterProbeAggregator.d.ts +68 -0
  487. package/dist/types/service/cluster/ClusterRelay.d.ts +57 -0
  488. package/dist/types/service/cluster/clusterMaster.d.ts +40 -0
  489. package/dist/types/service/cluster/clusterMessage.d.ts +94 -0
  490. package/dist/types/service/cluster/cpuQuota.d.ts +37 -0
  491. package/dist/types/service/cluster/instanceProbe.d.ts +82 -0
  492. package/dist/types/service/cluster/podEnvironment.d.ts +35 -0
  493. package/dist/types/service/cluster/processProbe.d.ts +53 -0
  494. package/dist/types/service/cluster/richProcessProbe.d.ts +77 -0
  495. package/dist/types/service/cluster/topology.d.ts +64 -0
  496. package/dist/types/service/dev/BootReporter.d.ts +67 -0
  497. package/dist/types/service/dev/DevSupervisor.d.ts +117 -0
  498. package/dist/types/service/dev/bootVerdict.d.ts +99 -0
  499. package/dist/types/service/dev/detachedStart.d.ts +164 -0
  500. package/dist/types/service/dev/devProcess.d.ts +638 -0
  501. package/dist/types/service/dev/devProjects.d.ts +121 -0
  502. package/dist/types/service/dev/devStatusReport.d.ts +141 -0
  503. package/dist/types/service/dev/devStop.d.ts +57 -0
  504. package/dist/types/service/fetchService.d.ts +29 -0
  505. package/dist/types/service/gitService.d.ts +24 -0
  506. package/dist/types/syslog/Pdu.d.ts +134 -0
  507. package/dist/types/syslog/Syslog.d.ts +482 -0
  508. package/dist/types/syslog/drivers/ClusterFileLogDriver.d.ts +40 -0
  509. package/dist/types/syslog/drivers/FileLogDriver.d.ts +55 -0
  510. package/dist/types/syslog/drivers/ILogDriver.d.ts +177 -0
  511. package/dist/types/syslog/drivers/LokiLogDriver.d.ts +44 -0
  512. package/dist/types/syslog/drivers/MemoryLogDriver.d.ts +20 -0
  513. package/dist/types/syslog/drivers/OpenSearchLogDriver.d.ts +40 -0
  514. package/dist/types/syslog/drivers/builtinLogDrivers.d.ts +43 -0
  515. package/dist/types/syslog/drivers/filterPdus.d.ts +21 -0
  516. package/dist/types/syslog/drivers/logDriverRegistry.d.ts +100 -0
  517. package/dist/types/syslog/drivers/opensearchShared.d.ts +11 -0
  518. package/dist/types/syslog/drivers/pduFlow.d.ts +53 -0
  519. package/dist/types/syslog/drivers/pduProtocol.d.ts +20 -0
  520. package/dist/types/syslog/httpFetch.d.ts +48 -0
  521. package/dist/types/syslog/logColor.d.ts +59 -0
  522. package/dist/types/syslog/sinks/FileSink.d.ts +78 -0
  523. package/dist/types/syslog/transports/BatchingHttpTransport.d.ts +59 -0
  524. package/dist/types/syslog/transports/ConsoleTransport.d.ts +8 -0
  525. package/dist/types/syslog/transports/FileTransport.d.ts +13 -0
  526. package/dist/types/syslog/transports/HttpTransport.d.ts +15 -0
  527. package/dist/types/syslog/transports/LokiTransport.d.ts +45 -0
  528. package/dist/types/syslog/transports/OpenSearchTransport.d.ts +40 -0
  529. package/dist/types/syslog/transports/SyslogTransport.d.ts +9 -0
  530. package/dist/types/syslog/transports/index.d.ts +12 -0
  531. package/dist/types/testing/index.d.ts +197 -0
  532. package/dist/types/types/IAdminApi.d.ts +246 -0
  533. package/dist/types/types/ICliKernel.d.ts +28 -0
  534. package/dist/types/types/ICommand.d.ts +14 -0
  535. package/dist/types/types/IContainer.d.ts +35 -0
  536. package/dist/types/types/IIdempotencyStore.d.ts +159 -0
  537. package/dist/types/types/IKernel.d.ts +138 -0
  538. package/dist/types/types/IMcpTool.d.ts +157 -0
  539. package/dist/types/types/IModule.d.ts +73 -0
  540. package/dist/types/types/IModuleManifest.d.ts +49 -0
  541. package/dist/types/types/IPage.d.ts +119 -0
  542. package/dist/types/types/IService.d.ts +62 -0
  543. package/dist/types/types/ISyslog.d.ts +58 -0
  544. package/dist/types/types/ITransport.d.ts +5 -0
  545. package/dist/types/types/globals.d.ts +47 -0
  546. package/docs/angular-services.md +318 -0
  547. package/docs/catalogue.md +184 -0
  548. package/docs/cli.md +369 -0
  549. package/docs/client.md +616 -0
  550. package/docs/debugbar.md +218 -0
  551. package/docs/environnement.md +308 -0
  552. package/docs/index.md +388 -0
  553. package/docs/kernel.md +670 -0
  554. package/docs/progression.md +228 -0
  555. package/docs/react-hooks.md +686 -0
  556. package/docs/request-context.md +507 -0
  557. package/docs/service.md +590 -0
  558. package/docs/svelte-reactivite.md +293 -0
  559. package/docs/syslog.md +848 -0
  560. package/docs/testing.md +203 -0
  561. package/docs/vue-composables.md +255 -0
  562. package/man/nodefony.1 +152 -0
  563. package/package.json +163 -83
  564. package/templates/app/agents/AGENTS.md.tpl +967 -0
  565. package/templates/app/agents/POINTEUR.md.tpl +12 -0
  566. package/templates/app/base/Dockerfile.tpl +104 -0
  567. package/templates/app/base/README.md.tpl +314 -0
  568. package/templates/app/base/dockerignore.tpl +28 -0
  569. package/templates/app/base/env.ts.tpl +130 -0
  570. package/templates/app/base/gitattributes.tpl +28 -0
  571. package/templates/app/base/github/workflows/ci.yml.tpl +91 -0
  572. package/templates/app/base/gitignore.tpl +46 -0
  573. package/templates/app/base/gitlab-ci.yml.tpl +42 -0
  574. package/templates/app/base/index.ts.tpl +69 -0
  575. package/templates/app/base/nodefony.config.ts.tpl +342 -0
  576. package/templates/app/base/oxlintrc.json.tpl +127 -0
  577. package/templates/app/base/package.json.tpl +75 -0
  578. package/templates/app/base/prettierignore.tpl +24 -0
  579. package/templates/app/base/prettierrc.json.tpl +12 -0
  580. package/templates/app/base/rolldown.config.ts.tpl +8 -0
  581. package/templates/app/base/tests/config.test.ts.tpl +29 -0
  582. package/templates/app/base/tests/e2e.setup.ts.tpl +181 -0
  583. package/templates/app/base/tests/e2e.test.ts.tpl +105 -0
  584. package/templates/app/base/tests/migrations.e2e.test.ts.tpl +579 -0
  585. package/templates/app/base/tsconfig.json.tpl +47 -0
  586. package/templates/app/base/vitest.config.ts.tpl +88 -0
  587. package/templates/app/base/vitest.e2e.config.ts.tpl +39 -0
  588. package/templates/app/complete/compose.yaml.tpl +328 -0
  589. package/templates/app/complete/deploy/migrate-job.yaml.tpl +100 -0
  590. package/templates/app/complete/docker/db/init-nodefony-e2e.sql.tpl +30 -0
  591. package/templates/app/complete/docker/grafana/provisioning/datasources/loki.yaml.tpl +15 -0
  592. package/templates/app/complete/env.local.tpl +11 -0
  593. package/templates/app/complete/env.tpl +53 -0
  594. package/templates/app/complete/nodefony/entity/User.ts.tpl +69 -0
  595. package/templates/app/complete/nodefony/security/provisionUsers.ts.tpl +124 -0
  596. package/templates/app/complete/nodefony/service/AppBannerService.ts.tpl +98 -0
  597. package/templates/app/complete/nodefony/service/AppInfoService.ts.tpl +85 -0
  598. package/templates/app/frontend/angular/frontend/src/accent.css.tpl +34 -0
  599. package/templates/app/frontend/angular/frontend/src/app/app.component.ts.tpl +400 -0
  600. package/templates/app/frontend/react/frontend/src/App.tsx.tpl +421 -0
  601. package/templates/app/frontend/react/frontend/src/accent.css.tpl +30 -0
  602. package/templates/app/frontend/shared/frontend/src/brand.ts.tpl +3 -0
  603. package/templates/app/frontend/shared/frontend/src/showcase.css.tpl +224 -0
  604. package/templates/app/frontend/shared/nodefony/controllers/AppController.ts.tpl +45 -0
  605. package/templates/app/frontend/svelte/frontend/src/App.svelte.tpl +371 -0
  606. package/templates/app/frontend/svelte/frontend/src/accent.css.tpl +34 -0
  607. package/templates/app/frontend/vue/frontend/src/App.vue.tpl +367 -0
  608. package/templates/app/frontend/vue/frontend/src/accent.css.tpl +34 -0
  609. package/templates/app/home/nodefony/controllers/HomeController.ts.tpl +38 -0
  610. package/templates/command/nodefony/command/__NAME__.ts.tpl +132 -0
  611. package/templates/controller/duplex/nodefony/controllers/__NAME__.ts.tpl +188 -0
  612. package/templates/controller/example/nodefony/controllers/__NAME__.ts.tpl +348 -0
  613. package/templates/controller/hello/nodefony/controllers/__NAME__.ts.tpl +143 -0
  614. package/templates/controller/realtime/nodefony/controllers/__NAME__.ts.tpl +153 -0
  615. package/templates/controller/realtime/tests/__KEBAB__-realtime.test.ts.tpl +79 -0
  616. package/templates/controller/rest/nodefony/controllers/__NAME__.ts.tpl +159 -0
  617. package/templates/entity/base/nodefony/entity/__PASCAL__.schema.ts.tpl +31 -0
  618. package/templates/entity/base/nodefony/entity/__PASCAL__.ts.tpl +68 -0
  619. package/templates/entity/controller/nodefony/controllers/__PASCAL__Controller.ts.tpl +335 -0
  620. package/templates/entity/service/nodefony/service/__PASCAL__Service.ts.tpl +126 -0
  621. package/templates/entity/tests/tests/__KEBAB__.e2e.test.ts.tpl +358 -0
  622. package/templates/entity/tests/tests/__KEBAB__.test.ts.tpl +87 -0
  623. package/templates/front/angular/frontend/src/app/app.component.ts.tpl +30 -0
  624. package/templates/front/base/nodefony/controllers/__NAME__.ts.tpl +60 -0
  625. package/templates/front/react/frontend/src/App.tsx.tpl +26 -0
  626. package/templates/front/svelte/frontend/src/App.svelte.tpl +19 -0
  627. package/templates/front/vue/frontend/src/App.vue.tpl +23 -0
  628. package/templates/module/ai/AGENTS.md.tpl +71 -0
  629. package/templates/module/base/README.md.tpl +62 -0
  630. package/templates/module/base/docs/index.md.tpl +55 -0
  631. package/templates/module/base/index.ts.tpl +50 -0
  632. package/templates/module/base/nodefony/config/config.ts.tpl +60 -0
  633. package/templates/module/base/nodefony/config/defineModuleConfig.ts.tpl +55 -0
  634. package/templates/module/base/nodefony/src/errors/__PASCAL__Error.ts.tpl +20 -0
  635. package/templates/module/base/package.json.tpl +53 -0
  636. package/templates/module/base/rolldown.config.ts.tpl +14 -0
  637. package/templates/module/base/tests/__KEBAB__.test.ts.tpl +39 -0
  638. package/templates/module/base/tsconfig.json.tpl +42 -0
  639. package/templates/module/base/vitest.config.ts.tpl +27 -0
  640. package/templates/module/packages/CLAUDE.md.tpl +51 -0
  641. package/templates/module/packages/MEMORY.md.tpl +39 -0
  642. package/templates/module/packages/tsconfig.declarations.json.tpl +19 -0
  643. package/templates/module/packages/tsconfig.tests.json.tpl +11 -0
  644. package/templates/module/service/nodefony/interfaces/I__PASCAL__Service.ts.tpl +14 -0
  645. package/templates/module/service/nodefony/interfaces/index.ts.tpl +1 -0
  646. package/templates/module/service/nodefony/service/__PASCAL__Service.ts.tpl +116 -0
  647. package/templates/service/nodefony/interfaces/I__PASCAL__Service.ts.tpl +19 -0
  648. package/templates/service/nodefony/service/__PASCAL__Service.ts.tpl +136 -0
  649. package/templates/service/tests/__PASCAL__Service.test.ts.tpl +53 -0
  650. package/templates/shared/front-entry/angular/frontend/src/main.ts.tpl +17 -0
  651. package/templates/shared/front-entry/react/frontend/src/main.tsx.tpl +11 -0
  652. package/templates/shared/front-entry/svelte/frontend/src/main.ts.tpl +13 -0
  653. package/templates/shared/front-entry/vue/frontend/src/main.ts.tpl +12 -0
  654. package/templates/shared/front-registrar/nodefony/frontend/register__PASCAL__Entry.ts.tpl +40 -0
  655. package/templates/shared/front-shell/frontend/index.html.tpl +39 -0
  656. package/templates/shared/ng-app-tsconfig/frontend/tsconfig.app.json.tpl +18 -0
  657. package/templates/shared/svelte-shim/frontend/src/env.d.ts.tpl +12 -0
  658. package/templates/shared/vue-shim/frontend/src/env.d.ts.tpl +15 -0
  659. package/.yarnclean +0 -45
  660. package/CHANGELOG.md +0 -406
  661. package/autoloader.es6 +0 -253
  662. package/builder.es6 +0 -366
  663. package/cli/builder/bundles/bundle.js +0 -328
  664. package/cli/builder/bundles/controller.js +0 -73
  665. package/cli/builder/bundles/users-bundle/.yarnclean +0 -45
  666. package/cli/builder/bundles/users-bundle/Command/fixtureTask.js +0 -47
  667. package/cli/builder/bundles/users-bundle/Command/usersCommand.js +0 -105
  668. package/cli/builder/bundles/users-bundle/Entity/mongoose/userEntity.js +0 -168
  669. package/cli/builder/bundles/users-bundle/Entity/sequelize/userEntity.js +0 -231
  670. package/cli/builder/bundles/users-bundle/Fixtures/users.js +0 -117
  671. package/cli/builder/bundles/users-bundle/Fixtures/usersFixtures.js +0 -114
  672. package/cli/builder/bundles/users-bundle/Resources/config/config.js +0 -90
  673. package/cli/builder/bundles/users-bundle/Resources/config/routing.js +0 -17
  674. package/cli/builder/bundles/users-bundle/Resources/config/security.js +0 -96
  675. package/cli/builder/bundles/users-bundle/Resources/config/services.js +0 -7
  676. package/cli/builder/bundles/users-bundle/Resources/config/webpack/webpack.dev.config.js +0 -13
  677. package/cli/builder/bundles/users-bundle/Resources/config/webpack/webpack.prod.config.js +0 -31
  678. package/cli/builder/bundles/users-bundle/Resources/config/webpack.config.js +0 -146
  679. package/cli/builder/bundles/users-bundle/Resources/js/users.js +0 -153
  680. package/cli/builder/bundles/users-bundle/Resources/public/favicon.ico +0 -0
  681. package/cli/builder/bundles/users-bundle/Resources/public/images/users-logo.png +0 -0
  682. package/cli/builder/bundles/users-bundle/Resources/scss/awesome/font-awesome.config.js +0 -11
  683. package/cli/builder/bundles/users-bundle/Resources/scss/custom.scss +0 -23
  684. package/cli/builder/bundles/users-bundle/Resources/scss/users.scss +0 -59
  685. package/cli/builder/bundles/users-bundle/Resources/swagger/openapi/login.js +0 -235
  686. package/cli/builder/bundles/users-bundle/Resources/swagger/openapi/users.js +0 -232
  687. package/cli/builder/bundles/users-bundle/Resources/translations/login.en_en.yml +0 -14
  688. package/cli/builder/bundles/users-bundle/Resources/translations/login.fr_fr.yml +0 -14
  689. package/cli/builder/bundles/users-bundle/Resources/translations/users.en_en.yml +0 -35
  690. package/cli/builder/bundles/users-bundle/Resources/translations/users.fr_fr.yml +0 -36
  691. package/cli/builder/bundles/users-bundle/Resources/views/base.html.twig +0 -32
  692. package/cli/builder/bundles/users-bundle/Resources/views/footer.html.twig +0 -7
  693. package/cli/builder/bundles/users-bundle/Resources/views/header.html.twig +0 -90
  694. package/cli/builder/bundles/users-bundle/Resources/views/login/login.html.twig +0 -43
  695. package/cli/builder/bundles/users-bundle/Resources/views/users/createUser.html.twig +0 -296
  696. package/cli/builder/bundles/users-bundle/Resources/views/users/readUsers.html.twig +0 -94
  697. package/cli/builder/bundles/users-bundle/controller/api/graphql/graphqlController.js +0 -56
  698. package/cli/builder/bundles/users-bundle/controller/api/graphql/userResolver.js +0 -121
  699. package/cli/builder/bundles/users-bundle/controller/api/graphql/usertype.js +0 -58
  700. package/cli/builder/bundles/users-bundle/controller/api/openapi/loginApiController.js +0 -210
  701. package/cli/builder/bundles/users-bundle/controller/api/openapi/restUserController.js +0 -244
  702. package/cli/builder/bundles/users-bundle/controller/loginController.js +0 -91
  703. package/cli/builder/bundles/users-bundle/controller/usersController.js +0 -332
  704. package/cli/builder/bundles/users-bundle/package.json +0 -58
  705. package/cli/builder/bundles/users-bundle/readme.md +0 -360
  706. package/cli/builder/bundles/users-bundle/services/usersService.js +0 -411
  707. package/cli/builder/bundles/users-bundle/src/providers/mongoose/userProvider.js +0 -47
  708. package/cli/builder/bundles/users-bundle/src/providers/sequelize/userProvider.js +0 -48
  709. package/cli/builder/bundles/users-bundle/tests/loginTest.js +0 -182
  710. package/cli/builder/bundles/users-bundle/usersBundle.js +0 -26
  711. package/cli/builder/microService/microService.js +0 -261
  712. package/cli/builder/microService/skeletons/.editorconfig +0 -18
  713. package/cli/builder/microService/skeletons/.env-cmdrc.js +0 -32
  714. package/cli/builder/microService/skeletons/.jshintrc +0 -37
  715. package/cli/builder/microService/skeletons/README.md +0 -117
  716. package/cli/builder/microService/skeletons/bin/bash/hello.sh +0 -5
  717. package/cli/builder/microService/skeletons/bin/cli +0 -13
  718. package/cli/builder/microService/skeletons/bin/python/hello.py +0 -20
  719. package/cli/builder/microService/skeletons/config/config.js +0 -29
  720. package/cli/builder/microService/skeletons/config/pm2.config.js +0 -72
  721. package/cli/builder/microService/skeletons/config/webpack/webpack.config.dev.js +0 -8
  722. package/cli/builder/microService/skeletons/config/webpack/webpack.config.prod.js +0 -25
  723. package/cli/builder/microService/skeletons/config/webpack.config.js +0 -114
  724. package/cli/builder/microService/skeletons/package.json +0 -76
  725. package/cli/builder/microService/skeletons/src/browser/index.css +0 -3
  726. package/cli/builder/microService/skeletons/src/browser/index.js +0 -16
  727. package/cli/builder/microService/skeletons/src/browser/socketio.js +0 -46
  728. package/cli/builder/microService/skeletons/src/cli/cli.js +0 -224
  729. package/cli/builder/microService/skeletons/src/cli/menu.js +0 -28
  730. package/cli/builder/microService/skeletons/src/n-api/README.md +0 -1
  731. package/cli/builder/microService/skeletons/src/n-api/binding.gyp +0 -14
  732. package/cli/builder/microService/skeletons/src/n-api/hello.cc +0 -14
  733. package/cli/builder/microService/skeletons/src/n-api/hello.js +0 -23
  734. package/cli/builder/microService/skeletons/src/n-api/package.json +0 -15
  735. package/cli/builder/microService/skeletons/src/node/examples/index.js +0 -120
  736. package/cli/builder/microService/skeletons/src/node/index.js +0 -102
  737. package/cli/builder/microService/skeletons/src/node/services/markdown/markdown.js +0 -27
  738. package/cli/builder/microService/skeletons/src/node/services/servers/http.js +0 -45
  739. package/cli/builder/microService/skeletons/src/node/services/servers/https.js +0 -44
  740. package/cli/builder/microService/skeletons/src/node/services/socketio/socketio.js +0 -46
  741. package/cli/builder/microService/skeletons/src/node/services/syscall/syscall.js +0 -111
  742. package/cli/builder/microService/skeletons/src/node/services/worker/thread.js +0 -59
  743. package/cli/builder/microService/skeletons/src/node/services/worker/worker.js +0 -60
  744. package/cli/builder/microService/skeletons/src/templates/base.html +0 -12
  745. package/cli/builder/microService/skeletons/src/templates/socket.html +0 -23
  746. package/cli/builder/microService/skeletons/tests/unit/serviceTest.js +0 -30
  747. package/cli/builder/project/project.js +0 -474
  748. package/cli/builder/project/skeletons/README.md +0 -778
  749. package/cli/builder/project/skeletons/bin/dev-deploy.sh +0 -5
  750. package/cli/builder/project/skeletons/bin/generateCertificates.sh.skeleton +0 -180
  751. package/cli/builder/project/skeletons/bin/prod-deploy.sh +0 -3
  752. package/cli/builder/project/skeletons/config/config.js.skeleton +0 -178
  753. package/cli/builder/project/skeletons/config/openssl/ca/openssl.cnf.skeleton +0 -150
  754. package/cli/builder/project/skeletons/config/openssl/ca_intermediate/openssl.cnf.skeleton +0 -152
  755. package/cli/builder/project/skeletons/config/pm2.config.js.skeleton +0 -62
  756. package/cli/builder/project/skeletons/documentation.html.twig +0 -24
  757. package/cli/builder/project/skeletons/editorconfig.skeleton +0 -18
  758. package/cli/builder/project/skeletons/eslintignore.skeleton +0 -15
  759. package/cli/builder/project/skeletons/eslintrc.js.skeleton +0 -97
  760. package/cli/builder/project/skeletons/gitignore.skeleton +0 -32
  761. package/cli/builder/project/skeletons/migrations/2022.12.25T17.37.37.entity-user.js +0 -137
  762. package/cli/builder/project/skeletons/migrations/2022.12.25T17.37.38.entity-session.js +0 -97
  763. package/cli/builder/project/skeletons/migrations/2022.12.25T17.37.39.entity-requests.js +0 -115
  764. package/cli/builder/project/skeletons/migrations/2022.12.25T17.37.40.entity-jwts.js +0 -97
  765. package/cli/builder/project/skeletons/migrations/migrations.skeleton.js +0 -101
  766. package/cli/builder/project/skeletons/package.json.twig +0 -71
  767. package/cli/builder/react/reactBuilder.js +0 -224
  768. package/cli/builder/react/skeletons/app/Resources/databases/nodefony.db +0 -0
  769. package/cli/builder/react/skeletons/app/Resources/translations/messages.en_en.yml +0 -5
  770. package/cli/builder/react/skeletons/app/Resources/translations/messages.fr_fr.yml +0 -5
  771. package/cli/builder/react/skeletons/app/Resources/views/base.html.twig +0 -33
  772. package/cli/builder/react/skeletons/app/config/config.js +0 -392
  773. package/cli/builder/react/skeletons/app/config/routing.js +0 -10
  774. package/cli/builder/react/skeletons/app/config/security.js +0 -74
  775. package/cli/builder/react/skeletons/app/controller/appController.js +0 -22
  776. package/cli/builder/sandbox/sandBoxBuilder.js +0 -610
  777. package/cli/builder/sandbox/skeletons/Resources/css/entry.css +0 -48
  778. package/cli/builder/sandbox/skeletons/Resources/js/entry.js +0 -60
  779. package/cli/builder/sandbox/skeletons/Resources/public/manifest.json +0 -32
  780. package/cli/builder/sandbox/skeletons/Resources/translations/messages.en_en.yml +0 -5
  781. package/cli/builder/sandbox/skeletons/Resources/translations/messages.fr_fr.yml +0 -5
  782. package/cli/builder/sandbox/skeletons/Resources/views/base.html.twig +0 -46
  783. package/cli/builder/sandbox/skeletons/Resources/views/index.html.twig +0 -34
  784. package/cli/builder/sandbox/skeletons/bootstrap/custom.scss +0 -27
  785. package/cli/builder/sandbox/skeletons/bootstrap/entry.scss +0 -60
  786. package/cli/builder/sandbox/skeletons/workbox/templates/index.html.twig +0 -32
  787. package/cli/builder/sandbox/skeletons/workbox/workers/service-worker.js +0 -196
  788. package/cli/builder/skeletons/Resources/public/favicon.ico +0 -0
  789. package/cli/builder/skeletons/Resources/public/images/app-logo.png +0 -0
  790. package/cli/builder/skeletons/Resources/views/framework-bundle/views/401.html.twig +0 -1
  791. package/cli/builder/skeletons/Resources/views/framework-bundle/views/401.json.twig +0 -24
  792. package/cli/builder/skeletons/Resources/views/framework-bundle/views/403.html.twig +0 -1
  793. package/cli/builder/skeletons/Resources/views/framework-bundle/views/403.json.twig +0 -20
  794. package/cli/builder/skeletons/Resources/views/framework-bundle/views/404.html.twig +0 -1
  795. package/cli/builder/skeletons/Resources/views/framework-bundle/views/404.json.twig +0 -20
  796. package/cli/builder/skeletons/Resources/views/framework-bundle/views/500.html.twig +0 -1
  797. package/cli/builder/skeletons/Resources/views/framework-bundle/views/base.html.twig +0 -30
  798. package/cli/builder/skeletons/Resources/views/framework-bundle/views/exception.html.twig +0 -1
  799. package/cli/builder/skeletons/Resources/views/framework-bundle/views/exception.json.twig +0 -24
  800. package/cli/builder/skeletons/Resources/views/framework-bundle/views/footer.html.twig +0 -33
  801. package/cli/builder/skeletons/Resources/views/framework-bundle/views/header.html.twig +0 -6
  802. package/cli/builder/skeletons/Resources/views/framework-bundle/views/index.html.twig +0 -6
  803. package/cli/builder/skeletons/Resources/views/framework-bundle/views/index.json.twig +0 -1
  804. package/cli/builder/skeletons/Resources/views/framework-bundle/views/layout.html.twig +0 -125
  805. package/cli/builder/skeletons/Resources/views/framework-bundle/views/layout.json.twig +0 -5
  806. package/cli/builder/skeletons/Resources/views/framework-bundle/views/timeout.html.twig +0 -1
  807. package/cli/builder/skeletons/Resources/views/framework-bundle/views/timeout.json.twig +0 -24
  808. package/cli/builder/skeletons/app/appKernel.js +0 -133
  809. package/cli/builder/skeletons/binding/binding.cc +0 -26
  810. package/cli/builder/skeletons/binding/binding.skeleton +0 -8
  811. package/cli/builder/skeletons/bundle/bundleClass.js +0 -37
  812. package/cli/builder/skeletons/command/commandClass.js +0 -11
  813. package/cli/builder/skeletons/command/taskClass.js +0 -20
  814. package/cli/builder/skeletons/config/config.js +0 -138
  815. package/cli/builder/skeletons/config/nodefony/elastic-bundle.js +0 -37
  816. package/cli/builder/skeletons/config/nodefony/framework-bundle.js +0 -22
  817. package/cli/builder/skeletons/config/nodefony/http-bundle.js +0 -104
  818. package/cli/builder/skeletons/config/nodefony/mail-bundle.js +0 -44
  819. package/cli/builder/skeletons/config/nodefony/mongoose-bundle.js +0 -51
  820. package/cli/builder/skeletons/config/nodefony/monitoring-bundle.js +0 -34
  821. package/cli/builder/skeletons/config/nodefony/realtime-bundle.js +0 -15
  822. package/cli/builder/skeletons/config/nodefony/redis-bundle.js +0 -44
  823. package/cli/builder/skeletons/config/nodefony/security-bundle.js +0 -25
  824. package/cli/builder/skeletons/config/nodefony/sequelize-bundle.js +0 -156
  825. package/cli/builder/skeletons/config/routing.js +0 -55
  826. package/cli/builder/skeletons/config/security.js +0 -102
  827. package/cli/builder/skeletons/config/services.js +0 -1
  828. package/cli/builder/skeletons/config/webpack/webpack.dev.config.js +0 -13
  829. package/cli/builder/skeletons/config/webpack/webpack.prod.config.js +0 -20
  830. package/cli/builder/skeletons/config/webpack.config.js +0 -148
  831. package/cli/builder/skeletons/controller/controllerClass.js +0 -75
  832. package/cli/builder/skeletons/package.json.twig +0 -57
  833. package/cli/builder/skeletons/unittest/testFile.js +0 -46
  834. package/cli/builder/vue/skeletons/Resources/public/images/app-logo.png +0 -0
  835. package/cli/builder/vue/skeletons/Resources/translations/messages.en_en.yml +0 -5
  836. package/cli/builder/vue/skeletons/Resources/translations/messages.fr_fr.yml +0 -5
  837. package/cli/builder/vue/skeletons/vue.config.js +0 -129
  838. package/cli/builder/vue/vueBuilder.js +0 -410
  839. package/cli/generate/generate.es6 +0 -103
  840. package/cli/install/install.js +0 -190
  841. package/cli/sequelize/sequelize.js +0 -94
  842. package/cli/start.js +0 -702
  843. package/cli/tools/pm2.es6 +0 -112
  844. package/cli/tools/tools.es6 +0 -97
  845. package/cli.es6 +0 -925
  846. package/container.es6 +0 -272
  847. package/error.es6 +0 -293
  848. package/fileClass.es6 +0 -242
  849. package/finder/file.es6 +0 -33
  850. package/finder/fileResult.es6 +0 -138
  851. package/finder/finder2.es6 +0 -275
  852. package/finder.es6 +0 -427
  853. package/kernel/annotations/annotations.es6 +0 -286
  854. package/kernel/api/api.es6 +0 -61
  855. package/kernel/api/graphqlApi.es6 +0 -107
  856. package/kernel/api/jsonApi.es6 +0 -177
  857. package/kernel/api/openApi.es6 +0 -139
  858. package/kernel/api/schemas/openApiSchema.js +0 -394
  859. package/kernel/babylon.es6 +0 -162
  860. package/kernel/bundle.es6 +0 -1200
  861. package/kernel/cliKernel.es6 +0 -869
  862. package/kernel/command.es6 +0 -78
  863. package/kernel/commands/nodefonyCommand.es6 +0 -247
  864. package/kernel/commands/tasks/bundlesTask.es6 +0 -59
  865. package/kernel/controllers/controller.es6 +0 -791
  866. package/kernel/fixture.es6 +0 -19
  867. package/kernel/injections/injections.es6 +0 -424
  868. package/kernel/kernel.es6 +0 -1960
  869. package/kernel/kernelWatcher.es6 +0 -387
  870. package/kernel/orm/entity.es6 +0 -59
  871. package/kernel/orm/orm.es6 +0 -139
  872. package/kernel/reader.es6 +0 -290
  873. package/kernel/security/encoder.es6 +0 -17
  874. package/kernel/security/factories/factory.es6 +0 -111
  875. package/kernel/security/factories/passeportFactory.es6 +0 -61
  876. package/kernel/security/provider.es6 +0 -58
  877. package/kernel/security/providers/chainProvider.es6 +0 -99
  878. package/kernel/security/providers/memoryProvider.es6 +0 -55
  879. package/kernel/security/providers/providerManager.es6 +0 -67
  880. package/kernel/security/providers/userEntityProvider.es6 +0 -50
  881. package/kernel/security/role.es6 +0 -12
  882. package/kernel/security/secureArea.es6 +0 -431
  883. package/kernel/security/tokens/token.es6 +0 -171
  884. package/kernel/security/user.es6 +0 -106
  885. package/kernel/serviceRealTime.es6 +0 -21
  886. package/kernel/services/connectionsService.es6 +0 -93
  887. package/kernel/services/cron/cronService.es6 +0 -123
  888. package/kernel/task.es6 +0 -80
  889. package/kernel/templates.es6 +0 -22
  890. package/kernel/templating/twig.es6 +0 -218
  891. package/kernel/tests/cliTest.js +0 -59
  892. package/kernel/tests/containerTest.js +0 -232
  893. package/kernel/tests/kernelTest.js +0 -39
  894. package/nodefony.es6 +0 -1084
  895. package/notificationsCenter.es6 +0 -118
  896. package/protocol.es6 +0 -86
  897. package/protocols/bayeux.es6 +0 -174
  898. package/protocols/jsonRpc/jsonrpc.es6 +0 -56
  899. package/result.es6 +0 -99
  900. package/service.es6 +0 -288
  901. package/syslog/pdu.es6 +0 -141
  902. package/syslog/syslog.es6 +0 -754
  903. package/watcher.es6 +0 -113
@@ -0,0 +1,4274 @@
1
+ //#region src/client/realtime/notice.ts
2
+ /**
3
+ * Traduit un refus de canal (`realtime:denied`, {@link IRealtimeDenied}) en
4
+ * {@link NodefonyNotice}. Pendant FRAME du {@link closeCodeToNotice} (qui traite
5
+ * la fermeture de la CONNEXION) : ici la connexion vit, seul l'abonnement à CE
6
+ * canal n'a pas abouti.
7
+ *
8
+ * Le message SUIT le motif, parce que les trois n'appellent pas le même geste :
9
+ * un accès refusé demande une élévation de droits, un plafond atteint demande de
10
+ * libérer un abonnement, un canal sans producteur demande de relire son nom. Un
11
+ * « accès refusé » affiché sur une faute de frappe envoie chercher un droit qui
12
+ * n'a jamais manqué. Ce qui reste générique, et le restera, c'est le DÉTAIL de la
13
+ * politique : le serveur ne dit jamais quel rôle ou quel scope manque.
14
+ *
15
+ * @param denied - `{ channel, reason }` reçu du serveur.
16
+ * @returns une notice prête à afficher (toujours, contrairement au close-code).
17
+ */
18
+ function deniedToNotice(denied) {
19
+ const message = denied.reason === "unknown" ? `Canal « ${denied.channel} » inconnu du serveur : personne ne le produit (nom exact ? module chargé ?)` : denied.reason === "limit" ? `Canal « ${denied.channel} » non ouvert : trop d'abonnements sur cette connexion` : `Accès au canal « ${denied.channel} » refusé`;
20
+ return {
21
+ level: denied.reason === "unknown" ? "warning" : "error",
22
+ title: "Temps réel",
23
+ message,
24
+ source: "realtime",
25
+ ts: Date.now()
26
+ };
27
+ }
28
+ /**
29
+ * Traduit un **code de fermeture WebSocket** (RFC 6455 §7.4) en {@link NodefonyNotice}
30
+ * — pendant CLIENT du `toWsCloseCode` serveur (`@nodefony/http`). Pur, sans état,
31
+ * donc trivialement testable et réutilisable.
32
+ *
33
+ * Retourne `null` pour une fermeture **propre** (1000) ou **attendue** (1001 going
34
+ * away = restart serveur, le reconnect rétablit) : pas de bruit inutile.
35
+ *
36
+ * @param code - code de fermeture reçu (`CloseEvent.code`).
37
+ * @param reason - description optionnelle (`CloseEvent.reason`), ajoutée au message.
38
+ * @returns une notice à afficher, ou `null` si la fermeture ne mérite aucune alerte.
39
+ */
40
+ function closeCodeToNotice(code, reason) {
41
+ if (code === 1e3 || code === 1001) return null;
42
+ let level = "warning";
43
+ let message;
44
+ switch (code) {
45
+ case 1002:
46
+ level = "error";
47
+ message = "Erreur de protocole temps réel";
48
+ break;
49
+ case 1003:
50
+ level = "error";
51
+ message = "Données temps réel non supportées";
52
+ break;
53
+ case 1006:
54
+ message = "Connexion temps réel perdue";
55
+ break;
56
+ case 1007:
57
+ level = "error";
58
+ message = "Trame temps réel invalide";
59
+ break;
60
+ case 1008:
61
+ level = "error";
62
+ message = "Accès temps réel refusé";
63
+ break;
64
+ case 1009:
65
+ message = "Message temps réel trop volumineux";
66
+ break;
67
+ case 1010:
68
+ level = "error";
69
+ message = "Extension temps réel requise manquante";
70
+ break;
71
+ case 1011:
72
+ level = "error";
73
+ message = "Erreur serveur temps réel";
74
+ break;
75
+ case 4004:
76
+ level = "error";
77
+ message = "Ressource temps réel introuvable";
78
+ break;
79
+ default: message = typeof code === "number" ? `Connexion temps réel fermée (code ${code})` : "Connexion temps réel fermée";
80
+ }
81
+ const trimmed = reason?.trim();
82
+ return {
83
+ level,
84
+ title: "Temps réel",
85
+ message: trimmed ? `${message} : ${trimmed}` : message,
86
+ source: "realtime",
87
+ code,
88
+ ts: Date.now()
89
+ };
90
+ }
91
+ /**
92
+ * Close codes (RFC 6455 §7.4 + privés Nodefony 4xxx) après lesquels une
93
+ * reconnexion automatique est INUTILE ou NUISIBLE — la cause ne disparaît pas en
94
+ * réessayant : fin propre (1000), erreurs de protocole/négociation
95
+ * (1002/1003/1007/1010), violation de politique = 401/403 mappés (1008), et
96
+ * ressource introuvable (4004, privé Nodefony). Une boucle de reco sur ces codes
97
+ * martèle le serveur pour rien (vécu : un anonyme sur un endpoint protégé).
98
+ */
99
+ const FATAL_CLOSE_CODES = /* @__PURE__ */ new Set([
100
+ 1e3,
101
+ 1002,
102
+ 1003,
103
+ 1007,
104
+ 1008,
105
+ 1010,
106
+ 4004
107
+ ]);
108
+ /**
109
+ * Faut-il tenter une reconnexion après ce close code WebSocket ? Pendant CLIENT
110
+ * de la sémantique RFC 6455 §7.4 émise par le serveur (`toWsCloseCode`).
111
+ *
112
+ * `false` pour les fermetures DÉFINITIVES (cf {@link FATAL_CLOSE_CODES}) : l'app
113
+ * doit agir (ex. login après un 1008) puis rétablir explicitement (`connect()` /
114
+ * `retryNow()`). `true` pour les transitoires (1001 restart, 1006 perte réseau,
115
+ * 1011 erreur serveur, code absent/inconnu) où la reconnexion a une chance.
116
+ *
117
+ * @param code - close code reçu (`CloseEvent.code`), ou `undefined` (anormal).
118
+ * @returns `true` si une reconnexion auto est pertinente.
119
+ */
120
+ function isReconnectableCloseCode(code) {
121
+ if (code === void 0) return true;
122
+ return !FATAL_CLOSE_CODES.has(code);
123
+ }
124
+ //#endregion
125
+ //#region src/realtime/JsonRpcPeer.ts
126
+ var RpcError = class extends Error {
127
+ name = "RpcError";
128
+ data;
129
+ constructor(message, code = -32e3, data) {
130
+ super(message);
131
+ this.code = code;
132
+ if (data !== void 0) this.data = data;
133
+ }
134
+ };
135
+ /**
136
+ * Réponse d'un handler qui veut JOINDRE des métadonnées serveur à son résultat.
137
+ *
138
+ * Un handler retourne normalement sa valeur nue (elle devient `result`). Quand
139
+ * il retourne une enveloppe, le peer la déballe : `result` reste la valeur nue,
140
+ * et `meta` voyage dans un champ frère de la trame JSON-RPC. Coût pour ceux qui
141
+ * ne l'utilisent pas : un `instanceof` par frame.
142
+ */
143
+ var RpcEnvelope = class {
144
+ result;
145
+ meta;
146
+ constructor(result, meta) {
147
+ this.result = result;
148
+ this.meta = meta;
149
+ }
150
+ };
151
+ var JsonRpcPeer = class JsonRpcPeer {
152
+ opts;
153
+ nextId = 1;
154
+ pending = null;
155
+ actions = null;
156
+ constructor(opts) {
157
+ this.opts = opts;
158
+ }
159
+ /** Expose une action (requête entrante `method`+`id` → `result`). Idempotent. */
160
+ register(method, handler) {
161
+ (this.actions ??= /* @__PURE__ */ new Map()).set(method, handler);
162
+ }
163
+ /** Retire une action. */
164
+ unregister(method) {
165
+ this.actions?.delete(method);
166
+ }
167
+ /** Liste des actions exposées (pour annoncer au handshake, découverte côté pair). */
168
+ get methods() {
169
+ return this.actions ? [...this.actions.keys()] : [];
170
+ }
171
+ /** Requête SORTANTE — `Promise` résolue avec le `result` (rejette sur `error`/timeout). */
172
+ request(method, params, timeoutMs = 3e4) {
173
+ return this.startCall(method, params, timeoutMs);
174
+ }
175
+ /**
176
+ * Requête SORTANTE **tracée** — rend `{ result, meta }` au lieu du `result` nu.
177
+ *
178
+ * Même appel que {@link request} : la seule différence est que l'appelant
179
+ * garde la méta serveur (en dev, le `requestId` du profil de la frame → sa
180
+ * radiographie). Un serveur qui n'en émet pas laisse simplement `meta`
181
+ * absent.
182
+ */
183
+ requestTraced(method, params, timeoutMs = 3e4) {
184
+ return this.startCall(method, params, timeoutMs, true);
185
+ }
186
+ /**
187
+ * Frame d'une notification sortante — **source unique** de sa forme.
188
+ *
189
+ * Extraite pour que le fan-out d'un canal diffusé puisse sérialiser la frame
190
+ * **une seule fois** pour N abonnés, sans jamais risquer de diverger du format
191
+ * qu'émet {@link notify} : les deux voies passent par ici.
192
+ *
193
+ * @param method - nom de la méthode (côté canal : le nom du canal).
194
+ * @param params - charge applicative ; omise de la frame si `undefined`.
195
+ * @returns la frame JSON-RPC 2.0, prête à sérialiser.
196
+ */
197
+ static buildNotification(method, params) {
198
+ return {
199
+ jsonrpc: "2.0",
200
+ method,
201
+ params
202
+ };
203
+ }
204
+ /**
205
+ * Notification SORTANTE (pas de réponse attendue).
206
+ *
207
+ * @returns `false` si le transport a signalé qu'il n'a pas émis. Une
208
+ * notification n'attend rien en retour : sans cette valeur, une frame perdue
209
+ * ne laisserait aucune trace chez l'appelant. Les appels qui se rattrapent
210
+ * seuls (ré-abonnement au reconnect) peuvent l'ignorer.
211
+ */
212
+ notify(method, params) {
213
+ return this.opts.send(JsonRpcPeer.buildNotification(method, params)) !== false;
214
+ }
215
+ /**
216
+ * Ingestion d'une frame ENTRANTE déjà parsée → classe (request/notification/
217
+ * response) et route. Renvoie la nature (utile log/tests). Ne lève jamais.
218
+ * (Le pendant sortant = `request`/`notify` ; le `send` brut est injecté.)
219
+ */
220
+ receive(msg) {
221
+ if (!msg || typeof msg !== "object" || msg.jsonrpc !== "2.0") {
222
+ this.opts.onFrameAudit?.("invalid", msg, this);
223
+ return "invalid";
224
+ }
225
+ const id = msg.id;
226
+ const hasId = typeof id === "number" || typeof id === "string";
227
+ const rawMethod = msg.method;
228
+ const method = typeof rawMethod === "string" ? rawMethod : void 0;
229
+ const params = msg.params;
230
+ if (method !== void 0) {
231
+ if (this.opts.beforeDispatch && !this.opts.beforeDispatch(msg, this)) {
232
+ this.opts.onFrameAudit?.("denied", msg, this);
233
+ if (hasId) {
234
+ this.opts.send({
235
+ jsonrpc: "2.0",
236
+ id,
237
+ error: {
238
+ code: -32001,
239
+ message: "unauthorized"
240
+ }
241
+ });
242
+ return "request";
243
+ }
244
+ return "notification";
245
+ }
246
+ if (hasId) {
247
+ this.handleRequest(id, method, params, msg);
248
+ return "request";
249
+ }
250
+ this.opts.onNotification?.(method, params);
251
+ return "notification";
252
+ }
253
+ if (hasId) {
254
+ this.handleResponse(msg);
255
+ return "response";
256
+ }
257
+ this.opts.onFrameAudit?.("invalid", msg, this);
258
+ return "invalid";
259
+ }
260
+ /** Annule tous les pending (fermeture du transport). */
261
+ dispose(reason = "peer disposed") {
262
+ if (!this.pending) return;
263
+ for (const p of this.pending.values()) {
264
+ if (p.timer) clearTimeout(p.timer);
265
+ p.reject(new Error(reason));
266
+ }
267
+ this.pending.clear();
268
+ }
269
+ startCall(method, params, timeoutMs, withMeta) {
270
+ const id = this.nextId++;
271
+ const pending = this.pending ??= /* @__PURE__ */ new Map();
272
+ return new Promise((resolve, reject) => {
273
+ const timer = setTimeout(() => {
274
+ pending.delete(id);
275
+ reject(/* @__PURE__ */ new Error(`RPC timeout: ${method}`));
276
+ }, timeoutMs);
277
+ pending.set(id, {
278
+ resolve: (v) => {
279
+ clearTimeout(timer);
280
+ resolve(v);
281
+ },
282
+ reject: (e) => {
283
+ clearTimeout(timer);
284
+ reject(e);
285
+ },
286
+ timer,
287
+ withMeta
288
+ });
289
+ if (this.opts.send({
290
+ jsonrpc: "2.0",
291
+ id,
292
+ method,
293
+ params
294
+ }) === false) {
295
+ clearTimeout(timer);
296
+ pending.delete(id);
297
+ reject(/* @__PURE__ */ new Error(`RPC non émis (transport indisponible) : ${method}`));
298
+ }
299
+ });
300
+ }
301
+ handleRequest(id, method, params, rawFrame) {
302
+ const handler = this.actions?.get(method);
303
+ if (!handler) {
304
+ this.opts.onFrameAudit?.("method_not_found", rawFrame, this);
305
+ this.opts.send({
306
+ jsonrpc: "2.0",
307
+ id,
308
+ error: {
309
+ code: -32601,
310
+ message: `method not found: ${method}`
311
+ }
312
+ });
313
+ return;
314
+ }
315
+ Promise.resolve().then(() => handler(params)).then((result) => {
316
+ if (result instanceof RpcEnvelope) {
317
+ this.opts.send({
318
+ jsonrpc: "2.0",
319
+ id,
320
+ result: result.result,
321
+ meta: result.meta
322
+ });
323
+ return;
324
+ }
325
+ this.opts.send({
326
+ jsonrpc: "2.0",
327
+ id,
328
+ result
329
+ });
330
+ }, (err) => {
331
+ if (err instanceof RpcError) {
332
+ const error = {
333
+ code: err.code,
334
+ message: err.message
335
+ };
336
+ if (err.data !== void 0) error.data = err.data;
337
+ this.opts.send({
338
+ jsonrpc: "2.0",
339
+ id,
340
+ error
341
+ });
342
+ return;
343
+ }
344
+ this.opts.onError?.(`rpc ${method}`, err);
345
+ this.opts.onFrameAudit?.("internal_error", rawFrame, this);
346
+ this.opts.send({
347
+ jsonrpc: "2.0",
348
+ id,
349
+ error: {
350
+ code: -32603,
351
+ message: "internal error"
352
+ }
353
+ });
354
+ });
355
+ }
356
+ handleResponse(msg) {
357
+ if (typeof msg.id !== "number" || !this.pending) return;
358
+ const pending = this.pending.get(msg.id);
359
+ if (!pending) return;
360
+ if (msg.error) {
361
+ this.pending.delete(msg.id);
362
+ pending.reject(new RpcError(msg.error.message, msg.error.code, msg.error.data));
363
+ return;
364
+ }
365
+ if ("result" in msg) {
366
+ this.pending.delete(msg.id);
367
+ pending.resolve(pending.withMeta ? {
368
+ result: msg.result,
369
+ meta: msg.meta
370
+ } : msg.result);
371
+ }
372
+ }
373
+ };
374
+ //#endregion
375
+ //#region src/client/realtime/localEvents.ts
376
+ /**
377
+ * localEvents — **noms des événements LOCAUX du client temps réel**.
378
+ *
379
+ * Ces six noms ne voyagent jamais sur le fil : ils ne servent qu'à propager, dans
380
+ * la page, ce que le client vient d'apprendre (changement d'état de connexion,
381
+ * identité résolue au welcome, échantillon de statistiques, notice normalisée,
382
+ * refus de canal). Ils partagent la table de `on()`/`off()` avec les canaux
383
+ * réseau ; le double soulignement est ce qui les en distingue.
384
+ *
385
+ * **La table est la source unique**, exactement pour la même raison que
386
+ * {@link PLATFORM_CHANNELS} l'est pour les canaux réseau : une chaîne recopiée
387
+ * dans un écran est une chaîne qui survivra au renommage suivant. Celle-ci avait
388
+ * déjà divergé — `"__state__"` était écrit en clair dans les trois gabarits
389
+ * d'application, dans la barre de debug et dans la console d'administration,
390
+ * sans qu'aucune constante ne l'exporte.
391
+ *
392
+ * Un consommateur n'a d'ailleurs pas à les connaître : `onState`, `onIdentity`,
393
+ * `onStats`, `onNotice`, `onDenied` et `onReconnect` sont les portes publiques. La table existe
394
+ * pour les implémentations (le client lui-même, ses observateurs agnostiques) et
395
+ * pour un test qui veut se brancher au plus bas niveau.
396
+ *
397
+ * Pur et isomorphe (aucun import, aucune allocation).
398
+ */
399
+ /**
400
+ * Événements locaux du {@link RealtimeClient} — jamais émis ni reçus par le réseau.
401
+ *
402
+ * @see {@link RealtimeClient.onState} · {@link RealtimeClient.onIdentity} ·
403
+ * {@link RealtimeClient.onStats} · {@link RealtimeClient.onNotice} ·
404
+ * {@link RealtimeClient.onDenied} — les portes publiques à préférer partout.
405
+ */
406
+ const LOCAL_EVENTS = {
407
+ /** État de la connexion (`"connected" | "reconnecting" | …`), à chaque transition. */
408
+ state: "__state__",
409
+ /** Identité résolue par le serveur au welcome ; `null` après un `disconnect()`. */
410
+ identity: "__identity__",
411
+ /** Tick d'échantillonnage des statistiques par canal (1×/s), sans charge utile. */
412
+ stats: "__stats__",
413
+ /** Notice normalisée (criticité temps réel interprétée). */
414
+ notice: "__notice__",
415
+ /** Refus d'un canal poussé par le serveur (`realtime:denied`). */
416
+ denied: "__denied__",
417
+ /** Tentative de reconnexion programmée (numéro d'essai, délai, échéance). */
418
+ reconnect: "__reconnect__"
419
+ };
420
+ //#endregion
421
+ //#region src/realtime/IRealtimeTransport.ts
422
+ /**
423
+ * IRealtimeTransport — le SEUL maillon qui diffère client/serveur.
424
+ *
425
+ * Le protocole ({@link JsonRpcPeer}) et les « smarts » (stats, pub/sub) sont
426
+ * isomorphes ; seule la façon dont les **octets** entrent et sortent change. On
427
+ * isole donc le transport derrière cette interface : un même endpoint compose
428
+ * N transports.
429
+ *
430
+ * Implémentations :
431
+ * - `BrowserWsTransport` (navigateur, `WebSocket` + reconnect géré au-dessus)
432
+ * - `WsConnectionTransport` (serveur, wrap d'une connexion `ws` — P… RealtimeController)
433
+ * - futurs P13 : `Tcp`/`Udp`/`Redis`/`SipTransport` (RealtimeService / IRealtimeHub)
434
+ *
435
+ * Les états suivent les constantes `WebSocket` (0..3) pour rester universels —
436
+ * voir {@link TransportState}.
437
+ *
438
+ * ── Sens des flux ──
439
+ * Sortant : `send` (frame brute déjà sérialisée), `close`.
440
+ * Entrant : le owner branche `onMessage` (chaque frame reçue) ; `onOpen`/`onClose`/
441
+ * `onError` pilotent le cycle de vie (l'orchestration reconnect/heartbeat vit
442
+ * AU-DESSUS, dans l'endpoint — le transport reste « bête »).
443
+ */
444
+ /** États d'un transport — alignés sur les constantes `WebSocket`. */
445
+ const TransportState = {
446
+ CONNECTING: 0,
447
+ OPEN: 1,
448
+ CLOSING: 2,
449
+ CLOSED: 3
450
+ };
451
+ //#endregion
452
+ //#region src/client/realtime/BrowserWsTransport.ts
453
+ /**
454
+ * BrowserWsTransport — transport {@link IRealtimeTransport} pour le navigateur :
455
+ * wrap d'un `WebSocket` natif. Volontairement « bête » : il ouvre/envoie/ferme et
456
+ * relaie les events. L'orchestration (reconnect, backoff, heartbeat, state machine)
457
+ * vit AU-DESSUS dans `RealtimeClient` — qui crée un transport NEUF à chaque tentative.
458
+ *
459
+ * L'URL reçue est déjà normalisée (ws/wss, token) par l'appelant.
460
+ */
461
+ var BrowserWsTransport = class {
462
+ url;
463
+ ws = null;
464
+ _onOpen = null;
465
+ _onMessage = null;
466
+ _onClose = null;
467
+ _onError = null;
468
+ constructor(url) {
469
+ this.url = url;
470
+ }
471
+ connect() {
472
+ const ws = new WebSocket(this.url);
473
+ this.ws = ws;
474
+ ws.addEventListener("open", () => this._onOpen?.());
475
+ ws.addEventListener("message", (ev) => {
476
+ if (typeof ev.data === "string") this._onMessage?.(ev.data);
477
+ });
478
+ ws.addEventListener("close", (ev) => {
479
+ this.ws = null;
480
+ this._onClose?.(ev.code, ev.reason);
481
+ });
482
+ ws.addEventListener("error", () => this._onError?.(/* @__PURE__ */ new Error("websocket error")));
483
+ }
484
+ send(raw) {
485
+ if (this.ws?.readyState === TransportState.OPEN) this.ws.send(raw);
486
+ }
487
+ close(code, reason) {
488
+ this.ws?.close(code, reason);
489
+ }
490
+ get readyState() {
491
+ return this.ws?.readyState ?? TransportState.CLOSED;
492
+ }
493
+ onOpen(cb) {
494
+ this._onOpen = cb;
495
+ }
496
+ onMessage(cb) {
497
+ this._onMessage = cb;
498
+ }
499
+ onClose(cb) {
500
+ this._onClose = cb;
501
+ }
502
+ onError(cb) {
503
+ this._onError = cb;
504
+ }
505
+ };
506
+ /** Le badge est-il déjà sorti sur cette page ? Posé même en production. */
507
+ function alreadyAnnounced() {
508
+ return globalThis.__nfAnnounced__ === true;
509
+ }
510
+ /**
511
+ * Mode annoncé par le SERVEUR au handshake temps réel — `null` tant qu'aucune
512
+ * socket n'a reçu d'accueil.
513
+ *
514
+ * Il existe parce que `import.meta.env.DEV` ne dit que le mode du BUNDLE : une
515
+ * application bâtie pour la production mais servie par un serveur de
516
+ * développement (banc, `--cluster`, image testée localement) se taisait alors
517
+ * qu'on avait tout intérêt à la faire parler. Le serveur, lui, sait.
518
+ */
519
+ let serverEnv = null;
520
+ /**
521
+ * Le détail est-il sorti sur cette page ? Un seul par page, comme le badge — et
522
+ * c'est le noyau qui gagne quand il y en a un.
523
+ *
524
+ * Sur `globalThis` et non en variable de module, pour la même raison que le
525
+ * badge : un module vit plus longtemps qu'une page dans un banc, et un état qui
526
+ * ne se réarme pas fait passer les tests suivants pour des régressions.
527
+ */
528
+ function alreadyDetailed() {
529
+ return globalThis.__nfDetailed__ === true;
530
+ }
531
+ /**
532
+ * Le mode annoncé par le serveur, quand il en annonce un.
533
+ *
534
+ * ⚠️ Le serveur **ne pose ce champ qu'en dehors de la production** : une absence
535
+ * vaut donc production, jamais l'inverse. C'est la même règle que pour le
536
+ * bundle, et pour la même raison — se taire à tort ne coûte qu'une ligne, parler
537
+ * à tort expose la console de tout le monde.
538
+ *
539
+ * @param env - la valeur reçue dans `realtime:welcome`, ou `undefined`.
540
+ */
541
+ function noteServerEnv(env) {
542
+ if (env) serverEnv = env;
543
+ }
544
+ /**
545
+ * Peut-on se permettre le détail ? Bundle de développement, OU serveur qui
546
+ * annonce un mode hors production.
547
+ */
548
+ function isVerbose() {
549
+ return isDevBuild() || serverEnv !== null && serverEnv !== "production";
550
+ }
551
+ /**
552
+ * Le bundle a-t-il été bâti en développement ?
553
+ *
554
+ * Le mode se LIT, il ne se devine pas : `import.meta.env.DEV` est posé par le
555
+ * bundler de l'application (Vite le remplace par une constante au build). Hors
556
+ * bundler il n'existe pas — et **l'absence de preuve de développement se traite
557
+ * comme la production**, jamais l'inverse : se taire à tort ne coûte qu'une
558
+ * ligne manquante, parler à tort pollue la console de tout le monde.
559
+ */
560
+ function isDevBuild() {
561
+ return import.meta.env?.DEV === true;
562
+ }
563
+ /** Les sockets partagées, lues dans le registre de `RealtimeClient.shared()`. */
564
+ function sharedSockets() {
565
+ const g = globalThis;
566
+ return g.__nfRealtime__ ? [...g.__nfRealtime__.values()] : [];
567
+ }
568
+ /**
569
+ * Pose ou actualise `globalThis.nodefony`.
570
+ *
571
+ * Développement seulement : la console d'une application publiée appartient à
572
+ * ses développeurs, et un handle global y serait une surface offerte pour rien.
573
+ */
574
+ function poseHandle() {
575
+ if (!isVerbose()) return;
576
+ globalThis.nodefony = {
577
+ get kernel() {},
578
+ get socket() {
579
+ return sharedSockets()[0];
580
+ },
581
+ sockets: () => sharedSockets().map((s) => ({
582
+ url: s.url ?? "(adresse non résolue)",
583
+ state: s.state
584
+ })),
585
+ identity: () => {
586
+ return sharedSockets()[0]?.identity ?? null;
587
+ }
588
+ };
589
+ }
590
+ /**
591
+ * Retire le handle et réarme le badge — appelé quand plus rien ne vit.
592
+ *
593
+ * « Plus rien » se CONSTATE : aucun noyau, et aucune socket dans le registre du
594
+ * partage. Une socket construite hors `shared()` échappe à ce constat ; le seul
595
+ * effet est qu'un badge pourrait ressortir, jamais une fuite.
596
+ */
597
+ function release() {
598
+ const g = globalThis;
599
+ delete g.nodefony;
600
+ delete g.__nfAnnounced__;
601
+ delete globalThis.__nfDetailed__;
602
+ serverEnv = null;
603
+ }
604
+ /**
605
+ * Émet le badge, au plus une fois par page.
606
+ *
607
+ * La forme est celle qu'ont adoptée Vue, Vite et les outils qui vivent dans une
608
+ * console partagée : un badge en couleur sur UNE ligne. Pas de dessin en
609
+ * caractères — celui du serveur a du sens dans un terminal qu'on ouvre une fois
610
+ * au démarrage ; dans une console de navigateur il se répète à chaque
611
+ * rechargement et pousse hors de vue les messages de l'application.
612
+ *
613
+ * @param name - ce qui suit « nodefony » ; le nom du noyau quand il y en a un.
614
+ */
615
+ function badge(name) {
616
+ if (alreadyAnnounced()) return;
617
+ const c = globalThis.console;
618
+ if (!c?.log) return;
619
+ globalThis.__nfAnnounced__ = true;
620
+ c.log(`%c◆ nodefony%c${name}%c`, "background:#0b1120;color:#5eead4;font-weight:700;padding:2px 6px;border-radius:3px 0 0 3px", "background:#1e293b;color:#e2e8f0;padding:2px 6px;border-radius:0 3px 3px 0", "");
621
+ }
622
+ /**
623
+ * Annonce une socket qui vit NUE — aucune composition, aucun noyau. C'est le cas
624
+ * des vitrines du dépôt et de tout widget qui monte une socket et rien d'autre.
625
+ *
626
+ * @param annoncer - `false` fait taire l'annonce (option `banner` de la socket).
627
+ * @returns le débranchement ; une socket partagée vit avec la page et n'a pas à
628
+ * l'appeler — il existe pour les bancs, qui doivent repartir d'une page vierge.
629
+ */
630
+ function announceRealtime(annoncer) {
631
+ if (annoncer === false) return () => void 0;
632
+ badge("client");
633
+ poseHandle();
634
+ return () => {
635
+ if (sharedSockets().length === 0) release();
636
+ };
637
+ }
638
+ /**
639
+ * Détail du framework, dans un groupe REPLIÉ de la console : un tableau
640
+ * `clé → valeur` et les deux rappels qui font gagner du temps.
641
+ *
642
+ * **Un seul détail par page**, et le noyau gagne : quand il y en a un, la socket
643
+ * ne le double pas — c'est lui qui a le plus à dire (état, identité, services).
644
+ * Une vitrine sans noyau, elle, obtient enfin le sien : c'est la moitié
645
+ * « diagnostic » que l'ADR-0007 D7 laissait derrière le péage de la composition.
646
+ *
647
+ * Le groupe est replié et non ouvert : présent pour qui le cherche, invisible
648
+ * pour qui débogue autre chose. `groupEnd` est atteint même si une lecture
649
+ * jette — un groupe laissé ouvert avale tous les messages suivants.
650
+ *
651
+ * @param rows - le tableau à rendre, `clé → { valeur }`, composé par
652
+ * l'appelant : lui seul sait ce qu'il a d'important à dire.
653
+ * @param subject - l'objet vivant à imprimer en dernier (noyau, ou socket).
654
+ * @param title - l'intitulé du groupe.
655
+ * @returns `true` si le détail a été émis.
656
+ */
657
+ function consoleDetails(rows, subject, title) {
658
+ if (alreadyDetailed() || !isVerbose()) return false;
659
+ const c = globalThis.console;
660
+ if (!c?.log || !c.groupCollapsed || !c.groupEnd) return false;
661
+ globalThis.__nfDetailed__ = true;
662
+ c.groupCollapsed(`%c${title}`, "color:#94a3b8");
663
+ try {
664
+ if (c.table) c.table(rows);
665
+ else for (const [k, v] of Object.entries(rows)) c.log(k, v.valeur);
666
+ c.log("%cconsole :%c nodefony.socket · nodefony.sockets() · nodefony.identity()", "color:#94a3b8", "color:#5eead4;font-family:monospace");
667
+ c.log("%ccorrélation :%c chaque journal porte un requestId — la même valeur côté serveur relie clic, route, base et réponse", "color:#94a3b8", "color:#e2e8f0");
668
+ c.log("%cobjet :%c", "color:#94a3b8", "", subject);
669
+ } finally {
670
+ c.groupEnd();
671
+ }
672
+ return true;
673
+ }
674
+ /** Un noyau est-il vivant sur cette page ? La socket s'efface devant lui. */
675
+ function hasKernel() {
676
+ return false;
677
+ }
678
+ //#endregion
679
+ //#region src/realtime/channelRate.ts
680
+ /**
681
+ * Construit le nom d'un canal cadencé à partir de sa base et de la cadence demandée.
682
+ *
683
+ * `intervalMs` omis OU égal à `defaultMs` → renvoie `base` nu : on **n'émet pas** de suffixe
684
+ * pour la cadence par défaut, ce qui évite de fragmenter le canal de base entre consommateurs
685
+ * qui veulent tous la valeur par défaut (ils partagent alors un seul ticker ref-compté).
686
+ *
687
+ * @param base - canal de base (ex. `"nodefony:orm:health"`).
688
+ * @param intervalMs - cadence demandée (ms). Omise → canal de base.
689
+ * @param defaultMs - cadence par défaut du canal ; `intervalMs === defaultMs` ⇒ canal de base.
690
+ * @returns `base` ou `base:<ms>`.
691
+ */
692
+ function rateChannel(base, intervalMs, defaultMs) {
693
+ if (intervalMs == null || intervalMs === defaultMs) return base;
694
+ return `${base}:${intervalMs}`;
695
+ }
696
+ //#endregion
697
+ //#region src/client/realtime/AdaptiveRate.ts
698
+ /**
699
+ * AdaptiveRate — cadence ADAPTATIVE automatique d'un canal d'ÉTAT, **client-driven**
700
+ * (niveau 1, zéro changement serveur). Pendant exact de l'ABR vidéo (HLS/DASH ajustent
701
+ * la qualité selon le buffer) et du contrôle de congestion réseau (TCP **AIMD**).
702
+ *
703
+ * Idée : la cadence vit dans le nom du canal (cf {@link rateChannel} — 1 canal = 1 cadence
704
+ * = 1 ticker serveur). Si le client observe que les frames arrivent **plus lentement** que
705
+ * demandé (famine ⇒ event-loop serveur ou réseau sous pression), il se ré-abonne à une
706
+ * cadence **plus grossière** (`1s→2s→5s`) → moins de pushes → la pression retombe → le flux
707
+ * récupère ; quand c'est sain durablement, il remonte **doucement** vers la cadence désirée.
708
+ *
709
+ * AIMD (asymétrie anti-oscillation, comme TCP) :
710
+ * - **Multiplicative Decrease** : famine ⇒ on monte d'un cran l'échelle (cadence grossie),
711
+ * immédiat. L'échelle géométrique (×2) rend la décrue multiplicative.
712
+ * - **Additive Increase** : on ne redescend d'un cran (cadence affinée) qu'après une
713
+ * **fenêtre** de N échantillons sains → reprise lente.
714
+ * - **bande morte** (hystérésis) entre « sain » et « famine » : aucun changement → pas de
715
+ * flip-flop.
716
+ *
717
+ * Réservé aux canaux d'**ÉTAT** (latest-wins : supervision, stats) où décimer est sans perte.
718
+ * PAS pour les canaux d'**ÉVÉNEMENTS** (syslog, frames protocole) où chaque item compte
719
+ * (eux se batchent, pas se décimer).
720
+ *
721
+ * Cette classe est la **machine à états pure** (aucun timer, aucune socket) → déterministe
722
+ * et testable. Le câblage à une socket réelle vit dans {@link bindAdaptiveChannel}.
723
+ *
724
+ * @see {@link bindAdaptiveChannel} — glue socket + watchdog.
725
+ * @see {@link rateChannel} — convention de nommage cadencé partagée client↔serveur.
726
+ */
727
+ /** Construit une échelle géométrique ×2 de `intervalMs` à `maxMs` (bornes incluses). */
728
+ function deriveLadder(intervalMs, maxMs) {
729
+ const ladder = [];
730
+ let ms = intervalMs;
731
+ while (ms < maxMs) {
732
+ ladder.push(ms);
733
+ ms *= 2;
734
+ }
735
+ ladder.push(maxMs);
736
+ return ladder;
737
+ }
738
+ var AdaptiveRate = class {
739
+ ladder;
740
+ starvationFactor;
741
+ healthyFactor;
742
+ recoveryWindow;
743
+ /** Position dans l'échelle (0 = cadence la plus fine = désirée). */
744
+ idx = 0;
745
+ /** Échantillons sains consécutifs depuis le dernier changement. */
746
+ healthy = 0;
747
+ /** Timestamp (ms) de la dernière frame ; `null` = pas de référence (post-switch/init). */
748
+ lastFrameTs = null;
749
+ constructor(options) {
750
+ const maxMs = options.maxMs ?? 6e4;
751
+ const ladder = options.ladder && options.ladder.length > 0 ? [...options.ladder].sort((a, b) => a - b) : deriveLadder(options.intervalMs, Math.max(options.intervalMs, maxMs));
752
+ this.ladder = ladder;
753
+ this.starvationFactor = options.starvationFactor ?? 1.8;
754
+ this.healthyFactor = options.healthyFactor ?? 1.25;
755
+ this.recoveryWindow = options.recoveryWindow ?? 4;
756
+ }
757
+ /** Cadence courante (ms). */
758
+ current() {
759
+ return this.ladder[this.idx];
760
+ }
761
+ /** Décale d'un cran vers le grossier (MD) ; renvoie `true` si la cadence a changé. */
762
+ decrease() {
763
+ if (this.idx >= this.ladder.length - 1) return false;
764
+ this.idx += 1;
765
+ return true;
766
+ }
767
+ /** Décale d'un cran vers le fin (AI) ; renvoie `true` si la cadence a changé. */
768
+ increase() {
769
+ if (this.idx <= 0) return false;
770
+ this.idx -= 1;
771
+ return true;
772
+ }
773
+ /** Réamorce la mesure (après un changement de cadence). */
774
+ resetSample() {
775
+ this.lastFrameTs = null;
776
+ this.healthy = 0;
777
+ }
778
+ /**
779
+ * À appeler à CHAQUE frame reçue. Pilote l'**AI** (reprise) et détecte aussi la famine
780
+ * sur frames lentes (**MD**).
781
+ *
782
+ * @param now - horloge (ms).
783
+ * @returns une {@link RateDecision} si la cadence doit changer, sinon `null`.
784
+ */
785
+ noteFrame(now) {
786
+ const prev = this.lastFrameTs;
787
+ this.lastFrameTs = now;
788
+ if (prev === null) return null;
789
+ const gap = now - prev;
790
+ const cur = this.current();
791
+ if (gap > this.starvationFactor * cur) {
792
+ if (this.decrease()) {
793
+ this.resetSample();
794
+ return {
795
+ intervalMs: this.current(),
796
+ reason: "decrease"
797
+ };
798
+ }
799
+ this.healthy = 0;
800
+ return null;
801
+ }
802
+ if (gap <= this.healthyFactor * cur) {
803
+ this.healthy += 1;
804
+ if (this.healthy >= this.recoveryWindow && this.increase()) {
805
+ this.resetSample();
806
+ return {
807
+ intervalMs: this.current(),
808
+ reason: "increase"
809
+ };
810
+ }
811
+ return null;
812
+ }
813
+ this.healthy = 0;
814
+ return null;
815
+ }
816
+ /**
817
+ * Watchdog : à appeler périodiquement. Détecte la famine **totale** (plus aucune frame)
818
+ * que {@link noteFrame} ne verrait pas (faute de frame).
819
+ *
820
+ * @param now - horloge (ms).
821
+ * @returns une {@link RateDecision} (toujours `"decrease"`) si la cadence doit grossir.
822
+ */
823
+ checkStarvation(now) {
824
+ if (this.lastFrameTs === null) return null;
825
+ if (now - this.lastFrameTs > this.starvationFactor * this.current()) {
826
+ if (this.decrease()) {
827
+ this.resetSample();
828
+ return {
829
+ intervalMs: this.current(),
830
+ reason: "decrease"
831
+ };
832
+ }
833
+ }
834
+ return null;
835
+ }
836
+ };
837
+ const defaultScheduler = {
838
+ set: (cb, ms) => setInterval(cb, ms),
839
+ clear: (h) => clearInterval(h)
840
+ };
841
+ const defaultClock = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
842
+ /**
843
+ * Câble un {@link AdaptiveRate} à une socket réelle : abonne `base` à sa cadence désirée,
844
+ * mesure la gigue d'arrivée, et **ré-abonne** automatiquement à une cadence plus grossière
845
+ * en cas de famine (MD) puis plus fine quand c'est sain (AI). Les frames continuent d'être
846
+ * livrées à `handler` à travers les changements de cadence.
847
+ *
848
+ * Réservé aux canaux d'ÉTAT (latest-wins). Pour les canaux d'événements, ne pas décimer.
849
+ *
850
+ * @param socket - la socket Nodefony ({@link IRealtimeSocket}).
851
+ * @param base - canal de base (sans suffixe de cadence).
852
+ * @param handler - reçoit le payload de chaque frame.
853
+ * @param options - cadence désirée + réglages AIMD + injections.
854
+ * @returns une {@link AdaptiveChannelBinding} (cadence courante + `dispose`).
855
+ */
856
+ function bindAdaptiveChannel(socket, base, handler, options) {
857
+ if (options.enabled === false) {
858
+ const channel = rateChannel(base, options.intervalMs, options.defaultMs);
859
+ socket.subscribe(channel);
860
+ const offFixed = socket.on(channel, handler);
861
+ options.onRate?.(options.intervalMs, "init");
862
+ let downFixed = false;
863
+ return {
864
+ get intervalMs() {
865
+ return options.intervalMs;
866
+ },
867
+ get channel() {
868
+ return channel;
869
+ },
870
+ dispose() {
871
+ if (downFixed) return;
872
+ downFixed = true;
873
+ offFixed();
874
+ socket.unsubscribe(channel);
875
+ }
876
+ };
877
+ }
878
+ const clock = options.clock ?? defaultClock;
879
+ const scheduler = options.scheduler ?? defaultScheduler;
880
+ const ar = new AdaptiveRate(options);
881
+ let currentMs = ar.current();
882
+ let currentChannel = rateChannel(base, currentMs, options.defaultMs);
883
+ let off = null;
884
+ let watchdog;
885
+ let disposed = false;
886
+ const wrapped = (...args) => {
887
+ const decision = ar.noteFrame(clock());
888
+ handler(...args);
889
+ if (decision) applyDecision(decision);
890
+ };
891
+ const armWatchdog = () => {
892
+ if (watchdog !== void 0) scheduler.clear(watchdog);
893
+ watchdog = scheduler.set(() => {
894
+ const decision = ar.checkStarvation(clock());
895
+ if (decision) applyDecision(decision);
896
+ }, currentMs);
897
+ };
898
+ function applyDecision(decision) {
899
+ if (disposed) return;
900
+ const nextChannel = rateChannel(base, decision.intervalMs, options.defaultMs);
901
+ currentMs = decision.intervalMs;
902
+ if (nextChannel !== currentChannel) {
903
+ const oldChannel = currentChannel;
904
+ const oldOff = off;
905
+ socket.subscribe(nextChannel);
906
+ off = socket.on(nextChannel, wrapped);
907
+ oldOff?.();
908
+ socket.unsubscribe(oldChannel);
909
+ currentChannel = nextChannel;
910
+ }
911
+ armWatchdog();
912
+ options.onRate?.(currentMs, decision.reason);
913
+ }
914
+ socket.subscribe(currentChannel);
915
+ off = socket.on(currentChannel, wrapped);
916
+ armWatchdog();
917
+ options.onRate?.(currentMs, "init");
918
+ return {
919
+ get intervalMs() {
920
+ return currentMs;
921
+ },
922
+ get channel() {
923
+ return currentChannel;
924
+ },
925
+ dispose() {
926
+ if (disposed) return;
927
+ disposed = true;
928
+ if (watchdog !== void 0) scheduler.clear(watchdog);
929
+ off?.();
930
+ socket.unsubscribe(currentChannel);
931
+ }
932
+ };
933
+ }
934
+ //#endregion
935
+ //#region src/realtime/platformChannels.ts
936
+ /**
937
+ * Canaux de **diffusion** de la plateforme (le serveur pousse, les écrans regardent).
938
+ *
939
+ * Les valeurs sont des **bases** : trois d'entre elles se déclinent par cible
940
+ * (`…@<pid>` pour un forage sur un process, `…@<id>` pour un job) et toutes acceptent
941
+ * le suffixe de cadence `:<ms>` (cf {@link rateChannel}). Composer, jamais concaténer
942
+ * un préfixe à la main.
943
+ */
944
+ const PLATFORM_CHANNELS = {
945
+ /** Journaux du pod (flux de `Pdu`, coalescé par le pont syslog). */
946
+ syslog: "nodefony:syslog",
947
+ /** Journal d'audit sécurité — plancher super-admin, un cran au-dessus des autres. */
948
+ audit: "nodefony:audit",
949
+ /** Métriques agrégées du tableau de bord (vue d'ensemble). */
950
+ dashboard: "nodefony:dashboard",
951
+ /** Supervision des process du cluster ; forage par process : `…@<pid>`. */
952
+ supervision: "nodefony:supervision",
953
+ /** Sondes de la barre de debug (mêmes mesures que la supervision, ticker séparé). */
954
+ debugbar: "nodefony:debugbar",
955
+ /** Auto-observabilité de la socket elle-même (canaux, abonnés, fan-out, backpressure). */
956
+ socket: "nodefony:socket",
957
+ /** Santé des connecteurs de base (état, ping). */
958
+ ormHealth: "nodefony:orm:health",
959
+ /** Flux ORM (débit de requêtes, latence, requêtes lentes) — plus dynamique que la santé. */
960
+ ormFlow: "nodefony:orm:flow",
961
+ /** Forage détaillé d'un connecteur sur un process donné : `…@<pid>`. */
962
+ ormRich: "nodefony:orm:rich",
963
+ /** Sortie d'un job de génération de code : `…@<jobId>` (développement uniquement). */
964
+ scaffoldJob: "nodefony:scaffold:job"
965
+ };
966
+ /**
967
+ * Méthodes **RPC** de la plateforme (le client demande, le serveur répond).
968
+ *
969
+ * Elles portent la même marque que les canaux pour la même raison : ce sont des
970
+ * surfaces du serveur, pas de l'application. `nodefony:kernel:gc` déclenche un cycle
971
+ * de ramasse-miettes bloquant — un nom aussi lourd de conséquences n'a rien à faire
972
+ * dans l'espace de nommage où une application déclare ses propres actions.
973
+ *
974
+ * ⚠️ À ne pas confondre avec les **notifications du protocole** (`realtime:welcome`,
975
+ * `realtime:denied`) : celles-là décrivent la mécanique de la socket elle-même, pas
976
+ * une surface de la plateforme, et ne sont ni des canaux ni des actions.
977
+ */
978
+ const PLATFORM_METHODS = {
979
+ /** Liveness + mesure du temps d'aller-retour. */
980
+ ping: "nodefony:kernel:ping",
981
+ /** Force un cycle GC (seulement si le pod tourne avec `--expose-gc`). */
982
+ gc: "nodefony:kernel:gc",
983
+ /** Ce qu'un scaffold écrirait — fichiers créés, diff des réécritures — sans rien écrire. */
984
+ scaffoldPreview: "nodefony:scaffold:preview",
985
+ /** Démarre un job de génération de code et rend son identifiant. */
986
+ scaffoldRun: "nodefony:scaffold:run",
987
+ /** Demande l'arrêt d'un job de génération en cours. */
988
+ scaffoldCancel: "nodefony:scaffold:cancel"
989
+ };
990
+ /**
991
+ * Événements **DOM** de la plateforme (`window`), dans le même espace de nommage.
992
+ *
993
+ * Ils ne passent pas par la socket — ce sont des `CustomEvent` que le navigateur
994
+ * fait circuler entre la barre de debug, le pont HMR et l'écran qui les écoute.
995
+ * Ils portent la marque pour la même raison que les canaux : `window` est un
996
+ * espace partagé avec l'application, et un nom d'événement générique s'y
997
+ * télescoperait.
998
+ */
999
+ const PLATFORM_EVENTS = {
1000
+ /** Un hot-update Vite vient d'être appliqué (émis par le pont du template). */
1001
+ hmr: "nodefony:hmr",
1002
+ /** L'utilisateur a choisi une entrée dans la barre de debug. */
1003
+ debugbarSelect: "nodefony:debugbar:select"
1004
+ };
1005
+ //#endregion
1006
+ //#region src/client/realtime/RealtimeClient.ts
1007
+ /**
1008
+ * RealtimeClient — Core isomorphe Nodefony côté navigateur (P14.11).
1009
+ *
1010
+ * Protocole : JSON-RPC 2.0 maison sur WebSocket (+ HTTP long-polling fallback TODO P13).
1011
+ * State machine : disconnected → connecting → connected → reconnecting → error.
1012
+ *
1013
+ * Features :
1014
+ * - reconnect exponentiel avec back-off
1015
+ * - pub/sub (on/off/emit) — notifications JSON-RPC sans id
1016
+ * - request/response — RPC bidirectionnel typé
1017
+ * - streaming — chunks `{ id, stream: { chunk, done } }` pour LLM token-by-token
1018
+ *
1019
+ * Le backend WS de référence (RealtimeService) sera implémenté en P13.4.
1020
+ * En attendant, ce client peut parler à n'importe quel serveur JSON-RPC 2.0.
1021
+ */
1022
+ /** Taille max du ring du log protocole (inspecteur realtime). */
1023
+ const FRAME_LOG_MAX = 300;
1024
+ /** Clés sensibles masquées dans le log protocole (sécurité — jamais de secret en clair). */
1025
+ const FRAME_REDACT_RE = /(token|password|secret|api[_-]?key|apikey|authorization|bearer)/i;
1026
+ /** Copie d'un payload JSON-RPC avec masquage des champs sensibles (bornée en profondeur). */
1027
+ function redactFrame(value, depth = 0) {
1028
+ if (depth > 4 || value === null || typeof value !== "object") return value;
1029
+ if (Array.isArray(value)) return value.map((v) => redactFrame(v, depth + 1));
1030
+ const out = {};
1031
+ for (const [k, v] of Object.entries(value)) out[k] = FRAME_REDACT_RE.test(k) ? "[redacted]" : redactFrame(v, depth + 1);
1032
+ return out;
1033
+ }
1034
+ var RealtimeClient = class RealtimeClient {
1035
+ opts;
1036
+ transport = null;
1037
+ transportFactory;
1038
+ _state = "disconnected";
1039
+ peer = new JsonRpcPeer({
1040
+ send: (frame) => this.send(frame),
1041
+ onNotification: (method, params) => this.dispatchNotification(method, params)
1042
+ });
1043
+ handlers = /* @__PURE__ */ new Map();
1044
+ _subscriptions = /* @__PURE__ */ new Map();
1045
+ _welcomed = false;
1046
+ reconnectAttempt = 0;
1047
+ _nextRetryAt = null;
1048
+ reconnectTimer = null;
1049
+ heartbeatTimer = null;
1050
+ intentionalClose = false;
1051
+ _stats = /* @__PURE__ */ new Map();
1052
+ _framesReceived = 0;
1053
+ _framesUnsent = 0;
1054
+ _unsentNotified = true;
1055
+ _lastFrameAt = null;
1056
+ _lastFrameMethod = null;
1057
+ statsTimer = null;
1058
+ _prevSampled = /* @__PURE__ */ new Map();
1059
+ _rawFrames = null;
1060
+ _identity = null;
1061
+ _serverChannels = null;
1062
+ _serverMethods = null;
1063
+ constructor(opts = {}, transportFactory) {
1064
+ this.opts = opts;
1065
+ this.transportFactory = transportFactory ?? ((url) => new BrowserWsTransport(url));
1066
+ this.startStatsSampler();
1067
+ announceRealtime(this.opts.banner);
1068
+ }
1069
+ /**
1070
+ * Instance de connexion **partagée par URL** (résolue en absolu) sur
1071
+ * `globalThis`. Plusieurs consommateurs d'une même page (ex. Studio + barre de
1072
+ * debug) obtiennent la MÊME instance → **une seule socket** WebSocket. Les
1073
+ * options ne s'appliquent qu'à la 1ʳᵉ création (les suivantes réutilisent).
1074
+ *
1075
+ * @param opts - options (au moins `url`), appliquées seulement à la création.
1076
+ * @returns l'instance partagée pour cette URL.
1077
+ */
1078
+ static shared(opts = {}) {
1079
+ const key = RealtimeClient.resolveUrl(opts.url);
1080
+ const g = globalThis;
1081
+ const map = g.__nfRealtime__ ??= /* @__PURE__ */ new Map();
1082
+ let client = map.get(key);
1083
+ if (!client) {
1084
+ client = new RealtimeClient({
1085
+ ...opts,
1086
+ url: key
1087
+ });
1088
+ map.set(key, client);
1089
+ }
1090
+ return client;
1091
+ }
1092
+ /**
1093
+ * Message d'une adresse manquante — LA seule formulation, employée par les
1094
+ * deux chemins qui peuvent la constater.
1095
+ *
1096
+ * Il n'y a **pas** de valeur par défaut, et c'est délibéré : la route dépend
1097
+ * de l'application, pas du framework. Deviner `/nodefony/api/realtime` — ce
1098
+ * que faisait ce client — donnait une socket qui ne se connecte jamais et se
1099
+ * contente de retenter, sans un mot. Un échec franc coûte trente secondes ;
1100
+ * une socket silencieusement morte coûte une soirée.
1101
+ */
1102
+ static missingUrl() {
1103
+ return /* @__PURE__ */ new Error("[nodefony] adresse du serveur temps réel manquante.\n Donne-la explicitement : RealtimeClient.shared({ url: \"/api/live/realtime\" })\n ou, en React : <NodefonyProvider url=\"/api/live/realtime\">\n Il n'y a pas de valeur par défaut : la route dépend de ton application (une application générée monte /api/live/realtime, la console d'administration /nodefony/studio/api/realtime).");
1104
+ }
1105
+ /** Résout une URL (relative ou absolue) en chaîne absolue stable = clé du singleton. */
1106
+ static resolveUrl(url) {
1107
+ if (!url) throw RealtimeClient.missingUrl();
1108
+ if (typeof window === "undefined") return url;
1109
+ try {
1110
+ const u = new URL(url, window.location.href);
1111
+ if (u.protocol === "http:") u.protocol = "ws:";
1112
+ else if (u.protocol === "https:") u.protocol = "wss:";
1113
+ return u.toString();
1114
+ } catch {
1115
+ throw new Error(`[nodefony] adresse du serveur temps réel illisible : ${JSON.stringify(url)}`);
1116
+ }
1117
+ }
1118
+ get state() {
1119
+ return this._state;
1120
+ }
1121
+ /** Nombre de tentatives de reconnexion depuis la dernière connexion réussie. */
1122
+ get reconnectAttempts() {
1123
+ return this.reconnectAttempt;
1124
+ }
1125
+ /**
1126
+ * Timestamp (ms epoch) de la prochaine tentative de reconnexion planifiée,
1127
+ * `null` hors backoff. Permet un compte à rebours UI synchronisé au vrai délai.
1128
+ */
1129
+ get nextRetryAt() {
1130
+ return this._nextRetryAt;
1131
+ }
1132
+ /**
1133
+ * Force une reconnexion **immédiate** (annule le backoff en cours). No-op si
1134
+ * déjà connecté/connexion en cours.
1135
+ */
1136
+ retryNow() {
1137
+ if (this._state === "connected" || this._state === "connecting") return;
1138
+ if (this.reconnectTimer) {
1139
+ clearTimeout(this.reconnectTimer);
1140
+ this.reconnectTimer = null;
1141
+ }
1142
+ this._nextRetryAt = null;
1143
+ this.intentionalClose = false;
1144
+ this.openSocket().catch(() => {});
1145
+ }
1146
+ /**
1147
+ * Ouvre la connexion WS. Idempotent — si déjà ouverte, no-op.
1148
+ * Resolve une fois `connected` ou reject sur erreur fatale.
1149
+ */
1150
+ async connect(url) {
1151
+ if (this._state === "connected" || this._state === "connecting") return;
1152
+ this.intentionalClose = false;
1153
+ this.opts.url = url ?? this.requireUrl();
1154
+ return this.openSocket();
1155
+ }
1156
+ /** L'URL configurée, ou l'échec franc — jamais une valeur devinée. */
1157
+ requireUrl() {
1158
+ if (!this.opts.url) throw RealtimeClient.missingUrl();
1159
+ return this.opts.url;
1160
+ }
1161
+ disconnect() {
1162
+ this.intentionalClose = true;
1163
+ this.clearTimers();
1164
+ this.transport?.close(1e3, "client disconnect");
1165
+ this.peer.dispose("client disconnect");
1166
+ this._welcomed = false;
1167
+ this._identity = null;
1168
+ this._serverChannels = null;
1169
+ this._serverMethods = null;
1170
+ this.fireLocal(LOCAL_EVENTS.identity, null);
1171
+ this.setState("disconnected");
1172
+ }
1173
+ /** Pub/sub local — handler sur un event server-pushed (notification JSON-RPC). */
1174
+ on(event, handler) {
1175
+ if (!this.handlers.has(event)) this.handlers.set(event, /* @__PURE__ */ new Set());
1176
+ this.handlers.get(event).add(handler);
1177
+ return () => this.off(event, handler);
1178
+ }
1179
+ off(event, handler) {
1180
+ this.handlers.get(event)?.delete(handler);
1181
+ }
1182
+ /**
1183
+ * S'abonne aux **notices normalisées** émises par le client : criticités qui
1184
+ * cassent le temps réel (close codes RFC 6455 interprétés via
1185
+ * {@link closeCodeToNotice}), erreurs serveur poussées, rétablissement de
1186
+ * connexion. Réutilisable par toute app — le centre de notifications (snackbar
1187
+ * Studio) s'y branche directement.
1188
+ *
1189
+ * @param handler - reçoit chaque {@link NodefonyNotice}.
1190
+ * @returns dispose (désabonnement).
1191
+ */
1192
+ onNotice(handler) {
1193
+ return this.on(LOCAL_EVENTS.notice, handler);
1194
+ }
1195
+ /**
1196
+ * S'abonne aux **refus de canal** poussés par le serveur (`realtime:denied`) :
1197
+ * un `subscribe`/push vers un canal protégé sans droit suffisant. Permet une
1198
+ * réaction CIBLÉE par canal (griser un contrôle, demander une élévation),
1199
+ * complémentaire de {@link onNotice} (UX générique). Event LOCAL `__denied__`.
1200
+ *
1201
+ * Polymorphisme CLIENT : la réaction est pluggable ici ; l'AUTORITÉ reste
1202
+ * serveur (le firewall a déjà décidé, le motif est générique — pas d'oracle).
1203
+ *
1204
+ * @param handler - reçoit `{ channel, reason }`.
1205
+ * @returns dispose (désabonnement).
1206
+ */
1207
+ onDenied(handler) {
1208
+ return this.on(LOCAL_EVENTS.denied, handler);
1209
+ }
1210
+ /**
1211
+ * Notification one-way client → server (pas de réponse attendue).
1212
+ *
1213
+ * C'est la SEULE émission dont personne n'apprend l'échec autrement : une
1214
+ * requête corrélée est rejetée, un (dés)abonnement est rejoué au reconnect —
1215
+ * une notification applicative perdue, elle, ne laisse aucune trace. Si le
1216
+ * transport est fermé, l'utilisateur en est donc averti (une fois par épisode
1217
+ * de coupure, cf {@link send}).
1218
+ */
1219
+ emit(method, params) {
1220
+ if (!this._emitRaw(method, params)) this.noticeUnsent();
1221
+ }
1222
+ /**
1223
+ * Émission interne sans typage strict — utilisée pour les notifications système
1224
+ * (`subscribe`, `unsubscribe`, `ping`) qui ne figurent pas dans la map `Emit`
1225
+ * utilisateur. Bypasse les types conditionnels de {@link emit}.
1226
+ *
1227
+ * Ces frames-là **ne préviennent pas** l'utilisateur quand elles se perdent :
1228
+ * les abonnements sont rejoués à la reconnexion et le heartbeat est arrêté
1229
+ * avec les timers. Avertir ferait paraître comme une perte ce que le client
1230
+ * rattrape tout seul — du bruit dans le centre de notifications d'une app qui
1231
+ * monte et démonte des vues (le cas de Studio).
1232
+ *
1233
+ * @returns `false` si le transport n'a pas émis la frame.
1234
+ */
1235
+ _emitRaw(method, params) {
1236
+ return this.peer.notify(method, params);
1237
+ }
1238
+ /**
1239
+ * Émet sur un canal — verbe socket de {@link IRealtimeSocket.publish}. Côté client =
1240
+ * notification au serveur (alias clair de {@link emit}).
1241
+ */
1242
+ publish(channel, payload) {
1243
+ this.emit(channel, payload);
1244
+ }
1245
+ /**
1246
+ * S'abonne à un canal pub/sub serveur (**ref-compté**). Émet la notification
1247
+ * `subscribe` au serveur UNIQUEMENT au 1er consommateur du canal ; les suivants
1248
+ * ne font qu'incrémenter le compteur. Ré-émis automatiquement à chaque
1249
+ * `realtime:welcome` — jamais à la simple ouverture de la socket, où le serveur
1250
+ * le jetterait sans un mot. NE remplace PAS {@link on} : `on(channel, h)` REÇOIT les
1251
+ * messages, `subscribe(channel)` DEMANDE au serveur de les pousser.
1252
+ *
1253
+ * Autorité unique partagée par le binding `nodefony/react` ET le store Studio →
1254
+ * deux consommateurs du même canal ne se coupent plus l'un l'autre.
1255
+ */
1256
+ subscribe(channel) {
1257
+ const c = channel;
1258
+ const n = (this._subscriptions.get(c) ?? 0) + 1;
1259
+ this._subscriptions.set(c, n);
1260
+ if (n === 1 && this._welcomed) this._emitRaw("subscribe", { channel: c });
1261
+ }
1262
+ /**
1263
+ * Désabonne un consommateur d'un canal (ref-compté) : émet `unsubscribe` au
1264
+ * serveur seulement au **dernier** consommateur. No-op si le canal n'est pas suivi.
1265
+ */
1266
+ unsubscribe(channel) {
1267
+ const c = channel;
1268
+ const cur = this._subscriptions.get(c);
1269
+ if (!cur) return;
1270
+ if (cur <= 1) {
1271
+ this._subscriptions.delete(c);
1272
+ if (this._welcomed) this._emitRaw("unsubscribe", { channel: c });
1273
+ } else this._subscriptions.set(c, cur - 1);
1274
+ }
1275
+ /** Canaux actuellement abonnés (≥ 1 consommateur). Lecture seule. */
1276
+ get subscribedChannels() {
1277
+ return Array.from(this._subscriptions.keys());
1278
+ }
1279
+ /**
1280
+ * L'adresse du serveur temps réel, telle qu'elle a été donnée (ou résolue en
1281
+ * absolu par {@link shared}). `null` tant qu'aucune n'a été fournie — le
1282
+ * framework n'en devine aucune.
1283
+ *
1284
+ * Publiée pour l'auto-observation : un instantané de socket qui ne dit pas DE
1285
+ * QUELLE socket il parle ne sert à rien dès qu'une page en tient plus d'une.
1286
+ */
1287
+ get url() {
1288
+ return this.opts.url ?? null;
1289
+ }
1290
+ /**
1291
+ * Identité de la connexion, **annoncée par le serveur** au `realtime:welcome`
1292
+ * ({@link RealtimeIdentity}). `null` tant qu'aucun welcome n'a été reçu ; une
1293
+ * fois reçu, un visiteur anonyme a `authenticated: false` (jamais `null`). Un
1294
+ * consommateur (Studio) sait ainsi s'il doit afficher le login **sans taper de
1295
+ * route**. Rafraîchie à chaque (re)connexion ; remise à `null` au
1296
+ * {@link disconnect} volontaire (logout).
1297
+ */
1298
+ get identity() {
1299
+ return this._identity;
1300
+ }
1301
+ /** Canaux pub/sub annoncés par le serveur au welcome (découverte). `null` si pas (encore) reçu. */
1302
+ get serverChannels() {
1303
+ return this._serverChannels;
1304
+ }
1305
+ /** Actions RPC annoncées par le serveur au welcome (découverte). `null` si pas (encore) reçu. */
1306
+ get serverMethods() {
1307
+ return this._serverMethods;
1308
+ }
1309
+ /**
1310
+ * S'abonne à l'identité résolue (event LOCAL `__identity__`, jamais réseau) :
1311
+ * le handler est rappelé à chaque (re)welcome et au `disconnect()`. Permet à
1312
+ * l'UI de basculer anonyme↔authentifié sans polling ni route `/auth/me`.
1313
+ *
1314
+ * @param handler - reçoit la {@link RealtimeIdentity} courante (ou `null`).
1315
+ * @returns dispose (désabonnement).
1316
+ */
1317
+ onIdentity(handler) {
1318
+ return this.on(LOCAL_EVENTS.identity, handler);
1319
+ }
1320
+ /**
1321
+ * S'abonne à l'**état de la connexion** (event LOCAL `__state__`, jamais réseau) :
1322
+ * le handler est rappelé à chaque transition (`connecting`, `connected`,
1323
+ * `reconnecting`, `error`, `disconnected`), jamais pour un état inchangé.
1324
+ *
1325
+ * C'est la porte à employer partout — un badge de connexion, une liaison de vue,
1326
+ * une barre de debug. Écrire `on("__state__", …)` en clair marche aussi, mais
1327
+ * recopie un nom d'événement interne dans du code applicatif : c'est ce qui a
1328
+ * fait diverger les trois gabarits d'application.
1329
+ *
1330
+ * @param handler - reçoit le nouvel {@link RealtimeState}.
1331
+ * @returns dispose (désabonnement).
1332
+ */
1333
+ onState(handler) {
1334
+ return this.on(LOCAL_EVENTS.state, handler);
1335
+ }
1336
+ /**
1337
+ * S'abonne au **tick d'échantillonnage des statistiques** (event LOCAL
1338
+ * `__stats__`, jamais réseau), émis une fois par seconde après recalcul des
1339
+ * débits par canal. Sans charge utile : lire ensuite {@link getChannelStats}
1340
+ * ou {@link getStats}, dont les valeurs viennent d'être rafraîchies.
1341
+ *
1342
+ * @param handler - appelé à chaque échantillon.
1343
+ * @returns dispose (désabonnement).
1344
+ */
1345
+ onStats(handler) {
1346
+ return this.on(LOCAL_EVENTS.stats, handler);
1347
+ }
1348
+ /**
1349
+ * S'abonne aux **tentatives de reconnexion programmées** (event LOCAL
1350
+ * `__reconnect__`, jamais réseau) : numéro d'essai, délai retenu par le
1351
+ * back-off, et échéance absolue. De quoi afficher un compte à rebours plutôt
1352
+ * qu'un « déconnecté » muet.
1353
+ *
1354
+ * @param handler - reçoit `{ attempt, delay, nextRetryAt }`.
1355
+ * @returns dispose (désabonnement).
1356
+ */
1357
+ onReconnect(handler) {
1358
+ return this.on(LOCAL_EVENTS.reconnect, handler);
1359
+ }
1360
+ /**
1361
+ * Handle « socket-like » d'un canal ({@link IRealtimeChannel}) — fine liaison sur
1362
+ * les primitives (`subscribe`/`on`/`publish`/`unsubscribe`), forme naturelle des
1363
+ * canaux à état (SIP, bridge) et point d'accroche des couches à venir (codec,
1364
+ * cadence, politique). N'ouvre rien : appeler `.open()` pour s'abonner. `kind` reste
1365
+ * indéfini côté client tant que le serveur ne l'annonce pas.
1366
+ */
1367
+ channel(name) {
1368
+ const hub = this;
1369
+ const disposers = /* @__PURE__ */ new Set();
1370
+ return {
1371
+ name,
1372
+ on(handler) {
1373
+ const dispose = hub.on(name, handler);
1374
+ disposers.add(dispose);
1375
+ return () => {
1376
+ dispose();
1377
+ disposers.delete(dispose);
1378
+ };
1379
+ },
1380
+ send(payload) {
1381
+ hub.publish(name, payload);
1382
+ },
1383
+ open() {
1384
+ hub.subscribe(name);
1385
+ },
1386
+ close() {
1387
+ for (const d of disposers) d();
1388
+ disposers.clear();
1389
+ hub.unsubscribe(name);
1390
+ }
1391
+ };
1392
+ }
1393
+ /**
1394
+ * S'abonne à un canal d'ÉTAT en **cadence adaptative** (AIMD client-driven) : la lib
1395
+ * mesure la gigue d'arrivée et ré-abonne automatiquement à une cadence plus grossière en
1396
+ * cas de famine, plus fine quand c'est sain — sans changement serveur. `handler` reçoit
1397
+ * les frames à travers les changements de cadence. Réservé aux canaux latest-wins.
1398
+ *
1399
+ * @param base - canal de base (sans suffixe de cadence).
1400
+ * @param handler - reçoit le payload de chaque frame.
1401
+ * @param options - cadence désirée + réglages AIMD (cf {@link BindAdaptiveOptions}).
1402
+ * @returns une poignée {@link AdaptiveChannelBinding} (cadence courante + `dispose`).
1403
+ */
1404
+ adaptiveChannel(base, handler, options) {
1405
+ return bindAdaptiveChannel(this, base, handler, options);
1406
+ }
1407
+ async request(method, params, timeoutMs = 3e4) {
1408
+ if (method.charCodeAt(0) === 47) {
1409
+ if (typeof params === "number") timeoutMs = params;
1410
+ params = { path: method };
1411
+ method = "api.request";
1412
+ }
1413
+ return this.peer.request(method, params, timeoutMs);
1414
+ }
1415
+ /**
1416
+ * **Mutation** API par le pont `api.request` (POST/PUT/PATCH/DELETE) — pendant
1417
+ * d'écriture de {@link request} (qui ne fait que des lectures GET). Transporte
1418
+ * la méthode HTTP **logique**, le corps, et une **clé d'idempotence**
1419
+ * (OBLIGATOIRE côté serveur : une socket reconnecte et peut rejouer une frame
1420
+ * en vol → la clé dédoublonne le rejeu, anti double-effet) :
1421
+ * ```ts
1422
+ * await socket.mutate("/nodefony/security/api/apikeys/42/revoke", {
1423
+ * method: "POST", idempotencyKey: crypto.randomUUID(),
1424
+ * });
1425
+ * ```
1426
+ * Échec → rejet {@link RpcError} (`data.status` = statut HTTP équivalent :
1427
+ * 400 clé absente, 409 rejeu concurrent, 403 refus, 404 path inconnu…).
1428
+ */
1429
+ async mutate(path, init) {
1430
+ return this.peer.request("api.request", {
1431
+ path,
1432
+ method: init.method,
1433
+ body: init.body,
1434
+ idempotencyKey: init.idempotencyKey
1435
+ }, init.timeoutMs ?? 3e4);
1436
+ }
1437
+ /**
1438
+ * Appel API par la socket rendant l'**enveloppe complète** — le résultat ET
1439
+ * la méta serveur — là où {@link request}/{@link mutate} ne rendent que la
1440
+ * valeur (forme courante, contrat « snapshot ≡ GET REST »).
1441
+ *
1442
+ * Utile à l'outillage plus qu'aux applications : en développement, le serveur
1443
+ * joint le `requestId` du profil de CETTE frame, ce qui permet d'aller lire sa
1444
+ * radiographie (`GET /nodefony/profiler/api/{requestId}` — phases, SQL,
1445
+ * décision du firewall). En production le serveur n'émet aucune méta :
1446
+ * `requestId` vaut alors `null`, et l'appel coûte exactement le même trafic.
1447
+ *
1448
+ * ```ts
1449
+ * const { result, requestId } = await socket.call("/nodefony/kernel/api/modules");
1450
+ * ```
1451
+ *
1452
+ * Échec → rejet {@link RpcError}, comme {@link request} ; l'id du profil, lui,
1453
+ * voyage dans `error.data.requestId` (un refus se radiographie aussi).
1454
+ *
1455
+ * @param path - chemin de la route (transport WEBSOCKET requis côté serveur).
1456
+ * @param init - méthode logique (défaut `GET`), corps, clé d'idempotence.
1457
+ */
1458
+ async call(path, init) {
1459
+ const { result, meta } = await this.peer.requestTraced("api.request", {
1460
+ path,
1461
+ method: init?.method ?? "GET",
1462
+ body: init?.body,
1463
+ idempotencyKey: init?.idempotencyKey
1464
+ }, init?.timeoutMs ?? 3e4);
1465
+ return {
1466
+ result,
1467
+ requestId: typeof meta?.requestId === "string" ? meta.requestId : null
1468
+ };
1469
+ }
1470
+ /**
1471
+ * Mesure le round-trip WS via la méthode RPC standard `nodefony:kernel:ping` — helper
1472
+ * RÉUTILISABLE par tout consommateur (topbar Studio, debug bar, app user) :
1473
+ * la mesure du RTT et la convention `nodefony:kernel:ping` vivent dans la lib cliente,
1474
+ * pas dupliquées dans chaque front. Renvoie le payload serveur enrichi de `rtt`
1475
+ * (ms, aller-retour mesuré côté client). Lève si le serveur ne répond pas
1476
+ * (`request` timeout) ou ne connaît pas la méthode (`-32601`).
1477
+ */
1478
+ async ping(timeoutMs = 5e3) {
1479
+ const now = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
1480
+ const t0 = now();
1481
+ return {
1482
+ ...await this.request(PLATFORM_METHODS.ping, void 0, timeoutMs),
1483
+ rtt: Math.round(now() - t0)
1484
+ };
1485
+ }
1486
+ /** Notification SORTANTE typée (pas de réponse). Pendant de {@link emit}/{@link publish}. */
1487
+ notify(method, params) {
1488
+ this.peer.notify(method, params);
1489
+ }
1490
+ /**
1491
+ * Expose une action appelable PAR LE PAIR (requête entrante serveur→client).
1492
+ * Cœur du duplex débloqué par L0 : sans handler, une requête entrante reçoit
1493
+ * `-32601` ; avec, le `result` repart au serveur (confirmation d'action,
1494
+ * invalidation de cache poussée, health applicatif serveur→client).
1495
+ */
1496
+ register(method, handler) {
1497
+ this.peer.register(method, handler);
1498
+ }
1499
+ /** Retire une action exposée. */
1500
+ unregister(method) {
1501
+ this.peer.unregister(method);
1502
+ }
1503
+ /** Actions exposées par CE client (découverte). ≠ {@link serverMethods} (côté serveur). */
1504
+ get methods() {
1505
+ return this.peer.methods;
1506
+ }
1507
+ /**
1508
+ * Ingestion d'une frame ENTRANTE déjà parsée → log + classification/route par le
1509
+ * moteur. Renvoie sa nature. Une frame `invalid` peut porter une erreur GLOBALE
1510
+ * serveur (`{jsonrpc, error}` hors spec JSON-RPC) → notice (cf {@link handleServerError}).
1511
+ */
1512
+ receive(frame) {
1513
+ this.recordFrame("in", frame);
1514
+ const kind = this.peer.receive(frame);
1515
+ if (kind === "invalid") this.handleServerError(frame);
1516
+ return kind;
1517
+ }
1518
+ /** Annule les requêtes sortantes en attente (fermeture transport / logout). */
1519
+ dispose(reason) {
1520
+ this.peer.dispose(reason);
1521
+ }
1522
+ /** Total de notifications reçues, tous canaux confondus (welcome inclus). */
1523
+ get framesReceived() {
1524
+ return this._framesReceived;
1525
+ }
1526
+ /**
1527
+ * Total de frames qui n'ont **pas** pu partir depuis la création du client
1528
+ * (transport fermé au moment de l'émission). Reste à `0` en fonctionnement
1529
+ * normal ; toute valeur non nulle date d'une coupure ou d'une émission
1530
+ * antérieure à la première connexion.
1531
+ */
1532
+ get framesUnsent() {
1533
+ return this._framesUnsent;
1534
+ }
1535
+ /** Timestamp (ms) de la dernière notification reçue. */
1536
+ get lastFrameAt() {
1537
+ return this._lastFrameAt;
1538
+ }
1539
+ /** `method`/canal de la dernière notification reçue. */
1540
+ get lastFrameMethod() {
1541
+ return this._lastFrameMethod;
1542
+ }
1543
+ /** Snapshot des stats par canal (msgCount/rate/series). Les objets sont les
1544
+ * refs internes — à LIRE, pas à muter (copier les valeurs si besoin). */
1545
+ getStats() {
1546
+ return Array.from(this._stats.values());
1547
+ }
1548
+ /** Stats d'un canal précis (== method JSON-RPC) ou `undefined`. */
1549
+ getChannelStats(method) {
1550
+ return this._stats.get(method);
1551
+ }
1552
+ /** Comptabilise une notification entrante (avant dispatch aux handlers). */
1553
+ trackFrame(method) {
1554
+ const now = Date.now();
1555
+ this._framesReceived++;
1556
+ this._lastFrameAt = now;
1557
+ this._lastFrameMethod = method;
1558
+ let st = this._stats.get(method);
1559
+ if (!st) {
1560
+ st = {
1561
+ method,
1562
+ msgCount: 0,
1563
+ lastMessage: null,
1564
+ rate: 0,
1565
+ series: []
1566
+ };
1567
+ this._stats.set(method, st);
1568
+ }
1569
+ st.msgCount++;
1570
+ st.lastMessage = now;
1571
+ }
1572
+ /**
1573
+ * (Ré)émet un `subscribe` pour chaque canal ref-compté, à la réception du
1574
+ * `realtime:welcome` — le seul instant où le serveur est prêt à l'enregistrer.
1575
+ *
1576
+ * Ouvrir la socket ne suffit pas : pendant que le serveur authentifie, son
1577
+ * transport JSON-RPC n'est pas encore branché et il **jette** les frames
1578
+ * entrantes, sans pouvoir répondre — c'est le contrat écrit par
1579
+ * `RealtimeController.handleRealtime`. Rejouer sur `onOpen` revenait à parler
1580
+ * dans le vide, et perdait en silence deux cas entiers : un `subscribe` posé
1581
+ * avant `start()`, et TOUS les abonnements après une reconnexion.
1582
+ *
1583
+ * Cold path (1×/connexion) : ne parcourt que les canaux réellement suivis, et
1584
+ * n'alloue rien.
1585
+ */
1586
+ replaySubscriptions() {
1587
+ this._welcomed = true;
1588
+ for (const channel of this._subscriptions.keys()) this._emitRaw("subscribe", { channel });
1589
+ }
1590
+ /**
1591
+ * Ingère le `realtime:welcome` : mémorise l'identité résolue + les capabilities
1592
+ * annoncées (canaux/actions découvrables) et émet `__identity__`. Tolérant à un
1593
+ * welcome partiel/legacy (champs absents → `null`). Cold path (1×/connexion).
1594
+ */
1595
+ ingestWelcome(params) {
1596
+ const w = params;
1597
+ if (!w || typeof w !== "object") return;
1598
+ this._identity = w.identity ?? null;
1599
+ this._serverChannels = Array.isArray(w.channels) ? w.channels : null;
1600
+ this._serverMethods = Array.isArray(w.methods) ? w.methods : null;
1601
+ noteServerEnv(w.env);
1602
+ if (this.opts.banner !== false && !hasKernel()) this.detailsSocket();
1603
+ this.fireLocal(LOCAL_EVENTS.identity, this._identity);
1604
+ }
1605
+ /**
1606
+ * Détail de la socket dans la console — le pendant, pour une page SANS noyau,
1607
+ * du groupe que le noyau émet quand il y en a un (#136).
1608
+ *
1609
+ * Une vitrine ne compose rien : sans ceci, elle n'a aucun endroit où lire son
1610
+ * adresse, ses canaux ou son identité, alors que c'est exactement ce qu'on
1611
+ * cherche quand une socket ne pousse rien. Émis au premier accueil, une seule
1612
+ * fois par page, et jamais en production.
1613
+ */
1614
+ detailsSocket() {
1615
+ consoleDetails({
1616
+ adresse: { valeur: this.url ?? "—" },
1617
+ état: { valeur: this.state },
1618
+ identité: { valeur: this._identity?.authenticated ? this._identity.userIdentifier ?? "authentifié" : "anonyme" },
1619
+ canaux: { valeur: this._serverChannels?.join(", ") || "aucun" },
1620
+ actions: { valeur: this._serverMethods?.join(", ") || "aucune" }
1621
+ }, this, "socket nodefony — détail et raccourcis");
1622
+ }
1623
+ /**
1624
+ * Ingère un `realtime:denied` (refus d'abonnement/push poussé par le serveur) :
1625
+ * émet une notice normalisée ({@link onNotice}) ET un event ciblé `__denied__`
1626
+ * ({@link onDenied}) pour une réaction polymorphe de l'app. Tolérant à un
1627
+ * payload partiel (motif par défaut `forbidden`). Cold path (refus rare).
1628
+ */
1629
+ ingestDenied(params) {
1630
+ const p = params;
1631
+ const channel = typeof p?.channel === "string" ? p.channel : "";
1632
+ const brut = p?.reason;
1633
+ const reason = brut === "unknown" || brut === "limit" ? brut : "forbidden";
1634
+ const detail = typeof p?.detail === "string" ? p.detail : void 0;
1635
+ const denied = {
1636
+ channel,
1637
+ reason,
1638
+ ...detail === void 0 ? {} : { detail }
1639
+ };
1640
+ this.fireNotice(deniedToNotice(denied));
1641
+ this.fireLocal(LOCAL_EVENTS.denied, denied);
1642
+ }
1643
+ /**
1644
+ * Dispatch d'une NOTIFICATION entrante — appelé par le moteur via `onNotification`.
1645
+ * Ordre figé : ingestion welcome (1ʳᵉ frame) → stats → handlers locaux + wildcard
1646
+ * (un handler `on("realtime:welcome")` voit donc l'identité déjà ingérée).
1647
+ */
1648
+ dispatchNotification(method, params) {
1649
+ if (method === "realtime:welcome") {
1650
+ this.ingestWelcome(params);
1651
+ this.replaySubscriptions();
1652
+ } else if (method === "realtime:denied") this.ingestDenied(params);
1653
+ this.trackFrame(method);
1654
+ this.handlers.get(method)?.forEach((h) => {
1655
+ try {
1656
+ h(params);
1657
+ } catch {}
1658
+ });
1659
+ this.handlers.get("*")?.forEach((h) => {
1660
+ try {
1661
+ h(method, params);
1662
+ } catch {}
1663
+ });
1664
+ }
1665
+ /**
1666
+ * Frame `invalid` (JSON-RPC strict) portant un `error` sans `id`/`method` = erreur
1667
+ * GLOBALE serveur (extension Nodefony hors spec, ex. refus tardif) → notice pour le
1668
+ * centre de notifications. Frame réellement malformée (sans `.error`) → no-op.
1669
+ */
1670
+ handleServerError(frame) {
1671
+ const err = frame.error;
1672
+ if (!err || typeof err !== "object") return;
1673
+ this.fireNotice({
1674
+ level: "error",
1675
+ title: "Temps réel",
1676
+ message: err.message || "Erreur serveur temps réel",
1677
+ source: "server",
1678
+ code: err.code,
1679
+ ts: Date.now()
1680
+ });
1681
+ }
1682
+ /** Échantillonne le débit (msg/s) + série par canal, 1×/s, puis émet
1683
+ * `__stats__` (event local) pour notifier les consommateurs réactifs. */
1684
+ startStatsSampler() {
1685
+ if (this.statsTimer) return;
1686
+ this.statsTimer = setInterval(() => {
1687
+ for (const st of this._stats.values()) {
1688
+ const prev = this._prevSampled.get(st.method) ?? st.msgCount;
1689
+ st.rate = Math.max(0, st.msgCount - prev);
1690
+ this._prevSampled.set(st.method, st.msgCount);
1691
+ st.series = [...st.series, st.rate].slice(-32);
1692
+ }
1693
+ this.fireLocal(LOCAL_EVENTS.stats);
1694
+ }, 1e3);
1695
+ this.statsTimer.unref?.();
1696
+ }
1697
+ /** Émet une notice normalisée aux abonnés `onNotice` (event local, pas réseau). */
1698
+ fireNotice(notice) {
1699
+ this.fireLocal(LOCAL_EVENTS.notice, notice);
1700
+ }
1701
+ /** Déclenche les handlers locaux d'un event interne (pas d'envoi réseau). */
1702
+ fireLocal(event, ...args) {
1703
+ this.handlers.get(event)?.forEach((h) => {
1704
+ try {
1705
+ h(...args);
1706
+ } catch {}
1707
+ });
1708
+ }
1709
+ openSocket() {
1710
+ return new Promise((resolve, reject) => {
1711
+ this._welcomed = false;
1712
+ this.setState(this.reconnectAttempt > 0 ? "reconnecting" : "connecting");
1713
+ let transport;
1714
+ try {
1715
+ const base = typeof window !== "undefined" ? window.location.href : "http://localhost";
1716
+ const url = new URL(this.requireUrl(), base);
1717
+ if (url.protocol === "http:") url.protocol = "ws:";
1718
+ else if (url.protocol === "https:") url.protocol = "wss:";
1719
+ if (this.opts.token) url.searchParams.set("token", this.opts.token);
1720
+ transport = this.transportFactory(url.toString());
1721
+ this.transport = transport;
1722
+ } catch (e) {
1723
+ this.setState("error");
1724
+ reject(e);
1725
+ return;
1726
+ }
1727
+ transport.onOpen(() => {
1728
+ const wasReconnecting = this.reconnectAttempt > 0;
1729
+ this.reconnectAttempt = 0;
1730
+ this._nextRetryAt = null;
1731
+ this.setState("connected");
1732
+ this.startHeartbeat();
1733
+ if (wasReconnecting) this.fireNotice({
1734
+ level: "success",
1735
+ title: "Temps réel",
1736
+ message: "Connexion temps réel rétablie",
1737
+ source: "realtime",
1738
+ ts: Date.now()
1739
+ });
1740
+ resolve();
1741
+ });
1742
+ transport.onMessage((raw) => this.handleMessage(raw));
1743
+ transport.onError(() => {});
1744
+ transport.onClose((code, reason) => {
1745
+ this.clearTimers();
1746
+ this.transport = null;
1747
+ if (this.intentionalClose) {
1748
+ this.setState("disconnected");
1749
+ return;
1750
+ }
1751
+ const notice = closeCodeToNotice(code, reason);
1752
+ if (notice) this.fireNotice(notice);
1753
+ const reconnectable = isReconnectableCloseCode(code);
1754
+ if (reconnectable && this.opts.autoReconnect !== false) this.scheduleReconnect();
1755
+ else this.setState(reconnectable ? "disconnected" : "error");
1756
+ });
1757
+ transport.connect();
1758
+ });
1759
+ }
1760
+ scheduleReconnect() {
1761
+ this.reconnectAttempt++;
1762
+ const base = this.opts.reconnectDelay ?? 1e3;
1763
+ const max = this.opts.reconnectDelayMax ?? 3e4;
1764
+ const delay = Math.min(base * 2 ** (this.reconnectAttempt - 1), max);
1765
+ this._nextRetryAt = Date.now() + delay;
1766
+ this.setState("reconnecting");
1767
+ this.fireLocal(LOCAL_EVENTS.reconnect, {
1768
+ attempt: this.reconnectAttempt,
1769
+ delay,
1770
+ nextRetryAt: this._nextRetryAt
1771
+ });
1772
+ this.reconnectTimer = setTimeout(() => {
1773
+ this.openSocket().catch(() => {});
1774
+ }, delay);
1775
+ }
1776
+ startHeartbeat() {
1777
+ const interval = this.opts.heartbeatInterval ?? 3e4;
1778
+ this.heartbeatTimer = setInterval(() => {
1779
+ if (this.transport?.readyState === TransportState.OPEN) this._emitRaw("ping", { ts: Date.now() });
1780
+ }, interval);
1781
+ }
1782
+ clearTimers() {
1783
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
1784
+ if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
1785
+ this.reconnectTimer = null;
1786
+ this.heartbeatTimer = null;
1787
+ }
1788
+ /**
1789
+ * Log protocole : les `FRAME_LOG_MAX` dernières frames JSON-RPC (redactées),
1790
+ * enregistrées EN CONTINU → la console « retrace l'instant » dès l'ouverture
1791
+ * (jamais de démarrage à vide). Construction + redaction faites ici, à la lecture.
1792
+ */
1793
+ get frameLog() {
1794
+ return (this._rawFrames ?? []).map((f) => RealtimeClient.buildFrame(f.dir, f.msg, f.ts));
1795
+ }
1796
+ /** Purge le log protocole. */
1797
+ clearFrameLog() {
1798
+ if (this._rawFrames) this._rawFrames.length = 0;
1799
+ }
1800
+ /**
1801
+ * Enregistre une frame dans le ring — coût = 1 push de réf (construction +
1802
+ * redaction DIFFÉRÉES). Émet `__frame__` (frame construite) seulement si la
1803
+ * console écoute.
1804
+ */
1805
+ recordFrame(dir, msg) {
1806
+ const ts = Date.now();
1807
+ (this._rawFrames ??= []).push({
1808
+ ts,
1809
+ dir,
1810
+ msg
1811
+ });
1812
+ if (this._rawFrames.length > FRAME_LOG_MAX) this._rawFrames.shift();
1813
+ const listeners = this.handlers.get("__frame__");
1814
+ if (listeners && listeners.size > 0) this.fireLocal("__frame__", RealtimeClient.buildFrame(dir, msg, ts));
1815
+ }
1816
+ /** Construit une frame affichable (kind/canal/id + payload redacté). */
1817
+ static buildFrame(dir, msg, ts) {
1818
+ const m = msg ?? {};
1819
+ let kind = "?";
1820
+ let channel;
1821
+ if (typeof m.method === "string") {
1822
+ kind = m.method;
1823
+ const p = m.params;
1824
+ if (p && typeof p.channel === "string") channel = p.channel;
1825
+ } else if ("error" in m) kind = "error";
1826
+ else if ("result" in m) kind = "response";
1827
+ return {
1828
+ ts,
1829
+ dir,
1830
+ kind,
1831
+ id: typeof m.id === "number" ? m.id : void 0,
1832
+ channel,
1833
+ payload: redactFrame(msg)
1834
+ };
1835
+ }
1836
+ /**
1837
+ * Émet une frame si — et seulement si — le transport est ouvert.
1838
+ *
1839
+ * La frame n'est **pas** mise en file d'attente : rejouer une intention après
1840
+ * coup peut être pire que la perdre (une commande obsolète appliquée en
1841
+ * retard, un abonnement rétabli sur un écran déjà quitté). Ce qui n'est plus
1842
+ * accepté, c'est de la perdre **en silence** — l'appelant reçoit `false`, une
1843
+ * requête corrélée est rejetée aussitôt par {@link JsonRpcPeer} plutôt qu'au
1844
+ * bout de son timeout, et l'utilisateur est prévenu une fois par épisode.
1845
+ *
1846
+ * @param msg - la frame JSON-RPC à sérialiser.
1847
+ * @returns `true` si la frame est partie, `false` si le transport était fermé.
1848
+ */
1849
+ send(msg) {
1850
+ if (this.transport?.readyState !== TransportState.OPEN) {
1851
+ this._framesUnsent++;
1852
+ return false;
1853
+ }
1854
+ this.transport.send(JSON.stringify(msg));
1855
+ this.recordFrame("out", msg);
1856
+ return true;
1857
+ }
1858
+ /**
1859
+ * Avertit que des notifications applicatives se perdent — **une fois par
1860
+ * épisode** de coupure, jamais par frame : une vue qui pousse en boucle
1861
+ * remplirait sinon le centre de notifications à elle seule.
1862
+ *
1863
+ * Appelé depuis {@link emit} uniquement. Les requêtes corrélées et les
1864
+ * (dés)abonnements ont déjà leur propre voie (rejet, rejeu au reconnect).
1865
+ */
1866
+ noticeUnsent() {
1867
+ if (this._unsentNotified) return;
1868
+ this._unsentNotified = true;
1869
+ this.fireNotice({
1870
+ level: "warning",
1871
+ title: "Temps réel",
1872
+ message: "Connexion interrompue — les messages émis pendant la coupure sont perdus",
1873
+ source: "realtime",
1874
+ ts: Date.now()
1875
+ });
1876
+ }
1877
+ handleMessage(raw) {
1878
+ if (typeof raw !== "string") return;
1879
+ let msg;
1880
+ try {
1881
+ msg = JSON.parse(raw);
1882
+ } catch {
1883
+ return;
1884
+ }
1885
+ this.receive(msg);
1886
+ }
1887
+ setState(s) {
1888
+ if (this._state === s) return;
1889
+ if (s === "connected") this._unsentNotified = false;
1890
+ this._state = s;
1891
+ this.fireLocal(LOCAL_EVENTS.state, s);
1892
+ }
1893
+ };
1894
+ //#endregion
1895
+ //#region src/syslog/Pdu.ts
1896
+ var SysLogSeverity = /* @__PURE__ */ function(SysLogSeverity) {
1897
+ SysLogSeverity[SysLogSeverity["EMERGENCY"] = 0] = "EMERGENCY";
1898
+ SysLogSeverity[SysLogSeverity["ALERT"] = 1] = "ALERT";
1899
+ SysLogSeverity[SysLogSeverity["CRITIC"] = 2] = "CRITIC";
1900
+ SysLogSeverity[SysLogSeverity["ERROR"] = 3] = "ERROR";
1901
+ SysLogSeverity[SysLogSeverity["WARNING"] = 4] = "WARNING";
1902
+ SysLogSeverity[SysLogSeverity["NOTICE"] = 5] = "NOTICE";
1903
+ SysLogSeverity[SysLogSeverity["INFO"] = 6] = "INFO";
1904
+ SysLogSeverity[SysLogSeverity["DEBUG"] = 7] = "DEBUG";
1905
+ return SysLogSeverity;
1906
+ }(SysLogSeverity || {});
1907
+ const sysLogSeverity = [
1908
+ 0,
1909
+ 1,
1910
+ 2,
1911
+ 3,
1912
+ 4,
1913
+ 5,
1914
+ 6,
1915
+ 7
1916
+ ];
1917
+ const translateSeverity = function(severity = "INFO") {
1918
+ if (typeof severity === "number") {
1919
+ if (sysLogSeverity[severity] !== void 0) return sysLogSeverity[severity];
1920
+ else throw new Error(`Not a valid nodefony syslog severity: ${severity}`);
1921
+ } else if (SysLogSeverity[severity] !== void 0) return SysLogSeverity[severity];
1922
+ else throw new Error(`Not a valid nodefony syslog severity: ${severity}`);
1923
+ };
1924
+ const sysLogSeverityObj = Object.entries(SysLogSeverity).reduce((acc, [key, value]) => {
1925
+ acc[key] = value;
1926
+ return acc;
1927
+ }, {});
1928
+ const severityNameMap = new Map(Object.entries(SysLogSeverity).filter(([, v]) => typeof v === "number").map(([k, v]) => [v, k]));
1929
+ const _gBuffer = globalThis.Buffer;
1930
+ const PID = globalThis.process?.pid ?? 0;
1931
+ const fastTypeOf = (value) => {
1932
+ if (value === null) return null;
1933
+ const t = typeof value;
1934
+ if (t !== "object") return t;
1935
+ if (Array.isArray(value)) return "array";
1936
+ if (value instanceof Date) return "date";
1937
+ if (value instanceof RegExp) return "RegExp";
1938
+ if (value instanceof Error) return "Error";
1939
+ if (_gBuffer?.isBuffer(value)) return "buffer";
1940
+ return "object";
1941
+ };
1942
+ /**
1943
+ * Process Data Unit — entrée de log unitaire conforme RFC 5424.
1944
+ *
1945
+ * Une instance représente un événement de log dans le pipeline Syslog.
1946
+ * Stockée dans le `CircularBuffer` (O(1)) et diffusée aux transports
1947
+ * via l'event `onLog` du {@link Syslog}.
1948
+ *
1949
+ * @example
1950
+ * ```ts
1951
+ * const pdu = new Pdu("user logged in", "INFO", "AUTH", "USER_LOGIN", "user@example.com");
1952
+ * pdu.severity; // 6
1953
+ * pdu.severityName; // "INFO"
1954
+ * pdu.payload; // "user logged in"
1955
+ * ```
1956
+ *
1957
+ * @remarks `pdu.severity` est numérique (RFC 5424) pour comparaisons rapides.
1958
+ * `pdu.severityName` est la string (`"INFO"`, `"CRITIC"` — pas `"CRITICAL"`).
1959
+ */
1960
+ let guid = 0;
1961
+ var Pdu = class Pdu {
1962
+ payload;
1963
+ uid;
1964
+ severity;
1965
+ timeStamp;
1966
+ severityName;
1967
+ typePayload;
1968
+ moduleName;
1969
+ msgid;
1970
+ msg;
1971
+ status;
1972
+ /** PID du process émetteur (= procid RFC 5424). Constant, capturé au chargement du module. */
1973
+ pid;
1974
+ /**
1975
+ * Identifiant de la requête en cours (corrélation log↔requête).
1976
+ *
1977
+ * Présent UNIQUEMENT si le Pdu est créé dans une bulle `RequestContext` active
1978
+ * (ALS Node) ET que {@link Pdu.requestIdProvider} a été branché côté Node — voir
1979
+ * `src/index.ts` (barrel Node uniquement, isomorphisme préservé : le bundle
1980
+ * client/browser ne branche pas, le provider reste `null` → 0 alloc, 0 lecture).
1981
+ *
1982
+ * Combiné à `pid` (procid RFC 5424), permet la corrélation **complète** :
1983
+ * `pid` = quel worker, `requestId` = quelle requête. Voyage dans ring buffer +
1984
+ * `nodefony:syslog` + transports JSON (champ public sérialisé naturellement).
1985
+ */
1986
+ requestId;
1987
+ /**
1988
+ * Provider injectable du `requestId` courant (ALS). Branché par le barrel
1989
+ * Node (`src/index.ts`) sur `RequestContext.getRequestId`. Reste `null` côté
1990
+ * browser/debugbar (le bundle client n'importe pas `node:async_hooks`) et
1991
+ * pendant les phases boot pré-Kernel (cohérent : pas de requête en cours).
1992
+ *
1993
+ * Coût : 1 test de référence par Pdu (~5 ns) quand `null` ; ~50-100 ns quand
1994
+ * branché (lecture ALS + accès propriété). Pas d'alloc supplémentaire si
1995
+ * `requestId` est `undefined` (le champ reste non-défini sur l'instance).
1996
+ */
1997
+ static requestIdProvider = null;
1998
+ /**
1999
+ * Construit un Pdu prêt à être publié dans le pipeline Syslog.
2000
+ *
2001
+ * @param pci - payload (string, Error, objet — typé `unknown`, narrower côté lecteur).
2002
+ * @param severity - niveau RFC 5424 (string `"INFO"` ou numérique `6`). Défaut `"INFO"`.
2003
+ * @param moduleName - nom du module/service qui a produit le log. Défaut `"nodefony"`.
2004
+ * @param msgid - catégorie de message (`"AUTH"`, `"ROUTER"`, etc.). Défaut `""`.
2005
+ * @param msg - détail libre optionnel. Défaut `""`.
2006
+ * @param date - timestamp (number, Date, ou string parsable). Défaut `Date.now()`.
2007
+ * @throws Si `severity` n'est pas une valeur valide de {@link SysLogSeverity}.
2008
+ */
2009
+ constructor(pci, severity, moduleName = "nodefony", msgid = "", msg = "", date) {
2010
+ if (date === void 0) this.timeStamp = Date.now();
2011
+ else if (typeof date === "number") this.timeStamp = date;
2012
+ else if (date instanceof Date) this.timeStamp = date.getTime();
2013
+ else this.timeStamp = new Date(date).getTime();
2014
+ this.uid = ++guid;
2015
+ this.severity = translateSeverity(severity);
2016
+ const sName = severityNameMap.get(this.severity);
2017
+ if (sName === void 0) throw new Error(`Invalid severity value: ${this.severity}`);
2018
+ this.severityName = sName;
2019
+ this.typePayload = fastTypeOf(pci);
2020
+ this.payload = pci;
2021
+ this.moduleName = moduleName;
2022
+ this.msgid = msgid;
2023
+ this.msg = msg;
2024
+ this.status = "NOTDEFINED";
2025
+ this.pid = PID;
2026
+ this.requestId = Pdu.requestIdProvider !== null ? Pdu.requestIdProvider() : void 0;
2027
+ }
2028
+ /**
2029
+ * Map bidirectionnelle des sévérités RFC 5424 (`{ INFO: 6, 6: "INFO", ... }`).
2030
+ *
2031
+ * @returns dictionnaire `{ severityName: severityValue }` plus enum reverse.
2032
+ */
2033
+ static sysLogSeverity() {
2034
+ return sysLogSeverityObj;
2035
+ }
2036
+ /**
2037
+ * Convertit une sévérité numérique ou string en son nom canonique.
2038
+ *
2039
+ * @param severity - valeur numérique (`6`) ou string (`"INFO"` ou `"6"`).
2040
+ * @returns nom canonique (`"INFO"`) ou `undefined` si invalide.
2041
+ */
2042
+ static severityToString(severity) {
2043
+ const numericSeverity = typeof severity === "string" ? parseInt(severity, 10) : severity;
2044
+ if (!isNaN(numericSeverity) && SysLogSeverity[numericSeverity] !== void 0) return SysLogSeverity[numericSeverity];
2045
+ const severityKey = Pdu.severityToString(SysLogSeverity[severity]);
2046
+ return severityKey !== void 0 ? severityKey : void 0;
2047
+ }
2048
+ /**
2049
+ * Retourne le timestamp formaté `HH:MM:SS GMT±HHMM (TZ Name)`.
2050
+ *
2051
+ * @returns time string lisible par humain.
2052
+ */
2053
+ getDate() {
2054
+ return new Date(this.timeStamp).toTimeString();
2055
+ }
2056
+ /**
2057
+ * Sérialise le Pdu en string mono-ligne pour transport console/file.
2058
+ *
2059
+ * @returns ligne formatée `TimeStamp:... Log:... ModuleName:... SeverityName:... MessageID:... UID:... Message:...`.
2060
+ */
2061
+ toString() {
2062
+ return `TimeStamp:${this.getDate()} Log:${this.payload} ModuleName:${this.moduleName} SeverityName:${this.severityName} MessageID:${this.msgid} UID:${this.uid} Message:${this.msg}`;
2063
+ }
2064
+ /**
2065
+ * Réhydrate un Pdu existant à partir d'une string JSON — mute uniquement les
2066
+ * clés déjà présentes sur l'instance (filtre `key in this`).
2067
+ *
2068
+ * @param str - JSON sérialisé d'un Pdu.
2069
+ * @returns objet parsé (ou `null` si JSON vide). Mute l'instance courante au passage.
2070
+ */
2071
+ parseJson(str) {
2072
+ const json = JSON.parse(str);
2073
+ Object.entries(json).forEach(([key, value]) => {
2074
+ if (key in this) this[key] = value;
2075
+ });
2076
+ return json;
2077
+ }
2078
+ };
2079
+ //#endregion
2080
+ //#region src/client/debugbar/format.ts
2081
+ /**
2082
+ * Helpers de formatage PURS de la debug bar — aucune dépendance DOM/réseau,
2083
+ * donc unit-testables côté Node (suite mocha du core).
2084
+ */
2085
+ const UNITS = [
2086
+ "B",
2087
+ "KB",
2088
+ "MB",
2089
+ "GB",
2090
+ "TB"
2091
+ ];
2092
+ /**
2093
+ * Formatte un nombre d'octets en unité lisible (1 décimale, base 1024).
2094
+ *
2095
+ * @param bytes - taille en octets (négatif/NaN → "0 B")
2096
+ * @returns ex. `"1.5 MB"`, `"0 B"`
2097
+ */
2098
+ function formatBytes(bytes) {
2099
+ if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
2100
+ let i = 0;
2101
+ let n = bytes;
2102
+ while (n >= 1024 && i < UNITS.length - 1) {
2103
+ n /= 1024;
2104
+ i++;
2105
+ }
2106
+ return `${i === 0 ? n : n.toFixed(1)} ${UNITS[i]}`;
2107
+ }
2108
+ /**
2109
+ * Formatte une durée (secondes) en chaîne compacte (`1h 02m`, `3m 12s`, `45s`).
2110
+ *
2111
+ * @param seconds - uptime en secondes
2112
+ */
2113
+ function formatUptime(seconds) {
2114
+ if (!Number.isFinite(seconds) || seconds < 0) return "0s";
2115
+ const s = Math.floor(seconds);
2116
+ const h = Math.floor(s / 3600);
2117
+ const m = Math.floor(s % 3600 / 60);
2118
+ const sec = s % 60;
2119
+ if (h > 0) return `${h}h ${String(m).padStart(2, "0")}m`;
2120
+ if (m > 0) return `${m}m ${String(sec).padStart(2, "0")}s`;
2121
+ return `${sec}s`;
2122
+ }
2123
+ /**
2124
+ * Classe une valeur 0-100 en palier vert/orange/rouge.
2125
+ *
2126
+ * @param percent - valeur de la jauge
2127
+ * @param warn - seuil orange (défaut 70)
2128
+ * @param crit - seuil rouge (défaut 90)
2129
+ */
2130
+ function gauge(percent, warn = 70, crit = 90) {
2131
+ if (!Number.isFinite(percent)) return "ok";
2132
+ if (percent >= crit) return "crit";
2133
+ if (percent >= warn) return "warn";
2134
+ return "ok";
2135
+ }
2136
+ const ANSI_RE = /\[[0-9;]*m/g;
2137
+ /** Retire les séquences d'échappement ANSI (couleurs terminal) d'une chaîne. */
2138
+ function stripAnsi(input) {
2139
+ return input.replace(ANSI_RE, "");
2140
+ }
2141
+ /**
2142
+ * Calcule l'attribut `points` d'un `<polyline>` SVG (sparkline) à partir d'une
2143
+ * série de valeurs. Pur → testable. Mise à l'échelle sur `[0, max]`.
2144
+ *
2145
+ * @param values - série temporelle (gauche = ancien, droite = récent)
2146
+ * @param width - largeur du viewBox
2147
+ * @param height - hauteur du viewBox
2148
+ * @param max - plafond de l'axe Y (défaut = max de la série, plancher 1)
2149
+ * @returns ex. `"0,14 10,7 20,2"` (vide si < 2 points)
2150
+ */
2151
+ function sparklinePoints(values, width, height, max) {
2152
+ const n = values.length;
2153
+ if (n < 2) return "";
2154
+ const top = max ?? Math.max(...values, 1);
2155
+ const ceil = top > 0 ? top : 1;
2156
+ const step = width / (n - 1);
2157
+ let out = "";
2158
+ for (let i = 0; i < n; i++) {
2159
+ const v = Number.isFinite(values[i]) ? values[i] : 0;
2160
+ const ratio = Math.min(1, Math.max(0, v / ceil));
2161
+ const x = Math.round(i * step * 100) / 100;
2162
+ const y = Math.round((height - ratio * height) * 100) / 100;
2163
+ out += i === 0 ? `${x},${y}` : ` ${x},${y}`;
2164
+ }
2165
+ return out;
2166
+ }
2167
+ /**
2168
+ * Heure locale d'un horodatage, à la milliseconde — `14:03:27.482`.
2169
+ *
2170
+ * La date n'y figure pas : une barre de débogage montre ce qui vient de se
2171
+ * passer, et une date répétée sur chaque ligne coûte de la place sans rien
2172
+ * apprendre. Le détail d'une entrée, lui, rend l'horodatage complet.
2173
+ */
2174
+ function fmtClock(ts) {
2175
+ const d = new Date(ts);
2176
+ const p2 = (n) => n < 10 ? `0${n}` : String(n);
2177
+ const ms = d.getMilliseconds();
2178
+ const p3 = ms < 10 ? `00${ms}` : ms < 100 ? `0${ms}` : String(ms);
2179
+ return `${p2(d.getHours())}:${p2(d.getMinutes())}:${p2(d.getSeconds())}.${p3}`;
2180
+ }
2181
+ //#endregion
2182
+ //#region src/client/debugbar/model.ts
2183
+ /** Sévérité RFC 5424 : ≤ 3 = erreur, 4 = warning. */
2184
+ const SEVERITY_ERROR_MAX = 3;
2185
+ const SEVERITY_WARNING = 4;
2186
+ /** Points conservés dans les séries de sparkline (~1 min à 1 tick/s). */
2187
+ const SERIES_POINTS = 60;
2188
+ function pushCapped(arr, v, cap) {
2189
+ arr.push(v);
2190
+ if (arr.length > cap) arr.shift();
2191
+ }
2192
+ function logText(pci) {
2193
+ if (pci == null) return "";
2194
+ if (typeof pci === "string") return stripAnsi(pci);
2195
+ if (typeof pci === "number" || typeof pci === "boolean") return String(pci);
2196
+ try {
2197
+ return stripAnsi(JSON.stringify(pci));
2198
+ } catch {
2199
+ return String(pci);
2200
+ }
2201
+ }
2202
+ var DebugBarModel = class {
2203
+ _state = "disconnected";
2204
+ _stats = null;
2205
+ _cpuPeak = 0;
2206
+ _heapPeak = 0;
2207
+ _loopPeak = 0;
2208
+ _cpuSeries = [];
2209
+ _heapSeries = [];
2210
+ _loopSeries = [];
2211
+ _logTotal = 0;
2212
+ _errorCount = 0;
2213
+ _warnCount = 0;
2214
+ _dropped = 0;
2215
+ _feed = [];
2216
+ setState(state) {
2217
+ this._state = state;
2218
+ }
2219
+ /** Mémorise le dernier tick stats + alimente les séries/peaks. */
2220
+ ingestStats(payload) {
2221
+ this._stats = payload;
2222
+ const cpu = payload.cpuPercent ?? 0;
2223
+ const loop = payload.eventLoopMs ?? 0;
2224
+ const heapUsed = payload.memory?.heapUsed ?? 0;
2225
+ const heapLimit = payload.memory?.heapLimit ?? 0;
2226
+ const heapPct = heapLimit > 0 ? Math.round(heapUsed / heapLimit * 100) : 0;
2227
+ pushCapped(this._cpuSeries, cpu, SERIES_POINTS);
2228
+ pushCapped(this._heapSeries, heapPct, SERIES_POINTS);
2229
+ pushCapped(this._loopSeries, loop, SERIES_POINTS);
2230
+ if (cpu > this._cpuPeak) this._cpuPeak = cpu;
2231
+ if (heapPct > this._heapPeak) this._heapPeak = heapPct;
2232
+ if (loop > this._loopPeak) this._loopPeak = loop;
2233
+ }
2234
+ /**
2235
+ * Comptabilise un batch de logs coalescés (compteurs cumulés + feed). Chaque
2236
+ * entrée est **réhydratée en {@link Pdu}** — la MÊME classe Core que côté
2237
+ * serveur (severityName/payload canoniques, pas de devinette de champ).
2238
+ */
2239
+ ingestSyslog(payload) {
2240
+ const logs = payload.logs;
2241
+ if (logs) {
2242
+ for (let i = 0; i < logs.length; i++) {
2243
+ const entry = logs[i];
2244
+ if (typeof entry?.severity !== "number") continue;
2245
+ let pdu;
2246
+ try {
2247
+ pdu = new Pdu(entry.payload, entry.severity, entry.moduleName ?? "nodefony", entry.msgid ?? "", entry.msg ?? "");
2248
+ } catch {
2249
+ continue;
2250
+ }
2251
+ const sev = pdu.severity;
2252
+ this._logTotal++;
2253
+ if (sev <= SEVERITY_ERROR_MAX) this._errorCount++;
2254
+ else if (sev === SEVERITY_WARNING) this._warnCount++;
2255
+ this._feed.push({
2256
+ severity: sev,
2257
+ name: pdu.severityName,
2258
+ text: logText(pdu.payload),
2259
+ module: pdu.moduleName,
2260
+ ts: typeof entry.timeStamp === "number" ? entry.timeStamp : Date.now(),
2261
+ uid: typeof entry.uid === "number" ? entry.uid : 0,
2262
+ msgid: typeof entry.msgid === "string" ? entry.msgid : "",
2263
+ pid: typeof entry.pid === "number" ? entry.pid : 0,
2264
+ requestId: typeof entry.requestId === "string" ? entry.requestId : void 0
2265
+ });
2266
+ }
2267
+ if (this._feed.length > 40) this._feed.splice(0, this._feed.length - 40);
2268
+ }
2269
+ if (typeof payload.dropped === "number") this._dropped += payload.dropped;
2270
+ }
2271
+ get view() {
2272
+ const s = this._stats;
2273
+ const mem = s?.memory;
2274
+ const app = s?.app;
2275
+ const heapUsed = mem?.heapUsed ?? 0;
2276
+ const heapLimit = mem?.heapLimit ?? 0;
2277
+ return {
2278
+ state: this._state,
2279
+ appName: app?.name ?? "",
2280
+ appVersion: app?.version ?? "",
2281
+ env: app?.env ?? "",
2282
+ debug: app?.debug ?? false,
2283
+ branch: app?.branch ?? "",
2284
+ cpuPercent: s?.cpuPercent ?? 0,
2285
+ cpuPeak: this._cpuPeak,
2286
+ cpuCount: s?.cpuCount ?? 0,
2287
+ eventLoopMs: s?.eventLoopMs ?? 0,
2288
+ eventLoopPeak: this._loopPeak,
2289
+ uptime: s?.uptime ?? 0,
2290
+ pid: s?.pid ?? 0,
2291
+ instanceId: s?.instanceId ?? "—",
2292
+ loadavg: s?.loadavg ?? [],
2293
+ rss: mem?.rss ?? 0,
2294
+ heapUsed,
2295
+ heapTotal: mem?.heapTotal ?? 0,
2296
+ heapLimit,
2297
+ external: mem?.external ?? 0,
2298
+ heapPercent: heapLimit > 0 ? Math.round(heapUsed / heapLimit * 100) : 0,
2299
+ heapPeak: this._heapPeak,
2300
+ cpuSeries: this._cpuSeries,
2301
+ heapSeries: this._heapSeries,
2302
+ loopSeries: this._loopSeries,
2303
+ logTotal: this._logTotal,
2304
+ errorCount: this._errorCount,
2305
+ warnCount: this._warnCount,
2306
+ dropped: this._dropped,
2307
+ feed: this._feed
2308
+ };
2309
+ }
2310
+ };
2311
+ //#endregion
2312
+ //#region src/client/debugbar/hmr.ts
2313
+ /**
2314
+ * Observation des hot-updates Vite **sans ouvrir de WebSocket**.
2315
+ *
2316
+ * Historique : cette sonde ouvrait jadis un **2ᵉ client** WebSocket `vite-hmr`
2317
+ * sur le serveur HMR de Vite. Ce client passif (il ne renvoyait jamais le
2318
+ * heartbeat attendu) se faisait fermer par Vite, puis se reconnectait en
2319
+ * boucle → une cascade de connexions `wss://…:5173` qui s'ouvrent et se ferment
2320
+ * sans fin dans la console. C'était aussi un WebSocket de trop (philosophie
2321
+ * Nodefony : une seule connexion temps réel côté navigateur).
2322
+ *
2323
+ * Désormais : aucun socket. On écoute un `CustomEvent` `nodefony:hmr` émis sur
2324
+ * `window` par un pont injecté côté page (TemplateHelper, `@nodefony/frontend`),
2325
+ * lequel se branche sur le client HMR **déjà chargé** par Vite via
2326
+ * `createHotContext` — zéro connexion ajoutée.
2327
+ */
2328
+ /** Nom de l'événement DOM émis par le pont HMR côté page. */
2329
+ const HMR_EVENT = PLATFORM_EVENTS.hmr;
2330
+ /**
2331
+ * Observe les hot-updates Vite via l'événement `window` `nodefony:hmr`.
2332
+ * Aucune connexion réseau ouverte. No-op hors navigateur.
2333
+ *
2334
+ * @param onEvent - appelé à chaque hot-update relayé par le pont.
2335
+ * @returns dispose() — retire le listener (OBLIGATOIRE au démontage).
2336
+ */
2337
+ function observeViteHmr(onEvent) {
2338
+ if (typeof window === "undefined") return () => {};
2339
+ const handler = (ev) => {
2340
+ const detail = ev.detail;
2341
+ if (detail && typeof detail.kind === "string") onEvent(detail);
2342
+ };
2343
+ window.addEventListener(HMR_EVENT, handler);
2344
+ return () => window.removeEventListener(HMR_EVENT, handler);
2345
+ }
2346
+ //#endregion
2347
+ //#region src/client/debugbar/network.ts
2348
+ /**
2349
+ * Intercepteur réseau **dev-only** de la debug bar — capture les appels AJAX
2350
+ * (`fetch` + `XMLHttpRequest`) de la page pour le panneau Network, et lit le
2351
+ * header `X-Request-Id` de chaque réponse → corrélation avec le profil serveur
2352
+ * (`/nodefony/profiler/api/{requestId}`).
2353
+ *
2354
+ * ⚠️ Monkey-patch des globals = intrusif. Donc 6 garde-fous (cf décision design) :
2355
+ * 1. **dev-only** : n'est installé que par la debug bar, jamais en prod.
2356
+ * 2. **défensif** : toute la collecte est en try/catch — un bug ici ne casse
2357
+ * JAMAIS la requête de l'app (on relaie toujours l'appel original).
2358
+ * 3. **header-only** : on ne touche jamais au body (pas de `.clone()`, pas de
2359
+ * lecture du stream) → 0 risque de consommer/altérer la réponse.
2360
+ * 4. **réversible** : `uninstall()` restaure les implémentations d'origine.
2361
+ * 5. **chain-safe** : on wrappe le `fetch`/XHR COURANT (déjà potentiellement
2362
+ * patché par une autre lib), on ne clobber pas — l'uninstall ne restaure que
2363
+ * si personne n'a re-patché par-dessus.
2364
+ * 6. **opt-out** : `mountDebugBar({ network:false })` n'installe rien.
2365
+ *
2366
+ * Coût : 2 `performance.now()` + lecture d'1 header + 1 push capé par appel.
2367
+ * Négligeable vs la latence réseau.
2368
+ */
2369
+ /**
2370
+ * En-têtes de corrélation posés par le framework sur chaque réponse :
2371
+ * - `x-request-id` = **clé de lookup** du profiler serveur (concept requestId) ;
2372
+ * - `traceparent` = W3C Trace Context (RFC-propre), affiché pour la corrélation
2373
+ * distribuée — pas la clé du profiler (qui est keyé par requestId).
2374
+ */
2375
+ const REQUEST_ID_HEADER = "x-request-id";
2376
+ const TRACEPARENT_HEADER = "traceparent";
2377
+ /** Extrait le path d'une URL (absolue ou relative), tolérant. */
2378
+ function toPath(url) {
2379
+ try {
2380
+ return new URL(url, location.href).pathname;
2381
+ } catch {
2382
+ return url;
2383
+ }
2384
+ }
2385
+ /** Normalise l'`input` polymorphe de `fetch` en URL string. */
2386
+ function fetchUrl(input) {
2387
+ if (typeof input === "string") return input;
2388
+ if (input && typeof input === "object") {
2389
+ const u = input.url;
2390
+ if (typeof u === "string") return u;
2391
+ }
2392
+ return String(input);
2393
+ }
2394
+ /**
2395
+ * Installe l'intercepteur fetch + XHR. No-op hors navigateur.
2396
+ *
2397
+ * @returns `uninstall()` — restaure les globals (OBLIGATOIRE à l'unmount).
2398
+ */
2399
+ function installNetworkInterceptor(opts) {
2400
+ if (typeof window === "undefined") return () => {};
2401
+ const ignore = opts.ignore ?? (() => false);
2402
+ let seq = 0;
2403
+ const origFetch = window.fetch;
2404
+ const patchedFetch = typeof origFetch === "function" ? function(...args) {
2405
+ let url = "";
2406
+ let method = "GET";
2407
+ try {
2408
+ url = fetchUrl(args[0]);
2409
+ method = (args[1]?.method ?? args[0]?.method ?? "GET").toString().toUpperCase();
2410
+ } catch {}
2411
+ if (!url || ignore(url)) return origFetch.apply(this, args);
2412
+ const entry = {
2413
+ id: ++seq,
2414
+ method,
2415
+ url,
2416
+ path: toPath(url),
2417
+ status: null,
2418
+ ok: false,
2419
+ durationMs: null,
2420
+ startedAt: performance.now(),
2421
+ requestId: null,
2422
+ traceparent: null,
2423
+ pending: true,
2424
+ error: null
2425
+ };
2426
+ safeEmit(opts, entry);
2427
+ return origFetch.apply(this, args).then((res) => {
2428
+ try {
2429
+ entry.status = res.status;
2430
+ entry.ok = res.ok;
2431
+ entry.requestId = res.headers.get(REQUEST_ID_HEADER);
2432
+ entry.traceparent = res.headers.get(TRACEPARENT_HEADER);
2433
+ entry.durationMs = round(performance.now() - entry.startedAt);
2434
+ entry.pending = false;
2435
+ safeEmit(opts, entry);
2436
+ } catch {}
2437
+ return res;
2438
+ }, (err) => {
2439
+ try {
2440
+ entry.error = err instanceof Error ? err.message : "network error";
2441
+ entry.durationMs = round(performance.now() - entry.startedAt);
2442
+ entry.pending = false;
2443
+ safeEmit(opts, entry);
2444
+ } catch {}
2445
+ throw err;
2446
+ });
2447
+ } : null;
2448
+ if (patchedFetch) window.fetch = patchedFetch;
2449
+ const XHR = window.XMLHttpRequest;
2450
+ const origOpen = XHR?.prototype.open;
2451
+ const origSend = XHR?.prototype.send;
2452
+ const state = /* @__PURE__ */ new WeakMap();
2453
+ if (XHR && origOpen && origSend) {
2454
+ XHR.prototype.open = function(method, url, ...rest) {
2455
+ try {
2456
+ const u = url.toString();
2457
+ if (u && !ignore(u)) state.set(this, {
2458
+ id: ++seq,
2459
+ method: (method ?? "GET").toUpperCase(),
2460
+ url: u,
2461
+ path: toPath(u),
2462
+ status: null,
2463
+ ok: false,
2464
+ durationMs: null,
2465
+ startedAt: 0,
2466
+ requestId: null,
2467
+ traceparent: null,
2468
+ pending: true,
2469
+ error: null
2470
+ });
2471
+ } catch {}
2472
+ return origOpen.apply(this, [
2473
+ method,
2474
+ url,
2475
+ ...rest
2476
+ ]);
2477
+ };
2478
+ XHR.prototype.send = function(...args) {
2479
+ const entry = state.get(this);
2480
+ if (entry) {
2481
+ entry.startedAt = performance.now();
2482
+ safeEmit(opts, entry);
2483
+ const finalize = (error) => {
2484
+ try {
2485
+ entry.status = this.status || null;
2486
+ entry.ok = this.status >= 200 && this.status < 400;
2487
+ entry.requestId = this.getResponseHeader(REQUEST_ID_HEADER) || null;
2488
+ entry.traceparent = this.getResponseHeader(TRACEPARENT_HEADER) || null;
2489
+ entry.durationMs = round(performance.now() - entry.startedAt);
2490
+ entry.error = error;
2491
+ entry.pending = false;
2492
+ safeEmit(opts, entry);
2493
+ } catch {}
2494
+ };
2495
+ this.addEventListener("load", () => finalize(null));
2496
+ this.addEventListener("error", () => finalize("network error"));
2497
+ this.addEventListener("abort", () => finalize("aborted"));
2498
+ this.addEventListener("timeout", () => finalize("timeout"));
2499
+ }
2500
+ return origSend.apply(this, args);
2501
+ };
2502
+ }
2503
+ return () => {
2504
+ if (patchedFetch && window.fetch === patchedFetch) window.fetch = origFetch;
2505
+ if (XHR && origOpen && XHR.prototype.open !== origOpen) XHR.prototype.open = origOpen;
2506
+ if (XHR && origSend && XHR.prototype.send !== origSend) XHR.prototype.send = origSend;
2507
+ };
2508
+ }
2509
+ function round(ms) {
2510
+ return Math.round(ms * 10) / 10;
2511
+ }
2512
+ /** Émission protégée — un onChange qui throw ne doit pas casser la requête. */
2513
+ function safeEmit(opts, entry) {
2514
+ try {
2515
+ opts.onChange(entry);
2516
+ } catch {}
2517
+ }
2518
+ //#endregion
2519
+ //#region src/client/debugbar/profile.ts
2520
+ /** Couleur stable par nom de phase canonique. */
2521
+ const PHASE_TIER = {
2522
+ parse: "parse",
2523
+ resolve: "resolve",
2524
+ firewall: "firewall",
2525
+ identity: "identity",
2526
+ initialize: "init",
2527
+ action: "action",
2528
+ render: "render",
2529
+ send: "send"
2530
+ };
2531
+ /** Mappe un nom de phase vers sa classe de couleur (défaut neutre). */
2532
+ function phaseTier(name) {
2533
+ return PHASE_TIER[name] ?? "other";
2534
+ }
2535
+ /** Logs/appels conservés dans le panneau Network. */
2536
+ const NET_MAX = 80;
2537
+ var NetworkModel = class {
2538
+ _byId = /* @__PURE__ */ new Map();
2539
+ _profiles = /* @__PURE__ */ new Map();
2540
+ _total = 0;
2541
+ _errors = 0;
2542
+ /** Upsert d'une entrée (pending puis résolue partagent le même `id`). */
2543
+ ingest(entry) {
2544
+ if (!this._byId.has(entry.id)) this._total++;
2545
+ const prev = this._byId.get(entry.id);
2546
+ const wasErr = prev ? isError(prev) : false;
2547
+ const isErr = isError(entry);
2548
+ if (isErr && !wasErr) this._errors++;
2549
+ else if (!isErr && wasErr) this._errors--;
2550
+ this._byId.set(entry.id, entry);
2551
+ if (this._byId.size > NET_MAX) {
2552
+ const oldest = this._byId.keys().next().value;
2553
+ if (oldest !== void 0) {
2554
+ const ev = this._byId.get(oldest);
2555
+ if (ev && isError(ev)) this._errors--;
2556
+ this._byId.delete(oldest);
2557
+ }
2558
+ }
2559
+ }
2560
+ /** Entrées récentes (récent → ancien) pour le rendu. */
2561
+ entries() {
2562
+ return [...this._byId.values()].reverse();
2563
+ }
2564
+ get total() {
2565
+ return this._total;
2566
+ }
2567
+ get errors() {
2568
+ return this._errors;
2569
+ }
2570
+ get pending() {
2571
+ let n = 0;
2572
+ for (const e of this._byId.values()) if (e.pending) n++;
2573
+ return n;
2574
+ }
2575
+ profileState(requestId) {
2576
+ return this._profiles.get(requestId);
2577
+ }
2578
+ setProfileState(requestId, state) {
2579
+ this._profiles.set(requestId, state);
2580
+ }
2581
+ clear() {
2582
+ this._byId.clear();
2583
+ this._profiles.clear();
2584
+ this._total = 0;
2585
+ this._errors = 0;
2586
+ }
2587
+ };
2588
+ /** Vrai si une entrée est en erreur réseau ou status ≥ 400. */
2589
+ function isError(e) {
2590
+ if (e.error) return true;
2591
+ return e.status !== null && e.status >= 400;
2592
+ }
2593
+ /**
2594
+ * Calcule le layout proportionnel des phases serveur (waterfall). Pur.
2595
+ *
2596
+ * Chaque barre est positionnée sur le span `[min start, max end]` en %. Une
2597
+ * phase de durée nulle reçoit une largeur plancher (visibilité). Span nul →
2598
+ * barres vides (évite la division par zéro).
2599
+ *
2600
+ * @param phases - phases du profil serveur (startMs relatif + durationMs)
2601
+ * @returns barres prêtes à rendre (gauche/largeur en %)
2602
+ */
2603
+ function computeWaterfall(phases) {
2604
+ if (phases.length === 0) return [];
2605
+ let min = Infinity;
2606
+ let max = -Infinity;
2607
+ for (const p of phases) {
2608
+ if (p.startMs < min) min = p.startMs;
2609
+ const end = p.startMs + (p.durationMs ?? 0);
2610
+ if (end > max) max = end;
2611
+ }
2612
+ const span = max - min;
2613
+ return phases.map((p) => {
2614
+ const dur = p.durationMs ?? 0;
2615
+ const leftPct = span > 0 ? (p.startMs - min) / span * 100 : 0;
2616
+ const widthRaw = span > 0 ? dur / span * 100 : 0;
2617
+ return {
2618
+ name: p.name,
2619
+ leftPct: clampPct(leftPct),
2620
+ widthPct: clampPct(Math.max(widthRaw, dur > 0 || span === 0 ? 1.5 : .6)),
2621
+ durationMs: Math.round(dur * 100) / 100,
2622
+ tier: phaseTier(p.name)
2623
+ };
2624
+ });
2625
+ }
2626
+ function clampPct(v) {
2627
+ if (!Number.isFinite(v) || v < 0) return 0;
2628
+ if (v > 100) return 100;
2629
+ return Math.round(v * 100) / 100;
2630
+ }
2631
+ //#endregion
2632
+ //#region src/client/debugbar/DebugBar.ts
2633
+ /**
2634
+ * DebugBar — toolbar de debug par-page type Symfony WDT, **dev-only**, pensée
2635
+ * comme une **vitrine du realtime Nodefony** (on doit *voir* le framework
2636
+ * respirer en direct).
2637
+ *
2638
+ * 2ᵉ consommateur navigateur du Core isomorphe après Studio : MÊME backbone
2639
+ * realtime (WS JSON-RPC 2.0, canaux `nodefony:supervision` / `nodefony:syslog`) via
2640
+ * {@link RealtimeClient}. Aucun rendu serveur splicé dans le body : le serveur
2641
+ * *collecte*, le client *rend*.
2642
+ *
2643
+ * UI : **onglets** (Realtime / Network / Perf / Logs / Runtime) — un seul pane
2644
+ * rendu/visible à la fois (≠ grid fourre-tout) + **poignée de resize** (hauteur
2645
+ * persistée). Vanilla TS + **Shadow DOM** + sparklines **SVG maison** — 0 dep UI.
2646
+ *
2647
+ * Perf scroll (critique) : fond OPAQUE sur le panneau (le `backdrop-filter:blur`
2648
+ * sur le conteneur scrollable recompositait à chaque frame), `contain:content`
2649
+ * par pane, et la liste Network en **mise à jour incrémentale** (nœuds stables,
2650
+ * jamais de rebuild `innerHTML` global → le clic n'est plus perdu, le scroll ne
2651
+ * saute plus).
2652
+ *
2653
+ * Panneau **Network** (dev-only) : intercepte `fetch`/`XHR` (header-only,
2654
+ * défensif, réversible) → liste des appels AJAX ; clic sur un appel → fetch du
2655
+ * profil serveur (`/nodefony/profiler/api/{requestId}`, corrélé via le header
2656
+ * `X-Request-Id`) → **waterfall des phases** du pipeline. SPA-first : on profile
2657
+ * les appels, pas la page. Le `traceparent` W3C (RFC-propre) est aussi remonté.
2658
+ */
2659
+ /** Canaux realtime consommés (figés, alignés sur les providers Studio). */
2660
+ const CHANNELS = {
2661
+ stats: PLATFORM_CHANNELS.debugbar,
2662
+ syslog: PLATFORM_CHANNELS.syslog
2663
+ };
2664
+ /** Endpoint WS realtime par défaut (porté par Studio aujourd'hui, RealtimeService demain). */
2665
+ const DEFAULT_PATH = "/nodefony/studio/api/realtime";
2666
+ /** Base du data-plane profiler (data-plane admin `IAdminApi` namespace `profiler`). */
2667
+ const DEFAULT_PROFILER_BASE = "/nodefony/profiler/api";
2668
+ const HOST_ID = "nodefony-debugbar";
2669
+ /** Dimensions des sparklines (unités viewBox SVG). */
2670
+ const MINI_W = 58;
2671
+ const MINI_H = 20;
2672
+ const CHART_W = 260;
2673
+ const CHART_H = 46;
2674
+ const RT_POINTS = 60;
2675
+ /** Borne basse de hauteur du panneau (px). */
2676
+ const PANEL_H_MIN = 140;
2677
+ /**
2678
+ * Hauteur par défaut = **fraction de l'écran** (≈48 % du viewport) — gros écran
2679
+ * → grand panneau, petit écran → panneau modeste. Bornée [300, 640] pour rester
2680
+ * raisonnable aux extrêmes.
2681
+ */
2682
+ function defaultPanelH() {
2683
+ const vh = typeof window !== "undefined" ? window.innerHeight : 800;
2684
+ return Math.min(640, Math.max(300, Math.round(vh * .48)));
2685
+ }
2686
+ /** Hauteur mini à l'ouverture d'un profil (waterfall confortable) ≈55 % écran. */
2687
+ function detailPanelH() {
2688
+ const vh = typeof window !== "undefined" ? window.innerHeight : 800;
2689
+ return Math.min(680, Math.max(360, Math.round(vh * .55)));
2690
+ }
2691
+ /** Métadonnées d'affichage par framework (couleur de marque officielle). */
2692
+ const FRAMEWORKS = {
2693
+ react19: {
2694
+ label: "React 19",
2695
+ color: "#61dafb"
2696
+ },
2697
+ react: {
2698
+ label: "React",
2699
+ color: "#61dafb"
2700
+ },
2701
+ vue3: {
2702
+ label: "Vue 3",
2703
+ color: "#42b883"
2704
+ },
2705
+ vue: {
2706
+ label: "Vue",
2707
+ color: "#42b883"
2708
+ },
2709
+ angular: {
2710
+ label: "Angular",
2711
+ color: "#dd0031"
2712
+ },
2713
+ vanilla: {
2714
+ label: "Vanilla",
2715
+ color: "#f7df1e"
2716
+ }
2717
+ };
2718
+ const STYLES = `
2719
+ :host { all: initial; }
2720
+ * { box-sizing: border-box; }
2721
+ .bar {
2722
+ position: fixed; left: 0; right: 0; z-index: 2147483000;
2723
+ font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
2724
+ color: #e8eaed;
2725
+ /* Fond OPAQUE (pas de backdrop-filter sur le conteneur scrollable : il
2726
+ recompositait le blur à chaque frame de scroll → lag). Blur seulement sur
2727
+ .strip (fin, non scrollé). */
2728
+ background: #14161a;
2729
+ --blue:#0067ba; --blue2:#3aa0ff; --orange:#ff8a3d; --ok:#36b37e; --warn:#ffab00;
2730
+ --crit:#ff5630; --info:#4c9aff; --muted:#8a9099; --line:#2a2e36; --card:#1c1f26;
2731
+ box-shadow: 0 -8px 40px rgba(0,0,0,.5);
2732
+ }
2733
+ .bar.bottom { bottom: 0; }
2734
+ .bar.top { top: 0; }
2735
+ .bar::before {
2736
+ content:""; position:absolute; left:0; right:0; height:2px;
2737
+ background: linear-gradient(90deg, var(--blue), var(--blue2), var(--orange));
2738
+ background-size: 200% 100%; animation: flow 4s linear infinite; opacity:.9;
2739
+ }
2740
+ .bar.bottom::before { top:0; } .bar.top::before { bottom:0; }
2741
+ @keyframes flow { to { background-position: 200% 0; } }
2742
+
2743
+ /* Strip responsive : police (et tout le contenu en em) scale avec la largeur
2744
+ d'écran, bornée 12→15px. Padding scale aussi. */
2745
+ .strip { display: flex; align-items: center; gap: clamp(12px,1.1vw,20px);
2746
+ padding: clamp(8px,1vh,13px) clamp(14px,1.4vw,26px); cursor: pointer;
2747
+ font-size: clamp(12px, 0.35vw + 8px, 16px); flex-wrap: nowrap; overflow: hidden;
2748
+ background: rgba(20,22,26,.6); backdrop-filter: blur(14px) saturate(140%); }
2749
+ .strip:hover { background: rgba(255,255,255,.03); }
2750
+ .brand { display:flex; align-items:center; gap:8px; font-weight:800; letter-spacing:.2px; flex:none; }
2751
+ .brand .logo { color: var(--blue2); font-size:1.05em; filter: drop-shadow(0 0 6px rgba(58,160,255,.6)); }
2752
+ .brand .name { background: linear-gradient(90deg,#fff,var(--blue2)); -webkit-background-clip:text;
2753
+ background-clip:text; -webkit-text-fill-color:transparent; }
2754
+ .rt-pill { display:flex; align-items:center; gap:5px; padding:.2em .7em; border-radius:11px; flex:none;
2755
+ font-size:.76em; font-weight:800; letter-spacing:.6px; text-transform:uppercase;
2756
+ color:var(--muted); background:#22262e; border:1px solid var(--line); }
2757
+ .rt-pill.connected { color:#fff; border-color:rgba(58,160,255,.5);
2758
+ background: linear-gradient(90deg, rgba(0,103,186,.35), rgba(255,138,61,.25));
2759
+ box-shadow: 0 0 14px rgba(58,160,255,.35); }
2760
+ .rt-pill .bolt { animation: bolt 1.6s ease-in-out infinite; }
2761
+ .rt-pill.connected .bolt { color: var(--orange); }
2762
+ @keyframes bolt { 0%,100%{opacity:.5;transform:scale(.9)} 50%{opacity:1;transform:scale(1.15)} }
2763
+
2764
+ .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); flex: none; color: var(--muted); }
2765
+ .dot.connected { background: var(--ok); color: var(--ok); animation: pulse 2s infinite; }
2766
+ .dot.connecting, .dot.reconnecting { background: var(--warn); color: var(--warn); }
2767
+ .dot.error, .dot.disconnected { background: var(--crit); color: var(--crit); }
2768
+ @keyframes pulse { 0%{box-shadow:0 0 0 0 currentColor} 70%{box-shadow:0 0 0 5px transparent} 100%{box-shadow:0 0 0 0 transparent} }
2769
+
2770
+ .metric { display: flex; align-items: center; gap: 6px; white-space: nowrap; flex:none; }
2771
+ .metric .k { color: var(--muted); text-transform: uppercase; font-size: .76em; letter-spacing:.5px; }
2772
+ .metric .v { font-weight: 700; min-width: 2.6em; font-size: 1em; }
2773
+ .mini { width: 3.8em; height: 1.35em; display:block; }
2774
+ .mini polyline { fill:none; stroke-width:1.5; vector-effect:non-scaling-stroke; }
2775
+ .chip { display:flex; align-items:center; gap:6px; padding:.2em .7em; border-radius:11px; flex:none;
2776
+ background:#22262e; font-weight:700; font-size:.92em; white-space:nowrap; }
2777
+ .chip .k { color: var(--muted); font-size:.82em; text-transform:uppercase; }
2778
+ .spacer { flex: 1 1 auto; min-width: 8px; }
2779
+ .toggle { color: var(--muted); font-size: .85em; transition: transform .25s; flex:none; }
2780
+ .bar.open .toggle { transform: rotate(180deg); }
2781
+ .ok{color:var(--ok)} .warn{color:var(--warn)} .crit{color:var(--crit)} .info{color:var(--info)} .muted{color:var(--muted)} .blue{color:var(--blue2)}
2782
+ .spark.ok{stroke:var(--ok)} .spark.warn{stroke:var(--warn)} .spark.crit{stroke:var(--crit)} .spark.rt{stroke:var(--blue2)}
2783
+ .area.ok{fill:rgba(54,179,126,.12)} .area.warn{fill:rgba(255,171,0,.14)} .area.crit{fill:rgba(255,86,48,.16)} .area.rt{fill:rgba(58,160,255,.16)}
2784
+
2785
+ /* ── Panneau : resize + onglets + panes ──────────────────────────────────── */
2786
+ .panelwrap { display:none; flex-direction:column; border-top:1px solid var(--line); }
2787
+ .bar.open .panelwrap { display:flex; }
2788
+ .resize { height:8px; cursor:ns-resize; display:flex; align-items:center; justify-content:center;
2789
+ flex:none; background:rgba(255,255,255,.015); }
2790
+ .resize::after { content:""; width:42px; height:3px; border-radius:2px; background:var(--line); transition:background .15s; }
2791
+ .resize:hover::after { background:var(--blue2); }
2792
+ .tabs { display:flex; gap:2px; padding:0 8px; flex:none; border-bottom:1px solid var(--line);
2793
+ overflow-x:auto; scrollbar-width:none; }
2794
+ .tabs::-webkit-scrollbar { display:none; }
2795
+ .tab { padding:7px 12px; font:inherit; font-size:11px; font-weight:700; color:var(--muted);
2796
+ cursor:pointer; border:0; background:none; border-bottom:2px solid transparent;
2797
+ text-transform:uppercase; letter-spacing:.4px; white-space:nowrap; display:flex; align-items:center; gap:5px; }
2798
+ .tab:hover { color:#fff; }
2799
+ .tab.active { color:#fff; border-bottom-color:var(--blue2); }
2800
+ .tab .tcount { font-size:9px; padding:0 5px; border-radius:8px; background:#22262e; color:var(--muted); }
2801
+ .tab.active .tcount { background:rgba(58,160,255,.25); color:#fff; }
2802
+ .tab .tcount.crit { background:rgba(255,86,48,.3); color:#fff; }
2803
+ .panes { overflow:hidden; }
2804
+ .pane { display:none; height:100%; overflow:auto; padding:14px; contain:content; }
2805
+ .pane.active { display:block; }
2806
+ .cards { display:grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap:14px; align-items:start; }
2807
+ .card { background:var(--card); border:1px solid var(--line); border-radius:10px; padding:11px 13px; }
2808
+ .card.hero { border-color: rgba(58,160,255,.35);
2809
+ background: linear-gradient(160deg, rgba(0,103,186,.16), rgba(28,31,38,.7) 60%); }
2810
+ .card > h4 { margin:0 0 9px; font-size:10px; letter-spacing:1px; text-transform:uppercase; color:var(--muted); font-weight:800; display:flex; gap:6px; align-items:center; }
2811
+
2812
+ .hero .big { font-size:30px; font-weight:800; line-height:1; letter-spacing:-.5px;
2813
+ background:linear-gradient(90deg,var(--blue2),var(--orange)); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; }
2814
+ .hero .big small { font-size:12px; -webkit-text-fill-color:var(--muted); color:var(--muted); font-weight:700; margin-left:4px; }
2815
+ .hero svg { width:100%; height:38px; display:block; margin:6px 0 8px; }
2816
+ .hero polyline { fill:none; stroke-width:1.75; vector-effect:non-scaling-stroke; }
2817
+ .tag { margin-top:9px; padding-top:8px; border-top:1px solid var(--line); color:var(--muted);
2818
+ font-size:10px; line-height:1.5; }
2819
+ .tag b { color:#cfd3d8; }
2820
+
2821
+ .chart { margin-bottom:10px; } .chart:last-child { margin-bottom:0; }
2822
+ .chart .hd { display:flex; justify-content:space-between; align-items:baseline; margin-bottom:2px; }
2823
+ .chart .lbl { color:var(--muted); font-size:10px; text-transform:uppercase; letter-spacing:.5px; }
2824
+ .chart .val { font-weight:700; font-size:13px; }
2825
+ .chart .peak { color:var(--muted); font-size:9px; }
2826
+ .chart svg { width:100%; height:${CHART_H}px; display:block; }
2827
+ .chart polyline { fill:none; stroke-width:1.5; vector-effect:non-scaling-stroke; }
2828
+
2829
+ .kv { display:flex; justify-content:space-between; gap:8px; padding:2px 0; }
2830
+ .kv .k { color:var(--muted); } .kv .v { font-weight:600; text-align:right; overflow:hidden; text-overflow:ellipsis; }
2831
+
2832
+ .counts { display:flex; gap:8px; margin-bottom:8px; flex-wrap:wrap; }
2833
+ .feed { font-size:11px; }
2834
+ /* ── Explorateur de journal ─────────────────────────────────────────────── */
2835
+ .counts { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-bottom:6px; }
2836
+ .counts .spacer { flex:1 1 auto; }
2837
+ .counts button { font: inherit; color: inherit; background:none; border:1px solid transparent; cursor:pointer; }
2838
+ .chip.sevf { border-color: var(--line); }
2839
+ .chip.sevf.on { border-color: var(--ok); color:#fff; }
2840
+ .search { font: inherit; font-size:11px; color:inherit; background:rgba(255,255,255,.04);
2841
+ border:1px solid var(--line); border-radius:6px; padding:3px 8px; min-width:120px; max-width:220px; }
2842
+ .search::placeholder { color: var(--muted); }
2843
+ .toolbtn { font: inherit; font-size:12px; line-height:1; color:var(--muted); background:none;
2844
+ border:1px solid var(--line); border-radius:6px; padding:3px 7px; cursor:pointer; }
2845
+ .toolbtn:hover { color:#fff; }
2846
+ .toolbtn.on { color:var(--ok); border-color:var(--ok); }
2847
+ .log { display:flex; align-items:baseline; gap:8px; padding:2px 0; cursor:pointer; }
2848
+ .log:hover { background:rgba(255,255,255,.04); }
2849
+ .log .ts { color:var(--muted); font-variant-numeric: tabular-nums; flex:none; }
2850
+ .log .rid { color:var(--muted); font-family:monospace; flex:none; margin-left:auto;
2851
+ border:1px solid var(--line); border-radius:4px; padding:0 4px; }
2852
+ .logdetail { margin:2px 0 6px 0; padding:6px 8px; border-left:2px solid var(--line);
2853
+ background:rgba(255,255,255,.03); border-radius:0 6px 6px 0; }
2854
+ .drow { display:flex; gap:8px; padding:1px 0; }
2855
+ .drow .dk { color:var(--muted); min-width:92px; flex:none; }
2856
+ .drow .dv { word-break:break-word; }
2857
+ .feed .empty { color:var(--muted); padding:6px 0; }
2858
+ .log { display:flex; gap:7px; padding:2px 0; align-items:baseline; border-bottom:1px solid rgba(255,255,255,.04); }
2859
+ .log .sev { flex:none; width:54px; font-size:9px; font-weight:800; text-transform:uppercase; }
2860
+ .log .mod { flex:none; color:var(--muted); max-width:90px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
2861
+ .log .txt { flex:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
2862
+
2863
+ .card.fe { border-color: rgba(255,138,61,.3);
2864
+ background: linear-gradient(160deg, rgba(255,138,61,.12), rgba(28,31,38,.7) 60%); }
2865
+ .fw { display:flex; align-items:center; gap:9px; margin-bottom:9px; }
2866
+ .fw .badge { padding:2px 10px; border-radius:7px; font-weight:800; font-size:11px; color:#0b0d10; }
2867
+ .fw .name { color:var(--muted); }
2868
+ .fe .hd { display:flex; justify-content:space-between; align-items:baseline; margin-bottom:2px; }
2869
+ .fe .lbl { color:var(--muted); font-size:10px; text-transform:uppercase; letter-spacing:.5px; }
2870
+ .hmr-big { font-size:26px; font-weight:800; line-height:1; transition: color .2s; }
2871
+ .hmr-big.hmr-flash { color: var(--orange); text-shadow: 0 0 14px rgba(255,138,61,.7); }
2872
+ .fe svg { width:100%; height:34px; display:block; margin:6px 0 8px; }
2873
+ .fe polyline { fill:none; stroke-width:1.75; vector-effect:non-scaling-stroke; }
2874
+ .spark.fe { stroke:var(--orange); } .area.fe { fill:rgba(255,138,61,.16); }
2875
+
2876
+ .env-badge { padding:.2em .7em; border-radius:6px; font-size:.76em; font-weight:800;
2877
+ letter-spacing:.6px; text-transform:uppercase; color:#0b0d10; background:var(--muted); flex:none; }
2878
+ .env-badge.dev { background: var(--ok); } .env-badge.prod { background: var(--crit); color:#fff; }
2879
+ .env-badge.test { background: var(--warn); } .env-badge.staging { background:#a06bff; color:#fff; }
2880
+ .branch { display:flex; align-items:center; gap:5px; padding:2px 9px; border-radius:6px; flex:none;
2881
+ background:#22262e; font-weight:700; max-width:200px; cursor:help; }
2882
+ .branch .git { color:var(--blue2); } .branch span:last-child { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
2883
+ /* Les contrôles et les indicateurs du bandeau sont de VRAIS <button> : ils
2884
+ s'atteignent au clavier et s'annoncent. Ce bloc leur retire l'apparence
2885
+ native que le navigateur leur donne, sans leur retirer leur nature. */
2886
+ .strip button { font: inherit; color: inherit; background: none; border: 0; margin: 0; padding: 0; cursor: pointer; }
2887
+ .strip button:focus-visible, .tab:focus-visible, .toolbtn:focus-visible {
2888
+ outline: 2px solid var(--ok); outline-offset: 2px; border-radius: 4px; }
2889
+ .goto:hover .v, .goto:hover .k { color: #fff; }
2890
+
2891
+ /* ── Infobulles ────────────────────────────────────────────────────────────
2892
+ Maison, et non l'attribut « title » du navigateur : celui-là met environ une
2893
+ seconde à venir, ne se style pas, et surtout n'apparaît JAMAIS au focus
2894
+ clavier — l'aide restait donc inaccessible à qui n'utilise pas la souris.
2895
+ Celle-ci s'ouvre au survol ET au focus, ce qui la rend utilisable par tout le
2896
+ monde. En CSS pur : aucun script, aucun écouteur, rien à libérer. */
2897
+ [data-tip] { position: relative; }
2898
+ [data-tip]::after {
2899
+ content: attr(data-tip);
2900
+ position: absolute; bottom: calc(100% + 9px); left: 50%; transform: translateX(-50%);
2901
+ width: max-content; max-width: 280px; white-space: normal; text-align: left;
2902
+ background: #0b0b0d; color: #f2f2f2; border: 1px solid var(--line);
2903
+ border-radius: 6px; padding: 6px 9px; font-size: 11px; line-height: 1.45;
2904
+ font-weight: 400; text-transform: none; letter-spacing: 0;
2905
+ box-shadow: 0 6px 20px rgba(0,0,0,.5);
2906
+ opacity: 0; visibility: hidden; transition: opacity .12s ease; pointer-events: none; z-index: 20;
2907
+ }
2908
+ [data-tip]:hover::after, [data-tip]:focus-visible::after { opacity: 1; visibility: visible; }
2909
+ /* Barre ancrée en haut : l'infobulle bascule dessous, sinon elle sort de l'écran. */
2910
+ .bar.top [data-tip]::after { bottom: auto; top: calc(100% + 9px); }
2911
+ /* Les bords : l'infobulle se recale pour ne pas déborder de la fenêtre. */
2912
+ .strip > :first-child[data-tip]::after { left: 0; transform: none; }
2913
+ .strip > :last-child[data-tip]::after { left: auto; right: 0; transform: none; }
2914
+ @media (prefers-reduced-motion: reduce) { [data-tip]::after { transition: none; } }
2915
+
2916
+ .ctrl { color:var(--muted); cursor:pointer; padding:0 4px; font-weight:800; font-size:1.05em; line-height:1; flex:none; }
2917
+ .ctrl:hover { color:#fff; }
2918
+ .ctrl.live { font-size:.78em; font-weight:700; border:1px solid var(--line); border-radius:6px; padding:2px 7px; letter-spacing:.3px; }
2919
+ .ctrl.live.on { color:var(--ok); border-color:var(--ok); }
2920
+ .ctrl.live:not(.on) { color:var(--muted); }
2921
+
2922
+ .minbar { position:fixed; z-index:2147483000; display:none; align-items:center; gap:8px;
2923
+ padding:6px 13px; border-radius:22px; cursor:pointer;
2924
+ font:12px/1 ui-monospace, SFMono-Regular, Menlo, monospace; color:#e8eaed;
2925
+ background:rgba(18,20,25,.92); backdrop-filter:blur(12px);
2926
+ border:1px solid rgba(58,160,255,.45); box-shadow:0 4px 20px rgba(0,0,0,.5); }
2927
+ .minbar:hover { border-color:#3aa0ff; box-shadow:0 4px 26px rgba(58,160,255,.4); }
2928
+ .minbar.bottom { bottom:14px; } .minbar.top { top:14px; }
2929
+ .minbar.dock-left { left:14px; } .minbar.dock-right { right:14px; }
2930
+ .minbar .dot { width:8px; height:8px; border-radius:50%; background:#36b37e; }
2931
+ .minbar .dot.connected { background:#36b37e; } .minbar .dot.disconnected,.minbar .dot.error { background:#ff5630; }
2932
+ .minbar .dot.connecting,.minbar .dot.reconnecting { background:#ffab00; }
2933
+ .minbar .mlogo { color:#ff8a3d; } .minbar .mrate { font-weight:800; }
2934
+ .minbar .mbadge { font-size:9px; font-weight:800; text-transform:uppercase; color:#8a9099; }
2935
+
2936
+ /* ── Network ─────────────────────────────────────────────────────────────── */
2937
+ .net-head { display:flex; align-items:center; gap:8px; margin-bottom:8px; flex-wrap:wrap; }
2938
+ .net-clear { margin-left:auto; color:var(--muted); cursor:pointer; font-size:10px;
2939
+ text-transform:uppercase; letter-spacing:.5px; padding:2px 6px; border-radius:6px; }
2940
+ .net-clear:hover { color:#fff; background:rgba(255,255,255,.06); }
2941
+ .net-list { border-top:1px solid var(--line); }
2942
+ .net-list .empty { color:var(--muted); padding:8px 0; }
2943
+ .net-row { display:flex; align-items:center; gap:9px; padding:3px 4px; cursor:pointer;
2944
+ border-bottom:1px solid rgba(255,255,255,.04); }
2945
+ .net-row:hover { background:rgba(255,255,255,.04); }
2946
+ .net-row.sel { background:rgba(58,160,255,.14); }
2947
+ .net-row.err .net-path { color:#ffb4a6; }
2948
+ .net-method { flex:none; width:46px; text-align:center; font-weight:800; font-size:9px;
2949
+ padding:1px 0; border-radius:5px; text-transform:uppercase; color:#0b0d10; background:var(--muted); }
2950
+ .net-method.get { background:var(--blue2); } .net-method.post { background:var(--ok); }
2951
+ .net-method.put,.net-method.patch { background:var(--orange); }
2952
+ .net-method.delete { background:var(--crit); color:#fff; }
2953
+ .net-method.ws { background:#a06bff; color:#fff; }
2954
+ .net-path { flex:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
2955
+ .net-status { flex:none; width:38px; text-align:center; font-weight:800; font-size:10px; }
2956
+ .net-status.s2 { color:var(--ok); } .net-status.s3 { color:var(--info); }
2957
+ .net-status.s4 { color:var(--warn); } .net-status.s5 { color:var(--crit); }
2958
+ .net-status.sp { color:var(--muted); }
2959
+ .net-dur { flex:none; width:58px; text-align:right; color:var(--muted); font-size:10px; }
2960
+ .net-rid { flex:none; color:var(--blue2); font-size:9px; opacity:.7; }
2961
+
2962
+ .net-detail { margin-top:10px; border-top:1px solid var(--line); padding-top:10px; }
2963
+ .net-detail .empty { color:var(--muted); }
2964
+ .det-grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(160px,1fr)); gap:2px 14px; margin-bottom:10px; }
2965
+ .wf { display:flex; flex-direction:column; gap:3px; }
2966
+ .wf-title { color:var(--muted); font-size:10px; text-transform:uppercase; letter-spacing:.5px; margin-bottom:4px; }
2967
+ .wf-row { display:flex; align-items:center; gap:8px; }
2968
+ .wf-name { flex:none; width:74px; text-align:right; color:#cfd3d8; font-size:10px; }
2969
+ .wf-track { flex:1; position:relative; height:14px; background:rgba(255,255,255,.04); border-radius:3px; }
2970
+ .wf-bar { position:absolute; top:0; bottom:0; border-radius:3px; min-width:2px;
2971
+ box-shadow: inset 0 0 0 1px rgba(255,255,255,.12); }
2972
+ .wf-ms { flex:none; width:62px; text-align:right; color:var(--muted); font-size:10px; }
2973
+ .wf-bar.parse { background:#4c9aff; } .wf-bar.resolve { background:#3aa0ff; }
2974
+ .wf-bar.firewall { background:#ff8a3d; } .wf-bar.init { background:#a06bff; }
2975
+ .wf-bar.action { background:#36b37e; } .wf-bar.render { background:#ffab00; }
2976
+ .wf-bar.send { background:#00b8d9; } .wf-bar.other { background:#8a9099; }
2977
+ .wf-bar.identity { background:#ff5c8a; }
2978
+ .det-loading,.det-err { color:var(--muted); padding:6px 0; } .det-err { color:var(--crit); }
2979
+
2980
+ /* Network pane = split DevTools : liste (scroll) + détail (scroll), détail
2981
+ TOUJOURS visible (≠ tout dans un seul scroll où le détail finit hors écran). */
2982
+ .pane.np.active { display:flex; flex-direction:column; padding:0; }
2983
+ .np .net-head { padding:10px 14px 8px; flex:none; margin:0; }
2984
+ .np .net-list { flex:1 1 auto; min-height:56px; overflow:auto; padding:0 14px; border-top:1px solid var(--line); }
2985
+ /* Détail : placeholder = petit (flex:0). Sélection active (.np.sel) → le détail
2986
+ prend une vraie part (60%) avec son propre scroll → le waterfall est visible,
2987
+ la liste se réduit. */
2988
+ .np .net-detail { flex:0 1 auto; max-height:50%; overflow:auto; padding:8px 14px 12px; margin-top:0; border-top:1px solid var(--line); }
2989
+ .np.sel .net-list { flex:1 1 40%; min-height:48px; }
2990
+ .np.sel .net-detail { flex:1 1 60%; max-height:none; }
2991
+ .det-bar { display:flex; justify-content:flex-end; margin-bottom:2px; }
2992
+ .det-close { cursor:pointer; color:var(--muted); font-weight:800; font-size:15px; line-height:1; padding:0 4px; }
2993
+ .det-close:hover { color:#fff; }
2994
+ `;
2995
+ function pushCap(arr, v, cap) {
2996
+ arr.push(v);
2997
+ if (arr.length > cap) arr.shift();
2998
+ }
2999
+ function clampH(h) {
3000
+ const max = typeof window !== "undefined" ? window.innerHeight * .85 : 700;
3001
+ if (h < PANEL_H_MIN) return PANEL_H_MIN;
3002
+ if (h > max) return Math.round(max);
3003
+ return Math.round(h);
3004
+ }
3005
+ /**
3006
+ * Version du format de l'état persisté.
3007
+ *
3008
+ * Elle existe pour une raison précise : ces clés survivent aux mises à jour du
3009
+ * framework, dans un navigateur que personne ne nettoie. Le jour où l'une change
3010
+ * de sens, une valeur écrite par une version d'avant est lue comme si elle
3011
+ * disait la même chose — et la barre s'ouvre dans un état incohérent que
3012
+ * l'utilisateur ne sait pas défaire. Au changement de version, tout est purgé :
3013
+ * on perd une préférence d'affichage, ce qui ne coûte rien, plutôt que de
3014
+ * charger un état dont on ne sait rien.
3015
+ */
3016
+ const LS_VERSION = "1";
3017
+ /** Clés de persistance localStorage (état chrome de la barre). */
3018
+ const LS = {
3019
+ /** Version du format — jamais purgée, c'est elle qui décide de la purge. */
3020
+ version: "nf.debugbar.v",
3021
+ visible: "nf.debugbar.visible",
3022
+ min: "nf.debugbar.min",
3023
+ side: "nf.debugbar.side",
3024
+ tab: "nf.debugbar.tab",
3025
+ h: "nf.debugbar.h",
3026
+ live: "nf.debugbar.live"
3027
+ };
3028
+ /**
3029
+ * Toutes les clés que la barre écrit sur CE navigateur.
3030
+ *
3031
+ * Cette liste n'est pas décorative : elle est ce que la barre AFFICHE dans son
3032
+ * onglet Runtime et ce que sa purge efface. Trois choses qui doivent rester
3033
+ * d'accord, donc une seule source.
3034
+ */
3035
+ function lsKeys() {
3036
+ return Object.values(LS);
3037
+ }
3038
+ /** Efface l'état persisté de la barre — et rien d'autre du stockage de l'app. */
3039
+ function lsPurge() {
3040
+ try {
3041
+ for (const k of lsKeys()) localStorage.removeItem(k);
3042
+ } catch {}
3043
+ }
3044
+ /**
3045
+ * Purge l'état si le format a changé depuis la dernière visite.
3046
+ *
3047
+ * Appelée avant toute lecture — sinon on lirait justement les valeurs qu'on
3048
+ * s'apprête à jeter.
3049
+ */
3050
+ function lsMigrate() {
3051
+ try {
3052
+ if (localStorage.getItem(LS.version) === LS_VERSION) return;
3053
+ lsPurge();
3054
+ localStorage.setItem(LS.version, LS_VERSION);
3055
+ } catch {}
3056
+ }
3057
+ function lsGet(key, def) {
3058
+ try {
3059
+ return localStorage.getItem(key) ?? def;
3060
+ } catch {
3061
+ return def;
3062
+ }
3063
+ }
3064
+ function lsSet(key, val) {
3065
+ try {
3066
+ localStorage.setItem(key, val);
3067
+ } catch {}
3068
+ }
3069
+ const GLOBAL_KEY = "__NODEFONY_DEBUGBAR__";
3070
+ /**
3071
+ * Widget DOM. Lazy : ne construit le Shadow DOM qu'au `mount()`. Idempotent
3072
+ * (un seul `#nodefony-debugbar` par page).
3073
+ */
3074
+ var DebugBar = class DebugBar {
3075
+ client;
3076
+ model = new DebugBarModel();
3077
+ url;
3078
+ position;
3079
+ startOpen;
3080
+ ownClient;
3081
+ frontend;
3082
+ host = null;
3083
+ bar = null;
3084
+ visible;
3085
+ minimized;
3086
+ side;
3087
+ activeTab;
3088
+ panelH;
3089
+ live;
3090
+ /** dispose des abonnements realtime (null quand OFF). */
3091
+ liveOff = null;
3092
+ rafPending = false;
3093
+ feedLen = -1;
3094
+ /** Filtre de sévérité de l'onglet Logs — `all` par défaut. */
3095
+ feedSev = "all";
3096
+ /** Recherche courante, déjà en minuscules (comparée telle quelle). */
3097
+ feedQuery = "";
3098
+ /**
3099
+ * Affichage suspendu. Les entrées continuent d'arriver dans le modèle : à la
3100
+ * reprise, la liste se reconstruit complète — mettre en pause ne fait pas
3101
+ * perdre ce qui s'est passé pendant, ce qui serait le pire des deux mondes.
3102
+ */
3103
+ feedPaused = false;
3104
+ /** La liste affichée doit être refaite (changement de filtre ou de recherche). */
3105
+ feedDirty = false;
3106
+ prevFrames = 0;
3107
+ rtRate = 0;
3108
+ rtPeak = 0;
3109
+ rtSeries = [];
3110
+ viteConnected = false;
3111
+ hmrCount = 0;
3112
+ hmrPrev = 0;
3113
+ hmrRate = 0;
3114
+ hmrLast = "—";
3115
+ hmrSeries = [];
3116
+ flashTimer = null;
3117
+ disposers = [];
3118
+ el = Object.create(null);
3119
+ networkEnabled;
3120
+ profilerBase;
3121
+ net = new NetworkModel();
3122
+ /** Observe la barre → publie sa hauteur (var CSS) pour l'app hôte. */
3123
+ ro = null;
3124
+ netRows = /* @__PURE__ */ new Map();
3125
+ selectedRid = null;
3126
+ selRowId = null;
3127
+ selNoRid = false;
3128
+ detailVersion = 0;
3129
+ detailRendered = -1;
3130
+ constructor(opts = {}) {
3131
+ this.url = opts.url ?? DEFAULT_PATH;
3132
+ this.position = opts.position ?? "bottom";
3133
+ this.startOpen = opts.open ?? false;
3134
+ this.ownClient = false;
3135
+ this.frontend = opts.frontend ?? null;
3136
+ this.networkEnabled = opts.network !== false;
3137
+ this.profilerBase = (opts.profilerBase ?? DEFAULT_PROFILER_BASE).replace(/\/$/, "");
3138
+ this.client = opts.client ?? RealtimeClient.shared({ url: this.url });
3139
+ lsMigrate();
3140
+ this.visible = lsGet(LS.visible, "1") !== "0";
3141
+ this.minimized = lsGet(LS.min, "0") === "1";
3142
+ this.side = lsGet(LS.side, "right") === "left" ? "left" : "right";
3143
+ this.activeTab = lsGet(LS.tab, "realtime") || "realtime";
3144
+ this.panelH = clampH(parseInt(lsGet(LS.h, String(defaultPanelH())), 10) || defaultPanelH());
3145
+ this.live = lsGet(LS.live, "0") === "1";
3146
+ }
3147
+ /** Construit le DOM, branche le realtime et ouvre la connexion. No-op si déjà monté. */
3148
+ mount() {
3149
+ if (typeof document === "undefined") return this;
3150
+ if (document.getElementById(HOST_ID)) return this;
3151
+ this.buildDom();
3152
+ this.wireRealtime();
3153
+ this.wireHmr();
3154
+ this.wireNetwork();
3155
+ this.applyChrome();
3156
+ this.registerHandle();
3157
+ this.client.connect().catch(() => {});
3158
+ this.render();
3159
+ return this;
3160
+ }
3161
+ /** Détache listeners, ferme la connexion (si propriétaire) et retire le DOM. */
3162
+ unmount() {
3163
+ for (const d of this.disposers) d();
3164
+ this.disposers.length = 0;
3165
+ if (this.flashTimer) clearTimeout(this.flashTimer);
3166
+ this.flashTimer = null;
3167
+ if (this.ownClient) this.client.disconnect();
3168
+ this.unregisterHandle();
3169
+ this.ro?.disconnect();
3170
+ this.ro = null;
3171
+ try {
3172
+ document.documentElement.style.removeProperty("--nodefony-debugbar-height");
3173
+ } catch {}
3174
+ this.host?.remove();
3175
+ this.host = null;
3176
+ this.bar = null;
3177
+ }
3178
+ /** Affiche/masque la barre (pilotable depuis une app via le handle global). */
3179
+ setVisible(v) {
3180
+ this.visible = v;
3181
+ lsSet(LS.visible, v ? "1" : "0");
3182
+ this.applyChrome();
3183
+ }
3184
+ setMinimized(v) {
3185
+ this.minimized = v;
3186
+ lsSet(LS.min, v ? "1" : "0");
3187
+ this.applyChrome();
3188
+ }
3189
+ toggleSide() {
3190
+ this.side = this.side === "left" ? "right" : "left";
3191
+ lsSet(LS.side, this.side);
3192
+ this.applyChrome();
3193
+ }
3194
+ /**
3195
+ * Déplie ou replie le panneau — le SEUL endroit qui bascule cet état.
3196
+ *
3197
+ * Il porte aussi ce qu'un lecteur d'écran doit entendre : `aria-expanded` sur
3198
+ * le bouton qui commande, et un nom qui dit l'action à venir (« Déplier » /
3199
+ * « Replier »), pas l'état courant — un nom qui décrit l'état laisse
3200
+ * l'utilisateur deviner ce que le bouton va faire.
3201
+ */
3202
+ setOpen(open) {
3203
+ const bar = this.bar;
3204
+ if (!bar) return;
3205
+ bar.classList.toggle("open", open);
3206
+ const btn = this.el.btnToggle;
3207
+ if (btn) {
3208
+ btn.setAttribute("aria-expanded", open ? "true" : "false");
3209
+ const label = open ? "Replier le panneau" : "Déplier le panneau";
3210
+ btn.setAttribute("aria-label", label);
3211
+ btn.setAttribute("data-tip", label);
3212
+ }
3213
+ this.render();
3214
+ }
3215
+ setTab(tab) {
3216
+ this.activeTab = tab;
3217
+ lsSet(LS.tab, tab);
3218
+ this.applyTab();
3219
+ this.render();
3220
+ }
3221
+ setPanelH(h) {
3222
+ this.panelH = clampH(h);
3223
+ const panes = this.el.panes;
3224
+ if (panes) panes.style.height = `${this.panelH}px`;
3225
+ }
3226
+ /** Reflète l'onglet actif sur les boutons + panes. */
3227
+ applyTab() {
3228
+ (this.bar?.querySelectorAll(".tab"))?.forEach((t) => t.classList.toggle("active", t.getAttribute("data-tab") === this.activeTab));
3229
+ (this.bar?.querySelectorAll(".pane"))?.forEach((p) => p.classList.toggle("active", p.getAttribute("data-pane") === this.activeTab));
3230
+ }
3231
+ /** Applique l'état chrome au DOM (display + classes de dock + hauteur). */
3232
+ applyChrome() {
3233
+ if (!this.host || !this.bar) return;
3234
+ this.host.style.display = this.visible ? "" : "none";
3235
+ this.bar.style.display = this.minimized ? "none" : "";
3236
+ const min = this.el.minbar;
3237
+ if (min) {
3238
+ min.style.display = this.minimized ? "flex" : "none";
3239
+ min.setAttribute("class", `minbar ${this.position} dock-${this.side}`);
3240
+ }
3241
+ this.setPanelH(this.panelH);
3242
+ this.applyTab();
3243
+ this.publishHeight();
3244
+ }
3245
+ /**
3246
+ * Publie la hauteur occupée par la barre (dock bas, visible, dépliée) en
3247
+ * variable CSS `--nodefony-debugbar-height` sur `:root`. L'app hôte (Studio)
3248
+ * la réserve en `padding-bottom` → le contenu n'est jamais masqué. `0px` si
3249
+ * masquée / réduite (chip flottante) / dockée en haut.
3250
+ */
3251
+ publishHeight() {
3252
+ if (typeof document === "undefined") return;
3253
+ const h = this.visible && !this.minimized && this.position === "bottom" && this.bar ? this.bar.offsetHeight : 0;
3254
+ document.documentElement.style.setProperty("--nodefony-debugbar-height", `${h}px`);
3255
+ }
3256
+ registerHandle() {
3257
+ globalThis[GLOBAL_KEY] = {
3258
+ isVisible: () => this.visible,
3259
+ setVisible: (v) => this.setVisible(v),
3260
+ toggle: () => this.setVisible(!this.visible),
3261
+ minimize: () => this.setMinimized(true),
3262
+ restore: () => this.setMinimized(false)
3263
+ };
3264
+ }
3265
+ unregisterHandle() {
3266
+ try {
3267
+ delete globalThis[GLOBAL_KEY];
3268
+ } catch {}
3269
+ }
3270
+ buildDom() {
3271
+ const host = document.createElement("div");
3272
+ host.id = HOST_ID;
3273
+ host.setAttribute("role", "complementary");
3274
+ host.setAttribute("aria-label", "Barre de débogage Nodefony");
3275
+ const shadow = host.attachShadow({ mode: "open" });
3276
+ const style = document.createElement("style");
3277
+ style.textContent = STYLES;
3278
+ const bar = document.createElement("div");
3279
+ bar.className = `bar ${this.position}${this.startOpen ? " open" : ""}`;
3280
+ bar.innerHTML = this.template();
3281
+ const minbar = document.createElement("div");
3282
+ minbar.className = `minbar ${this.position} dock-${this.side}`;
3283
+ minbar.innerHTML = `<span class="dot" data-el="mdot"></span><span class="mlogo">⚡</span><span class="mrate" data-el="mrate">0/s</span><span class="mbadge" data-el="mEnv"></span>`;
3284
+ shadow.append(style, bar, minbar);
3285
+ document.body.appendChild(host);
3286
+ this.host = host;
3287
+ this.bar = bar;
3288
+ this.el.minbar = minbar;
3289
+ if (typeof ResizeObserver !== "undefined") {
3290
+ this.ro = new ResizeObserver(() => this.publishHeight());
3291
+ this.ro.observe(bar);
3292
+ }
3293
+ shadow.querySelectorAll("[data-el]").forEach((node) => {
3294
+ const key = node.getAttribute("data-el");
3295
+ if (key) this.el[key] = node;
3296
+ });
3297
+ const strip = bar.querySelector(".strip");
3298
+ this.wireBtn("btnToggle", () => this.setOpen(!bar.classList.contains("open")));
3299
+ const onGoto = (ev) => {
3300
+ const id = (ev.target?.closest?.(".goto"))?.getAttribute("data-goto");
3301
+ if (!id) return;
3302
+ this.setOpen(true);
3303
+ this.setTab(id);
3304
+ };
3305
+ strip.addEventListener("click", onGoto);
3306
+ this.disposers.push(() => strip.removeEventListener("click", onGoto));
3307
+ this.wireBtn("btnMin", () => this.setMinimized(true));
3308
+ this.wireBtn("btnSide", () => this.toggleSide());
3309
+ this.wireBtn("btnLive", () => this.setLive(!this.live));
3310
+ this.updateLiveBtn();
3311
+ const tabsBar = bar.querySelector(".tabs");
3312
+ if (tabsBar) {
3313
+ const onTab = (ev) => {
3314
+ const id = (ev.target?.closest?.(".tab"))?.getAttribute("data-tab");
3315
+ if (id) this.setTab(id);
3316
+ };
3317
+ tabsBar.addEventListener("click", onTab);
3318
+ this.disposers.push(() => tabsBar.removeEventListener("click", onTab));
3319
+ }
3320
+ const resize = this.el.resize;
3321
+ if (resize) {
3322
+ const onDown = (e) => this.startResize(e);
3323
+ resize.addEventListener("pointerdown", onDown);
3324
+ this.disposers.push(() => resize.removeEventListener("pointerdown", onDown));
3325
+ }
3326
+ const counts = bar.querySelector(".counts");
3327
+ if (counts) {
3328
+ const onSev = (ev) => {
3329
+ const sev = (ev.target?.closest?.(".sevf"))?.getAttribute("data-sev");
3330
+ if (sev !== "all" && sev !== "err" && sev !== "warn") return;
3331
+ this.feedSev = this.feedSev === sev ? "all" : sev;
3332
+ counts.querySelectorAll(".sevf").forEach((n) => n.classList.toggle("on", n.getAttribute("data-sev") === this.feedSev && this.feedSev !== "all"));
3333
+ this.feedDirty = true;
3334
+ this.render();
3335
+ };
3336
+ counts.addEventListener("click", onSev);
3337
+ this.disposers.push(() => counts.removeEventListener("click", onSev));
3338
+ }
3339
+ const search = this.el.logSearch;
3340
+ if (search) {
3341
+ const onSearch = () => {
3342
+ this.feedQuery = search.value.trim().toLowerCase();
3343
+ this.feedDirty = true;
3344
+ this.render();
3345
+ };
3346
+ search.addEventListener("input", onSearch);
3347
+ this.disposers.push(() => search.removeEventListener("input", onSearch));
3348
+ }
3349
+ this.wireBtn("btnPause", () => {
3350
+ this.feedPaused = !this.feedPaused;
3351
+ const b = this.el.btnPause;
3352
+ if (b) {
3353
+ b.setAttribute("aria-pressed", this.feedPaused ? "true" : "false");
3354
+ b.classList.toggle("on", this.feedPaused);
3355
+ b.textContent = this.feedPaused ? "▶" : "⏸";
3356
+ const tip = this.feedPaused ? "Reprendre l'affichage — la liste se recompose avec ce qui est arrivé pendant la pause" : "Suspendre l'affichage — les entrées continuent d'arriver et se rattrapent à la reprise";
3357
+ b.setAttribute("data-tip", tip);
3358
+ }
3359
+ if (!this.feedPaused) {
3360
+ this.feedDirty = true;
3361
+ this.render();
3362
+ }
3363
+ });
3364
+ this.wireBtn("btnCopy", () => {
3365
+ const node = this.el.feed;
3366
+ const text = node ? node.textContent ?? "" : "";
3367
+ navigator.clipboard?.writeText(text).then(() => this.flashBtn("btnCopy", "✓"), () => this.flashBtn("btnCopy", "✗"));
3368
+ });
3369
+ this.wireBtn("btnClearFeed", () => {
3370
+ const node = this.el.feed;
3371
+ if (node) node.innerHTML = `<div class="empty">liste vidée — les nouvelles entrées s'afficheront ici.</div>`;
3372
+ this.feedLen = this.model.view.feed.length;
3373
+ });
3374
+ const feed = this.el.feed;
3375
+ if (feed) {
3376
+ const onFeed = (ev) => {
3377
+ const row = ev.target?.closest?.(".log");
3378
+ if (!row) return;
3379
+ if (row.nextElementSibling?.classList.contains("logdetail")) {
3380
+ row.nextElementSibling?.remove();
3381
+ row.setAttribute("aria-expanded", "false");
3382
+ return;
3383
+ }
3384
+ const uid = Number(row.getAttribute("data-uid"));
3385
+ const entry = this.model.view.feed.find((l) => l.uid === uid);
3386
+ if (!entry) return;
3387
+ feed.querySelectorAll(".logdetail").forEach((n) => n.remove());
3388
+ feed.querySelectorAll(".log[aria-expanded='true']").forEach((n) => n.setAttribute("aria-expanded", "false"));
3389
+ row.insertAdjacentHTML("afterend", this.feedDetail(entry));
3390
+ row.setAttribute("aria-expanded", "true");
3391
+ };
3392
+ const onFeedKey = (ev) => {
3393
+ const k = ev.key;
3394
+ if (k !== "Enter" && k !== " ") return;
3395
+ const row = ev.target?.closest?.(".log");
3396
+ if (!row) return;
3397
+ ev.preventDefault();
3398
+ row.click();
3399
+ };
3400
+ feed.addEventListener("click", onFeed);
3401
+ feed.addEventListener("keydown", onFeedKey);
3402
+ this.disposers.push(() => {
3403
+ feed.removeEventListener("click", onFeed);
3404
+ feed.removeEventListener("keydown", onFeedKey);
3405
+ });
3406
+ }
3407
+ this.wireBtn("btnPurge", () => {
3408
+ lsPurge();
3409
+ this.flashBtn("btnPurge", "état effacé ✓");
3410
+ });
3411
+ const onMin = () => this.setMinimized(false);
3412
+ minbar.addEventListener("click", onMin);
3413
+ this.disposers.push(() => minbar.removeEventListener("click", onMin));
3414
+ let resizePending = false;
3415
+ const onWinResize = () => {
3416
+ if (resizePending) return;
3417
+ resizePending = true;
3418
+ requestAnimationFrame(() => {
3419
+ resizePending = false;
3420
+ this.setPanelH(this.panelH);
3421
+ });
3422
+ };
3423
+ window.addEventListener("resize", onWinResize);
3424
+ this.disposers.push(() => window.removeEventListener("resize", onWinResize));
3425
+ }
3426
+ startResize(ev) {
3427
+ ev.preventDefault();
3428
+ const startY = ev.clientY;
3429
+ const startH = this.panelH;
3430
+ const onMove = (e) => {
3431
+ const delta = this.position === "bottom" ? startY - e.clientY : e.clientY - startY;
3432
+ this.setPanelH(startH + delta);
3433
+ };
3434
+ const onUp = () => {
3435
+ document.removeEventListener("pointermove", onMove);
3436
+ document.removeEventListener("pointerup", onUp);
3437
+ lsSet(LS.h, String(this.panelH));
3438
+ };
3439
+ document.addEventListener("pointermove", onMove);
3440
+ document.addEventListener("pointerup", onUp);
3441
+ }
3442
+ /**
3443
+ * Retour visuel bref sur un bouton d'action.
3444
+ *
3445
+ * Une copie réussie ne change rien à l'écran : sans ce clin d'œil, on ne sait
3446
+ * pas si le clic a porté, et on reclique.
3447
+ */
3448
+ flashBtn(key, glyph) {
3449
+ const b = this.el[key];
3450
+ if (!b) return;
3451
+ const before = b.textContent ?? "";
3452
+ b.textContent = glyph;
3453
+ const t = setTimeout(() => {
3454
+ b.textContent = before;
3455
+ }, 900);
3456
+ this.disposers.push(() => clearTimeout(t));
3457
+ }
3458
+ wireBtn(key, handler) {
3459
+ const node = this.el[key];
3460
+ if (!node) return;
3461
+ node.addEventListener("click", handler);
3462
+ this.disposers.push(() => node.removeEventListener("click", handler));
3463
+ }
3464
+ template() {
3465
+ return `
3466
+ <div class="strip">
3467
+ <span class="dot" data-el="dot" data-tip="État de la connexion temps réel"></span>
3468
+ <span class="brand" data-tip="Barre de débogage Nodefony — visible en développement uniquement"><span class="logo">◆</span><span class="name">nodefony</span></span>
3469
+ <span class="env-badge" data-el="envBadge" data-tip="Environnement dans lequel tourne l'application">env</span>
3470
+ <span class="branch" data-el="branch" data-tip="Branche git de la copie de travail"><span class="git">⎇</span><span data-el="branchName">—</span></span>
3471
+ <span class="rt-pill" data-el="rtPill" data-tip="Transport temps réel et état de la socket"><span class="bolt">⚡</span> realtime</span>
3472
+ ${this.miniMetric("rt", "rt", "rtMini", "0/s", "realtime", "Messages temps réel reçus par seconde — cliquer pour ouvrir l'onglet Realtime")}
3473
+ ${this.miniMetric("cpu", "cpu", "cpuMini", "0%", "perf", "Charge processeur du serveur — cliquer pour ouvrir l'onglet Perf")}
3474
+ ${this.miniMetric("mem", "mem", "memMini", "0%", "perf", "Mémoire utilisée par le serveur — cliquer pour ouvrir l'onglet Perf")}
3475
+ <span class="spacer"></span>
3476
+ ${this.networkEnabled ? `<button type="button" class="chip goto" data-goto="network" data-tip="Requêtes réseau observées — cliquer pour ouvrir l'onglet Network"><span class="k">net</span><span class="blue" data-el="netChip">0</span></button>` : ""}
3477
+ <button type="button" class="chip goto" data-goto="logs" data-tip="Entrées de journal reçues — cliquer pour ouvrir l'onglet Logs"><span class="k">logs</span><span data-el="logs">0</span></button>
3478
+ <button type="button" class="chip goto" data-goto="logs" data-tip="Erreurs et alertes — cliquer pour ouvrir l'onglet Logs"><span class="k">err</span><span class="crit" data-el="err">0</span></button>
3479
+ <button type="button" class="ctrl live" data-el="btnLive" aria-pressed="false" data-tip="Recevoir les mesures et les journaux en direct. Coupé par défaut : le flux maintient des compteurs côté serveur.">flux OFF</button>
3480
+ <button type="button" class="ctrl" data-el="btnSide" aria-label="Changer la barre de côté" data-tip="Changer la barre de côté">⇄</button>
3481
+ <button type="button" class="ctrl" data-el="btnMin" aria-label="Réduire la barre en pastille" data-tip="Réduire en pastille">—</button>
3482
+ <button type="button" class="toggle" data-el="btnToggle" aria-expanded="${this.startOpen}" aria-controls="nf-db-panel" aria-label="${this.startOpen ? "Replier" : "Déplier"} le panneau" data-tip="${this.startOpen ? "Replier" : "Déplier"} le panneau">▴</button>
3483
+ </div>
3484
+ <div class="panelwrap" id="nf-db-panel">
3485
+ <div class="resize" data-el="resize" title="Redimensionner"></div>
3486
+ <div class="tabs">
3487
+ <button class="tab" data-tab="realtime">Realtime</button>
3488
+ ${this.networkEnabled ? `<button class="tab" data-tab="network">Network <span class="tcount" data-el="netTab">0</span></button>` : ""}
3489
+ <button class="tab" data-tab="perf">Perf</button>
3490
+ <button class="tab" data-tab="logs">Logs <span class="tcount" data-el="logsTab">0</span></button>
3491
+ <button class="tab" data-tab="runtime">Runtime</button>
3492
+ </div>
3493
+ <div class="panes" data-el="panes">
3494
+ ${this.realtimePane()}
3495
+ ${this.networkEnabled ? this.networkPane() : ""}
3496
+ ${this.perfPane()}
3497
+ ${this.logsPane()}
3498
+ ${this.runtimePane()}
3499
+ </div>
3500
+ </div>`;
3501
+ }
3502
+ realtimePane() {
3503
+ return `<div class="pane" data-pane="realtime"><div class="cards">
3504
+ <div class="card hero">
3505
+ <h4><span class="blue">⚡</span> Realtime</h4>
3506
+ <div class="big"><span data-el="rtBig">0</span><small>msg/s</small></div>
3507
+ <svg viewBox="0 0 ${CHART_W} 38" preserveAspectRatio="none">
3508
+ <polygon class="area rt" data-el="rtArea" points=""/>
3509
+ <polyline class="spark rt" data-el="rtLine" points=""/>
3510
+ </svg>
3511
+ ${this.kv("transport", "rtTransport")}
3512
+ ${this.kv("protocole", "rtProto")}
3513
+ ${this.kv("état", "rtState")}
3514
+ ${this.kv("frames reçues", "rtFrames")}
3515
+ ${this.kv("pic", "rtPeak")}
3516
+ <div class="tag"><b>HTTP &amp; WebSocket, même contexte.</b> Push natif, <b>0 polling</b> — chaque chiffre arrive en temps réel par le Core isomorphe.</div>
3517
+ </div>
3518
+ ${this.frontendCard()}
3519
+ </div></div>`;
3520
+ }
3521
+ networkPane() {
3522
+ return `<div class="pane np" data-pane="network">
3523
+ <div class="net-head">
3524
+ <span class="chip"><span class="k">total</span><span data-el="netTotal">0</span></span>
3525
+ <span class="chip"><span class="k">err</span><span class="crit" data-el="netErr">0</span></span>
3526
+ <span class="chip"><span class="k">pending</span><span class="muted" data-el="netPend">0</span></span>
3527
+ <span class="net-clear" data-el="netClear" title="vider la liste">vider</span>
3528
+ </div>
3529
+ <div class="net-list" data-el="netList"><div class="empty">en attente d'appels AJAX (fetch / XHR)…</div></div>
3530
+ <div class="net-detail" data-el="netDetail"><div class="empty">clique un appel → profil serveur (waterfall des phases, route, user).</div></div>
3531
+ </div>`;
3532
+ }
3533
+ perfPane() {
3534
+ return `<div class="pane" data-pane="perf"><div class="cards">
3535
+ <div class="card">
3536
+ <h4>Performance</h4>
3537
+ ${this.chart("CPU", "cpuVal", "cpuPeak", "cpuLine", "cpuArea")}
3538
+ ${this.chart("Heap", "heapVal", "heapPeak", "heapLine", "heapArea")}
3539
+ ${this.chart("Event loop", "loopVal", "loopPeak", "loopLine", "loopArea")}
3540
+ </div>
3541
+ </div></div>`;
3542
+ }
3543
+ /**
3544
+ * L'onglet Logs — une petite console de lecture, pas une liste qui défile.
3545
+ *
3546
+ * Ce qu'elle porte, et pourquoi : un FILTRE par sévérité et une RECHERCHE
3547
+ * (sans quoi on lit le bruit au lieu de son incident) ; une PAUSE (sans quoi
3548
+ * la ligne qu'on essaie de lire s'en va) ; une COPIE (une trace se colle dans
3549
+ * un ticket) ; et le DÉTAIL d'une entrée, où vit le `requestId` — le champ qui
3550
+ * relie ce journal à sa requête, et que la barre recevait sans jamais le
3551
+ * montrer.
3552
+ */
3553
+ logsPane() {
3554
+ return `<div class="pane" data-pane="logs">
3555
+ <div class="counts">
3556
+ <button type="button" class="chip sevf" data-sev="all" data-tip="Tout afficher"><span class="k">total</span><span data-el="cTotal">0</span></button>
3557
+ <button type="button" class="chip sevf" data-sev="err" data-tip="N'afficher que les erreurs (sévérité 0 à 3)"><span class="k">err</span><span class="crit" data-el="cErr">0</span></button>
3558
+ <button type="button" class="chip sevf" data-sev="warn" data-tip="N'afficher que les alertes (sévérité 4)"><span class="k">warn</span><span class="warn" data-el="cWarn">0</span></button>
3559
+ <span class="chip" data-tip="Entrées écartées par le serveur faute de place dans le tampon — le flux était plus rapide que l'envoi"><span class="k">dropped</span><span class="muted" data-el="cDrop">0</span></span>
3560
+ <span class="spacer"></span>
3561
+ <input class="search" data-el="logSearch" type="search" placeholder="rechercher…"
3562
+ aria-label="Filtrer les journaux sur leur texte, leur module ou leur requête" />
3563
+ <button type="button" class="toolbtn" data-el="btnPause" aria-pressed="false" data-tip="Suspendre l'affichage — les entrées continuent d'arriver et se rattrapent à la reprise">⏸</button>
3564
+ <button type="button" class="toolbtn" data-el="btnCopy" data-tip="Copier les entrées affichées dans le presse-papiers">⧉</button>
3565
+ <button type="button" class="toolbtn" data-el="btnClearFeed" data-tip="Vider la liste affichée (n'efface rien côté serveur)">⌫</button>
3566
+ </div>
3567
+ <div class="feed" data-el="feed" role="log" aria-label="Journaux du serveur"><div class="empty">en attente de logs…</div></div>
3568
+ </div>`;
3569
+ }
3570
+ /**
3571
+ * Ce que la barre garde sur CE navigateur — écrit noir sur blanc.
3572
+ *
3573
+ * Une console de développement qui écrit dans le stockage local sans le dire
3574
+ * est exactement ce qu'on reproche aux autres. La liste est dérivée des clés
3575
+ * réelles, donc elle ne peut pas mentir par oubli, et le bouton efface
3576
+ * précisément celles-là — jamais le stockage de l'application hôte.
3577
+ */
3578
+ storageCard() {
3579
+ return `<div class="card">
3580
+ <h4>Stockage local</h4>
3581
+ <div class="tag">Ces valeurs vivent dans ce navigateur, sur cet appareil. Elles ne quittent jamais la page et ne concernent que l'apparence de la barre.</div>
3582
+ ${lsKeys().map((k) => `<div class="drow"><span class="dk">${escapeHtml(k.replace("nf.debugbar.", ""))}</span><span class="dv">${escapeHtml(lsGet(k, "—"))}</span></div>`).join("")}
3583
+ <div style="margin-top:8px"><button type="button" class="toolbtn" data-el="btnPurge" data-tip="Effacer les préférences de la barre sur ce navigateur — l'application n'est pas touchée">Purger l'état de la barre</button></div>
3584
+ </div>`;
3585
+ }
3586
+ runtimePane() {
3587
+ return `<div class="pane" data-pane="runtime"><div class="cards">
3588
+ ${this.storageCard()}
3589
+ <div class="card">
3590
+ <h4>Runtime</h4>
3591
+ ${this.kv("app", "appName")}
3592
+ ${this.kv("version", "appVersion")}
3593
+ ${this.kv("environnement", "envRow")}
3594
+ ${this.kv("branche", "branchRow")}
3595
+ ${this.kv("pid", "pid")}
3596
+ ${this.kv("uptime", "uptime")}
3597
+ ${this.kv("instance", "instance")}
3598
+ ${this.kv("cpu cores", "cores")}
3599
+ ${this.kv("loadavg", "load")}
3600
+ ${this.kv("rss", "rss")}
3601
+ ${this.kv("heap used", "heapUsed")}
3602
+ ${this.kv("heap total", "heapTotal")}
3603
+ ${this.kv("heap limit", "heapLimit")}
3604
+ ${this.kv("external", "external")}
3605
+ </div>
3606
+ </div></div>`;
3607
+ }
3608
+ /**
3609
+ * Un indicateur du bandeau — et un RACCOURCI vers l'onglet qui le détaille.
3610
+ *
3611
+ * C'est un `<button>` et non un `<span>` : le bandeau ne se replie plus au
3612
+ * clic (c'était le défaut principal — lire une valeur refermait le panneau),
3613
+ * donc chaque indicateur peut faire quelque chose d'utile de son propre clic.
3614
+ * Mener à l'onglet qui explique le chiffre est ce que l'utilisateur attend,
3615
+ * et un bouton l'annonce et s'atteint au clavier.
3616
+ */
3617
+ miniMetric(label, valKey, sparkKey, init, goto, tip) {
3618
+ return `<button type="button" class="metric goto" data-goto="${goto}" data-tip="${escapeHtml(tip)}">
3619
+ <span class="k">${label}</span>
3620
+ <span class="v" data-el="${valKey}">${init}</span>
3621
+ <svg class="mini" viewBox="0 0 ${MINI_W} ${MINI_H}" preserveAspectRatio="none">
3622
+ <polyline class="spark ok" data-el="${sparkKey}" points=""/>
3623
+ </svg>
3624
+ </button>`;
3625
+ }
3626
+ chart(label, valKey, peakKey, lineKey, areaKey) {
3627
+ return `<div class="chart">
3628
+ <div class="hd">
3629
+ <span class="lbl">${label}</span>
3630
+ <span><span class="val" data-el="${valKey}">—</span> <span class="peak" data-el="${peakKey}"></span></span>
3631
+ </div>
3632
+ <svg viewBox="0 0 ${CHART_W} ${CHART_H}" preserveAspectRatio="none">
3633
+ <polygon class="area ok" data-el="${areaKey}" points=""/>
3634
+ <polyline class="spark ok" data-el="${lineKey}" points=""/>
3635
+ </svg>
3636
+ </div>`;
3637
+ }
3638
+ kv(label, key) {
3639
+ return `<div class="kv"><span class="k">${label}</span><span class="v" data-el="${key}">—</span></div>`;
3640
+ }
3641
+ /** Carte Frontend / HMR — affichée seulement si un contexte frontend est fourni. */
3642
+ frontendCard() {
3643
+ if (!this.frontend) return "";
3644
+ const fw = FRAMEWORKS[this.frontend.framework ?? ""] ?? {
3645
+ label: this.frontend.framework ?? "Frontend",
3646
+ color: "#3aa0ff"
3647
+ };
3648
+ return `<div class="card fe">
3649
+ <h4><span style="color:${fw.color}">●</span> Frontend</h4>
3650
+ <div class="fw">
3651
+ <span class="badge" style="background:${fw.color}">${escapeHtml(fw.label)}</span>
3652
+ <span class="name">${escapeHtml(this.frontend.name ?? "")}</span>
3653
+ </div>
3654
+ <div class="hd"><span class="lbl">HMR hot updates</span><span class="hmr-big" data-el="hmrBig">0</span></div>
3655
+ <svg viewBox="0 0 ${CHART_W} 34" preserveAspectRatio="none">
3656
+ <polygon class="area fe" data-el="hmrArea" points=""/>
3657
+ <polyline class="spark fe" data-el="hmrLine" points=""/>
3658
+ </svg>
3659
+ ${this.kv("bundler", "feVite")}
3660
+ ${this.kv("dernier module", "hmrLast")}
3661
+ <div class="tag"><b>Backend &amp; frontend, un seul process.</b> HMR Vite branché au runtime — sauvegarde un composant, la barre pulse.</div>
3662
+ </div>`;
3663
+ }
3664
+ wireRealtime() {
3665
+ const offState = this.client.onState((state) => {
3666
+ this.model.setState(state);
3667
+ this.scheduleRender();
3668
+ });
3669
+ const offTick = this.client.onStats(() => {
3670
+ if (!this.live) return;
3671
+ this.sampleThroughput();
3672
+ this.scheduleRender();
3673
+ });
3674
+ const offStats = this.client.on(CHANNELS.stats, (...a) => {
3675
+ const p = a[0];
3676
+ if (p && typeof p === "object") this.model.ingestStats(p);
3677
+ this.scheduleRender();
3678
+ });
3679
+ const offSyslog = this.client.on(CHANNELS.syslog, (...a) => {
3680
+ const p = a[0];
3681
+ if (p && typeof p === "object") this.model.ingestSyslog(p);
3682
+ this.scheduleRender();
3683
+ });
3684
+ this.model.setState(this.client.state);
3685
+ this.disposers.push(offState, offTick, offStats, offSyslog, () => {
3686
+ this.stopLive();
3687
+ });
3688
+ if (this.live) this.startLive();
3689
+ }
3690
+ /**
3691
+ * Démarre les abonnements realtime (tickers serveur stats+syslog). REF-COMPTÉ
3692
+ * (UNE fois) sur le client PARTAGÉ : un canal reste actif tant qu'un
3693
+ * consommateur le veut ; le client ré-abonne seul au reconnect. Idempotent.
3694
+ */
3695
+ startLive() {
3696
+ if (this.liveOff) return;
3697
+ this.client.subscribe(CHANNELS.stats);
3698
+ this.client.subscribe(CHANNELS.syslog);
3699
+ this.liveOff = () => {
3700
+ this.client.unsubscribe(CHANNELS.stats);
3701
+ this.client.unsubscribe(CHANNELS.syslog);
3702
+ };
3703
+ }
3704
+ /** Arrête les abonnements realtime (relâche la réf de la barre). Idempotent. */
3705
+ stopLive() {
3706
+ if (!this.liveOff) return;
3707
+ this.liveOff();
3708
+ this.liveOff = null;
3709
+ }
3710
+ /** Active/désactive le temps réel (bouton de la barre) + persiste la préférence. */
3711
+ setLive(v) {
3712
+ if (v === this.live && !!this.liveOff === v) return;
3713
+ this.live = v;
3714
+ lsSet(LS.live, v ? "1" : "0");
3715
+ if (v) {
3716
+ this.prevFrames = this.client.framesReceived;
3717
+ this.startLive();
3718
+ } else {
3719
+ this.stopLive();
3720
+ this.rtRate = 0;
3721
+ pushCap(this.rtSeries, 0, RT_POINTS);
3722
+ }
3723
+ this.updateLiveBtn();
3724
+ this.scheduleRender();
3725
+ }
3726
+ /** Met à jour l'aspect du bouton « Temps réel » selon l'état. */
3727
+ updateLiveBtn() {
3728
+ const btn = this.el["btnLive"];
3729
+ if (!btn) return;
3730
+ btn.textContent = this.live ? "flux ON" : "flux OFF";
3731
+ btn.title = this.live ? "Flux push ACTIF (stats + logs) — clic pour couper. ≠ état de la connexion (pastille « realtime »)." : "Flux push coupé (perf) — clic pour activer (stats + logs). ≠ état de la connexion (pastille « realtime »).";
3732
+ btn.setAttribute("aria-pressed", this.live ? "true" : "false");
3733
+ btn.classList.toggle("on", this.live);
3734
+ }
3735
+ sampleThroughput() {
3736
+ const total = this.client.framesReceived;
3737
+ this.rtRate = Math.max(0, total - this.prevFrames);
3738
+ this.prevFrames = total;
3739
+ if (this.rtRate > this.rtPeak) this.rtPeak = this.rtRate;
3740
+ pushCap(this.rtSeries, this.rtRate, RT_POINTS);
3741
+ this.hmrRate = Math.max(0, this.hmrCount - this.hmrPrev);
3742
+ this.hmrPrev = this.hmrCount;
3743
+ pushCap(this.hmrSeries, this.hmrRate, RT_POINTS);
3744
+ }
3745
+ wireHmr() {
3746
+ if (!this.frontend) return;
3747
+ const dispose = observeViteHmr((e) => this.onHmr(e));
3748
+ this.disposers.push(dispose);
3749
+ }
3750
+ onHmr(e) {
3751
+ if (e.kind === "connected") this.viteConnected = true;
3752
+ else if (e.kind === "update") {
3753
+ this.hmrCount++;
3754
+ this.hmrLast = e.path ?? "(module)";
3755
+ this.flashHmr();
3756
+ } else if (e.kind === "full-reload") {
3757
+ this.hmrCount++;
3758
+ this.hmrLast = e.path ?? "full reload";
3759
+ this.flashHmr();
3760
+ }
3761
+ this.scheduleRender();
3762
+ }
3763
+ flashHmr() {
3764
+ const node = this.el.hmrBig;
3765
+ if (!node) return;
3766
+ node.setAttribute("class", "hmr-big hmr-flash");
3767
+ if (this.flashTimer) clearTimeout(this.flashTimer);
3768
+ this.flashTimer = setTimeout(() => {
3769
+ node.setAttribute("class", "hmr-big");
3770
+ }, 600);
3771
+ }
3772
+ wireNetwork() {
3773
+ if (!this.networkEnabled) return;
3774
+ const viteOrigin = this.frontend?.viteOrigin ?? "";
3775
+ const uninstall = installNetworkInterceptor({
3776
+ onChange: (e) => this.onNetEntry(e),
3777
+ ignore: (url) => url.includes(this.profilerBase) || url.includes("/@vite/") || url.includes("/@fs/") || url.includes("/node_modules/.vite/") || viteOrigin !== "" && url.includes(viteOrigin)
3778
+ });
3779
+ this.disposers.push(uninstall);
3780
+ const list = this.el.netList;
3781
+ if (list) {
3782
+ const onClick = (ev) => {
3783
+ const row = ev.target?.closest?.(".net-row");
3784
+ if (!row) return;
3785
+ const rid = row.dataset.rid;
3786
+ this.selectRow(row, rid || null);
3787
+ };
3788
+ list.addEventListener("click", onClick);
3789
+ this.disposers.push(() => list.removeEventListener("click", onClick));
3790
+ }
3791
+ const detail = this.el.netDetail;
3792
+ if (detail) {
3793
+ const onDetail = (ev) => {
3794
+ if (ev.target?.closest?.("[data-act=\"close\"]")) this.deselect();
3795
+ };
3796
+ detail.addEventListener("click", onDetail);
3797
+ this.disposers.push(() => detail.removeEventListener("click", onDetail));
3798
+ }
3799
+ const clear = this.el.netClear;
3800
+ if (clear) {
3801
+ const onClear = (e) => {
3802
+ e.stopPropagation();
3803
+ this.net.clear();
3804
+ this.netRows.clear();
3805
+ this.selectedRid = null;
3806
+ this.selRowId = null;
3807
+ const node = this.el.netList;
3808
+ if (node) node.innerHTML = `<div class="empty">en attente d'appels AJAX (fetch / XHR)…</div>`;
3809
+ this.detailVersion++;
3810
+ this.scheduleRender();
3811
+ };
3812
+ clear.addEventListener("click", onClear);
3813
+ this.disposers.push(() => clear.removeEventListener("click", onClear));
3814
+ }
3815
+ }
3816
+ onNetEntry(e) {
3817
+ this.net.ingest(e);
3818
+ this.upsertNetRow(e);
3819
+ if (e.requestId && e.requestId === this.selectedRid) this.detailVersion++;
3820
+ this.scheduleRender();
3821
+ }
3822
+ /** Crée ou met à jour le nœud de ligne d'un appel (jamais d'innerHTML global). */
3823
+ upsertNetRow(e) {
3824
+ const list = this.el.netList;
3825
+ if (!list) return;
3826
+ let row = this.netRows.get(e.id);
3827
+ if (!row) {
3828
+ if (this.netRows.size === 0) list.textContent = "";
3829
+ row = document.createElement("div");
3830
+ row.className = "net-row";
3831
+ row.innerHTML = "<span class=\"net-method\"></span><span class=\"net-path\"></span><span class=\"net-rid\"></span><span class=\"net-status\"></span><span class=\"net-dur\"></span>";
3832
+ this.netRows.set(e.id, row);
3833
+ list.insertBefore(row, list.firstChild);
3834
+ while (this.netRows.size > 80 && list.lastElementChild) {
3835
+ const last = list.lastElementChild;
3836
+ for (const [id, n] of this.netRows) if (n === last) {
3837
+ this.netRows.delete(id);
3838
+ break;
3839
+ }
3840
+ last.remove();
3841
+ }
3842
+ }
3843
+ this.fillNetRow(row, e);
3844
+ }
3845
+ /** Remplit/actualise les cellules d'une ligne (textContent → 0 échappement). */
3846
+ fillNetRow(row, e) {
3847
+ row.dataset.rid = e.requestId ?? "";
3848
+ row.className = `net-row${e.requestId && e.requestId === this.selectedRid ? " sel" : ""}${isError(e) ? " err" : ""}`;
3849
+ const [m, path, rid, status, dur] = row.children;
3850
+ m.className = `net-method ${methodClass(e.method)}`;
3851
+ m.textContent = e.method;
3852
+ path.textContent = e.path;
3853
+ path.title = e.url;
3854
+ rid.textContent = e.requestId ? shortId(e.requestId) : "";
3855
+ if (e.pending) {
3856
+ status.className = "net-status sp";
3857
+ status.textContent = "···";
3858
+ } else if (e.error) {
3859
+ status.className = "net-status s5";
3860
+ status.textContent = "ERR";
3861
+ } else {
3862
+ status.className = `net-status s${statusFamily(e.status)}`;
3863
+ status.textContent = e.status === null ? "—" : String(e.status);
3864
+ }
3865
+ dur.textContent = e.durationMs === null ? "" : `${e.durationMs}ms`;
3866
+ }
3867
+ /** Sélectionne une ligne (highlight) + déclenche le fetch du profil. */
3868
+ selectRow(row, rid) {
3869
+ if (this.selRowId !== null) this.netRows.get(this.selRowId)?.classList.remove("sel");
3870
+ row.classList.add("sel");
3871
+ const idEntry = [...this.netRows.entries()].find(([, n]) => n === row);
3872
+ this.selRowId = idEntry ? idEntry[0] : null;
3873
+ this.selectedRid = rid;
3874
+ this.selNoRid = !rid;
3875
+ this.detailVersion++;
3876
+ const minDetail = detailPanelH();
3877
+ if (rid && this.panelH < minDetail) this.setPanelH(minDetail);
3878
+ if (rid && !this.net.profileState(rid)) this.fetchProfile(rid);
3879
+ if (rid && typeof window !== "undefined") try {
3880
+ window.dispatchEvent(new CustomEvent(PLATFORM_EVENTS.debugbarSelect, { detail: { requestId: rid } }));
3881
+ } catch {}
3882
+ this.scheduleRender();
3883
+ }
3884
+ /** Fetch `/{profilerBase}/{requestId}` (ignoré par l'intercepteur). */
3885
+ fetchProfile(requestId) {
3886
+ if (typeof fetch === "undefined") return;
3887
+ this.net.setProfileState(requestId, { status: "loading" });
3888
+ fetch(`${this.profilerBase}/${encodeURIComponent(requestId)}`, { headers: { accept: "application/json" } }).then(async (res) => {
3889
+ if (res.status === 404) {
3890
+ this.net.setProfileState(requestId, { status: "missing" });
3891
+ return;
3892
+ }
3893
+ const ct = res.headers.get("content-type") ?? "";
3894
+ if (!res.ok || !ct.includes("application/json")) {
3895
+ this.net.setProfileState(requestId, {
3896
+ status: "error",
3897
+ message: res.ok ? "réponse non-JSON (path profiler non proxifié par Vite ?)" : `HTTP ${res.status}`
3898
+ });
3899
+ return;
3900
+ }
3901
+ const profile = await res.json();
3902
+ this.net.setProfileState(requestId, {
3903
+ status: "ready",
3904
+ profile
3905
+ });
3906
+ }).catch((err) => {
3907
+ this.net.setProfileState(requestId, {
3908
+ status: "error",
3909
+ message: err instanceof Error ? err.message : "fetch failed"
3910
+ });
3911
+ }).finally(() => {
3912
+ if (requestId === this.selectedRid) this.detailVersion++;
3913
+ this.scheduleRender();
3914
+ });
3915
+ }
3916
+ scheduleRender() {
3917
+ if (typeof requestAnimationFrame === "undefined") {
3918
+ this.render();
3919
+ return;
3920
+ }
3921
+ if (this.rafPending) return;
3922
+ this.rafPending = true;
3923
+ requestAnimationFrame(() => {
3924
+ this.rafPending = false;
3925
+ this.render();
3926
+ });
3927
+ }
3928
+ text(key, value) {
3929
+ const node = this.el[key];
3930
+ if (node) node.textContent = value;
3931
+ }
3932
+ /** className safe pour HTML *et* SVG (SVGElement.className n'est pas une string). */
3933
+ cls(key, value) {
3934
+ this.el[key]?.setAttribute("class", value);
3935
+ }
3936
+ setPoints(lineKey, areaKey, series, tier, height, max) {
3937
+ const pts = sparklinePoints(series, CHART_W, height, max);
3938
+ this.el[lineKey]?.setAttribute("points", pts);
3939
+ this.el[lineKey]?.setAttribute("class", `spark ${tier}`);
3940
+ if (areaKey) {
3941
+ const area = this.el[areaKey];
3942
+ if (area) {
3943
+ area.setAttribute("points", pts ? `${pts} ${CHART_W},${height} 0,${height}` : "");
3944
+ area.setAttribute("class", `area ${tier}`);
3945
+ }
3946
+ }
3947
+ }
3948
+ render() {
3949
+ if (!this.bar) return;
3950
+ const v = this.model.view;
3951
+ this.renderStrip(v);
3952
+ if (!this.bar.classList.contains("open")) return;
3953
+ switch (this.activeTab) {
3954
+ case "realtime":
3955
+ this.renderRealtimePane(v);
3956
+ break;
3957
+ case "network":
3958
+ this.renderNetwork();
3959
+ break;
3960
+ case "perf":
3961
+ this.renderPerf(v);
3962
+ break;
3963
+ case "logs":
3964
+ this.renderLogsPane(v);
3965
+ break;
3966
+ case "runtime": this.renderRuntime(v);
3967
+ }
3968
+ }
3969
+ /** Bandeau toujours visible (chips + pouls) — léger. */
3970
+ renderStrip(v) {
3971
+ const connected = v.state === "connected";
3972
+ const cpuT = gauge(v.cpuPercent);
3973
+ const memT = gauge(v.heapPercent);
3974
+ this.text("envBadge", v.env || "env");
3975
+ this.cls("envBadge", `env-badge ${envClass(v.env)}`);
3976
+ this.text("branchName", v.branch || "—");
3977
+ this.el.branch?.setAttribute("title", v.branch ? `branche git : ${v.branch}` : "branche git");
3978
+ this.text("mEnv", v.env);
3979
+ this.cls("dot", `dot ${v.state}`);
3980
+ this.cls("mdot", `dot ${v.state}`);
3981
+ this.text("mrate", `${this.rtRate}/s`);
3982
+ this.cls("rtPill", connected ? "rt-pill connected" : "rt-pill");
3983
+ const connTitle = `Connexion temps réel : ${v.state}`;
3984
+ this.el.rtPill?.setAttribute("title", connTitle);
3985
+ this.el.dot?.setAttribute("title", connTitle);
3986
+ this.text("rt", `${this.rtRate}/s`);
3987
+ this.cls("rt", "v blue");
3988
+ this.el.rtMini?.setAttribute("points", sparklinePoints(this.rtSeries, MINI_W, MINI_H));
3989
+ this.el.rtMini?.setAttribute("class", "spark rt");
3990
+ this.text("cpu", `${v.cpuPercent}%`);
3991
+ this.cls("cpu", `v ${cpuT}`);
3992
+ this.text("mem", `${v.heapPercent}%`);
3993
+ this.cls("mem", `v ${memT}`);
3994
+ this.el.cpuMini?.setAttribute("points", sparklinePoints(v.cpuSeries, MINI_W, MINI_H, 100));
3995
+ this.el.cpuMini?.setAttribute("class", `spark ${cpuT}`);
3996
+ this.el.memMini?.setAttribute("points", sparklinePoints(v.heapSeries, MINI_W, MINI_H, 100));
3997
+ this.el.memMini?.setAttribute("class", `spark ${memT}`);
3998
+ if (this.networkEnabled) {
3999
+ this.text("netChip", String(this.net.total));
4000
+ this.cls("netChip", this.net.errors > 0 ? "crit" : "blue");
4001
+ this.text("netTab", String(this.net.total));
4002
+ this.cls("netTab", this.net.errors > 0 ? "tcount crit" : "tcount");
4003
+ }
4004
+ this.text("logs", String(v.logTotal));
4005
+ this.text("err", String(v.errorCount));
4006
+ this.text("logsTab", String(v.logTotal));
4007
+ this.cls("logsTab", v.errorCount > 0 ? "tcount crit" : "tcount");
4008
+ }
4009
+ renderRealtimePane(v) {
4010
+ const live = v.state === "connected";
4011
+ this.text("rtBig", String(this.rtRate));
4012
+ this.setPoints("rtLine", "rtArea", this.rtSeries, "rt", 38);
4013
+ this.text("rtTransport", "WebSocket");
4014
+ this.text("rtProto", "JSON-RPC 2.0");
4015
+ this.text("rtState", v.state);
4016
+ this.cls("rtState", `v ${live ? "ok" : "warn"}`);
4017
+ this.text("rtFrames", String(this.client.framesReceived));
4018
+ this.text("rtPeak", `${this.rtPeak}/s`);
4019
+ if (this.frontend) {
4020
+ this.text("hmrBig", String(this.hmrCount));
4021
+ this.setPoints("hmrLine", "hmrArea", this.hmrSeries, "fe", 34);
4022
+ this.text("feVite", this.viteConnected ? "Vite · connecté" : "Vite · …");
4023
+ this.cls("feVite", `v ${this.viteConnected ? "ok" : "warn"}`);
4024
+ this.text("hmrLast", this.hmrLast);
4025
+ }
4026
+ }
4027
+ renderPerf(v) {
4028
+ const cpuT = gauge(v.cpuPercent);
4029
+ const memT = gauge(v.heapPercent);
4030
+ const loopT = gauge(v.eventLoopMs, 50, 200);
4031
+ this.text("cpuVal", `${v.cpuPercent}%`);
4032
+ this.text("cpuPeak", v.cpuPeak ? `peak ${v.cpuPeak}%` : "");
4033
+ this.cls("cpuVal", `val ${cpuT}`);
4034
+ this.setPoints("cpuLine", "cpuArea", v.cpuSeries, cpuT, CHART_H, 100);
4035
+ this.text("heapVal", `${v.heapPercent}%`);
4036
+ this.text("heapPeak", v.heapPeak ? `peak ${v.heapPeak}%` : "");
4037
+ this.cls("heapVal", `val ${memT}`);
4038
+ this.setPoints("heapLine", "heapArea", v.heapSeries, memT, CHART_H, 100);
4039
+ this.text("loopVal", `${v.eventLoopMs}ms`);
4040
+ this.text("loopPeak", v.eventLoopPeak ? `peak ${v.eventLoopPeak}ms` : "");
4041
+ this.cls("loopVal", `val ${loopT}`);
4042
+ this.setPoints("loopLine", "loopArea", v.loopSeries, loopT, CHART_H);
4043
+ }
4044
+ renderRuntime(v) {
4045
+ this.text("appName", v.appName || "—");
4046
+ this.text("appVersion", v.appVersion || "—");
4047
+ this.text("envRow", v.debug ? `${v.env} · debug` : v.env || "—");
4048
+ this.text("branchRow", v.branch || "—");
4049
+ this.text("pid", String(v.pid));
4050
+ this.text("uptime", formatUptime(v.uptime));
4051
+ this.text("instance", v.instanceId);
4052
+ this.text("cores", String(v.cpuCount));
4053
+ this.text("load", v.loadavg.map((n) => n.toFixed(2)).join(" ") || "—");
4054
+ this.text("rss", formatBytes(v.rss));
4055
+ this.text("heapUsed", formatBytes(v.heapUsed));
4056
+ this.text("heapTotal", formatBytes(v.heapTotal));
4057
+ this.text("heapLimit", formatBytes(v.heapLimit));
4058
+ this.text("external", formatBytes(v.external));
4059
+ }
4060
+ renderLogsPane(v) {
4061
+ this.text("cTotal", String(v.logTotal));
4062
+ this.text("cErr", String(v.errorCount));
4063
+ this.text("cWarn", String(v.warnCount));
4064
+ this.text("cDrop", String(v.dropped));
4065
+ this.renderFeed(v.feed);
4066
+ }
4067
+ /** Classe de couleur d'une entrée, dérivée de sa sévérité RFC 5424. */
4068
+ static tierOf(severity) {
4069
+ if (severity <= 3) return "crit";
4070
+ if (severity === 4) return "warn";
4071
+ if (severity === 7) return "muted";
4072
+ return "info";
4073
+ }
4074
+ /** L'entrée passe-t-elle le filtre de sévérité et la recherche en cours ? */
4075
+ feedMatch(l) {
4076
+ if (this.feedSev === "err" && l.severity > 3) return false;
4077
+ if (this.feedSev === "warn" && l.severity !== 4) return false;
4078
+ const q = this.feedQuery;
4079
+ if (q === "") return true;
4080
+ return l.text.toLowerCase().includes(q) || l.module.toLowerCase().includes(q) || l.msgid.toLowerCase().includes(q) || (l.requestId ?? "").toLowerCase().includes(q);
4081
+ }
4082
+ /** Une ligne de journal — horodatage, sévérité, module, texte, requête. */
4083
+ feedRow(l) {
4084
+ const tier = DebugBar.tierOf(l.severity);
4085
+ const rid = l.requestId ? `<span class="rid" data-tip="requête ${escapeHtml(l.requestId)} — la même valeur relie ce journal à sa route, à ses requêtes de base et à sa réponse">${escapeHtml(l.requestId.slice(0, 8))}</span>` : "";
4086
+ return `<div class="log" data-uid="${l.uid}" tabindex="0" role="button" aria-expanded="false" aria-label="Entrée ${escapeHtml(l.name)} de ${escapeHtml(l.module)} — ouvrir le détail"><span class="ts">${fmtClock(l.ts)}</span><span class="sev ${tier}">${escapeHtml(l.name)}</span><span class="mod">${escapeHtml(l.module)}</span><span class="txt">${escapeHtml(l.text)}</span>${rid}</div>`;
4087
+ }
4088
+ /** Le détail d'une entrée — ce qu'on colle dans un ticket. */
4089
+ feedDetail(l) {
4090
+ const row = (k, v) => `<div class="drow"><span class="dk">${k}</span><span class="dv">${escapeHtml(v)}</span></div>`;
4091
+ return `<div class="logdetail">
4092
+ ${row("horodatage", new Date(l.ts).toISOString())}
4093
+ ${row("sévérité", `${l.name} (${l.severity})`)}
4094
+ ${row("module", l.module)}
4095
+ ${l.msgid ? row("catégorie", l.msgid) : ""}
4096
+ ${l.pid ? row("worker (pid)", String(l.pid)) : ""}
4097
+ ${row("séquence", String(l.uid))}
4098
+ ${l.requestId ? row("requête", l.requestId) : ""}
4099
+ ${row("message", l.text)}
4100
+ </div>`;
4101
+ }
4102
+ /**
4103
+ * Rendu du journal — **incrémental**, et c'est le point.
4104
+ *
4105
+ * L'ancienne version réassignait `innerHTML` en entier à chaque arrivée. Deux
4106
+ * conséquences qu'on ne voit pas dans le code et qu'on subit à l'écran : la
4107
+ * SÉLECTION de texte s'efface — impossible de copier une ligne pendant que le
4108
+ * flux tourne — et la position de DÉFILEMENT saute. Ici, seules les entrées
4109
+ * nouvelles sont insérées en tête ; ce qui est affiché n'est jamais reconstruit.
4110
+ *
4111
+ * Un changement de filtre ou de recherche, lui, refait bien la liste : le jeu
4112
+ * affiché change entièrement, il n'y a rien à préserver.
4113
+ */
4114
+ renderFeed(feed) {
4115
+ const node = this.el.feed;
4116
+ if (!node) return;
4117
+ if (this.feedPaused) return;
4118
+ const refilter = this.feedDirty;
4119
+ if (!refilter && feed.length === this.feedLen) return;
4120
+ const previous = this.feedLen;
4121
+ this.feedLen = feed.length;
4122
+ this.feedDirty = false;
4123
+ if (refilter) {
4124
+ const rows = [];
4125
+ for (let i = feed.length - 1; i >= 0; i--) {
4126
+ const l = feed[i];
4127
+ if (this.feedMatch(l)) rows.push(this.feedRow(l));
4128
+ }
4129
+ node.innerHTML = rows.length === 0 ? `<div class="empty">${feed.length === 0 ? "en attente de logs…" : "aucune entrée ne correspond au filtre."}</div>` : rows.join("");
4130
+ return;
4131
+ }
4132
+ const added = feed.length - previous;
4133
+ if (previous < 0 || added < 0 || added > feed.length) {
4134
+ this.feedDirty = true;
4135
+ this.renderFeed(feed);
4136
+ return;
4137
+ }
4138
+ if (added === 0) return;
4139
+ let html = "";
4140
+ for (let i = feed.length - 1; i >= feed.length - added; i--) {
4141
+ const l = feed[i];
4142
+ if (this.feedMatch(l)) html += this.feedRow(l);
4143
+ }
4144
+ if (html === "") return;
4145
+ if (node.querySelector(".empty")) node.innerHTML = "";
4146
+ node.insertAdjacentHTML("afterbegin", html);
4147
+ while (node.childElementCount > 40) node.lastElementChild?.remove();
4148
+ }
4149
+ renderNetwork() {
4150
+ this.text("netTotal", String(this.net.total));
4151
+ this.text("netErr", String(this.net.errors));
4152
+ this.text("netPend", String(this.net.pending));
4153
+ if (this.detailVersion !== this.detailRendered) {
4154
+ this.detailRendered = this.detailVersion;
4155
+ this.renderDetail();
4156
+ }
4157
+ }
4158
+ renderDetail() {
4159
+ const node = this.el.netDetail;
4160
+ if (!node) return;
4161
+ const rid = this.selectedRid;
4162
+ const hasSel = !!rid || this.selNoRid;
4163
+ node.parentElement?.classList.toggle("sel", hasSel);
4164
+ if (!hasSel) {
4165
+ node.innerHTML = `<div class="empty">clique un appel → profil serveur (waterfall des phases, route, user).</div>`;
4166
+ return;
4167
+ }
4168
+ const closeBar = `<div class="det-bar"><span class="det-close" data-act="close" title="Fermer le détail">✕</span></div>`;
4169
+ let body;
4170
+ if (!rid) body = `<div class="det-err">cet appel n'a pas de requestId lisible — réponse sans header <b>X-Request-Id</b> (cross-origin sans Access-Control-Expose-Headers, ou appel hors Nodefony).</div>`;
4171
+ else {
4172
+ const st = this.net.profileState(rid);
4173
+ if (!st || st.status === "loading") body = `<div class="det-loading">profil <b>${escapeHtml(shortId(rid))}</b> — chargement…</div>`;
4174
+ else if (st.status === "missing") body = `<div class="det-err">profil introuvable (évincé du ring buffer, ou requête sans timing).</div>`;
4175
+ else if (st.status === "error") body = `<div class="det-err">erreur profiler : ${escapeHtml(st.message)}</div>`;
4176
+ else body = this.profileHtml(st.profile);
4177
+ }
4178
+ node.innerHTML = closeBar + body;
4179
+ }
4180
+ /** Désélectionne la requête → referme le détail (placeholder). */
4181
+ deselect() {
4182
+ if (this.selRowId !== null) this.netRows.get(this.selRowId)?.classList.remove("sel");
4183
+ this.selRowId = null;
4184
+ this.selectedRid = null;
4185
+ this.selNoRid = false;
4186
+ this.detailVersion++;
4187
+ this.scheduleRender();
4188
+ }
4189
+ /** Rend le détail d'un profil serveur (méta + waterfall des phases). */
4190
+ profileHtml(p) {
4191
+ const bars = computeWaterfall(p.phases);
4192
+ const total = p.durationMs === null ? "—" : `${p.durationMs}ms`;
4193
+ const kv = (k, val) => `<div class="kv"><span class="k">${k}</span><span class="v">${val}</span></div>`;
4194
+ const meta = `<div class="det-grid">` + kv("route", escapeHtml(p.route ?? "—")) + kv("controller", escapeHtml(p.controller ? `${p.controller}.${p.action ?? "?"}` : "—")) + kv("status", `${p.status ?? "—"}`) + kv("total serveur", total) + kv("user", escapeHtml(p.user ?? "anonyme")) + kv("kind", p.kind) + kv("requestId", escapeHtml(shortId(p.requestId))) + kv("traceparent", escapeHtml(traceId(p))) + (p.error ? kv("erreur", `<span class="crit">${escapeHtml(p.error)}</span>`) : "") + `</div>`;
4195
+ let wf = "";
4196
+ if (bars.length === 0) wf = `<div class="empty">aucune phase mesurée (timing désactivé ?).</div>`;
4197
+ else {
4198
+ wf = `<div class="wf-title">timeline des phases (serveur)</div><div class="wf">`;
4199
+ for (const b of bars) wf += `<div class="wf-row"><span class="wf-name">${escapeHtml(b.name)}</span><div class="wf-track"><div class="wf-bar ${b.tier}" style="left:${b.leftPct}%;width:${b.widthPct}%"></div></div><span class="wf-ms">${b.durationMs}ms</span></div>`;
4200
+ wf += `</div>`;
4201
+ }
4202
+ return meta + wf + this.queriesHtml(p.queries);
4203
+ }
4204
+ /** Rend les requêtes ORM (SEAM futur) — vide tant qu'aucun adapter ne pushe. */
4205
+ queriesHtml(queries) {
4206
+ if (!queries || queries.length === 0) return "";
4207
+ let out = `<div class="wf-title" style="margin-top:10px">requêtes ORM (${queries.length})</div><div class="net-list" style="border:0">`;
4208
+ for (const q of queries) out += `<div class="net-row" style="cursor:default"><span class="net-path" title="${escapeHtml(q.sql)}">${escapeHtml(q.sql)}</span>` + (q.connector ? `<span class="net-rid">${escapeHtml(q.connector)}</span>` : "") + (typeof q.rows === "number" ? `<span class="net-dur">${q.rows} rows</span>` : "") + `<span class="net-dur">${q.durationMs}ms</span></div>`;
4209
+ out += `</div>`;
4210
+ return out;
4211
+ }
4212
+ };
4213
+ /** Famille de status (2/3/4/5) → classe de couleur. `null` → 5 (inconnu). */
4214
+ function statusFamily(status) {
4215
+ if (status === null) return 5;
4216
+ return Math.floor(status / 100);
4217
+ }
4218
+ /** Méthode HTTP → classe de couleur du chip. */
4219
+ function methodClass(method) {
4220
+ const m = method.toLowerCase();
4221
+ if (m === "get" || m === "post" || m === "put" || m === "patch" || m === "delete") return m;
4222
+ return "ws";
4223
+ }
4224
+ /** Raccourci d'un id (1er bloc UUID, ou 8 chars). */
4225
+ function shortId(id) {
4226
+ return id.length > 8 ? id.slice(0, 8) : id;
4227
+ }
4228
+ /** Extrait le trace-id (2ᵉ segment) d'un `traceparent` W3C, raccourci. */
4229
+ function traceId(p) {
4230
+ if (!p.traceparent) return "—";
4231
+ const seg = p.traceparent.split("-");
4232
+ const tid = seg.length >= 2 ? seg[1] : p.traceparent;
4233
+ return tid && tid.length > 12 ? tid.slice(0, 12) + "…" : tid ?? "—";
4234
+ }
4235
+ /** Mappe un nom d'environnement vers une classe de couleur du badge. */
4236
+ function envClass(env) {
4237
+ const e = env.toLowerCase();
4238
+ if (e.startsWith("prod")) return "prod";
4239
+ if (e.startsWith("dev")) return "dev";
4240
+ if (e.startsWith("test")) return "test";
4241
+ if (e.startsWith("stag")) return "staging";
4242
+ return "";
4243
+ }
4244
+ function escapeHtml(s) {
4245
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4246
+ }
4247
+ //#endregion
4248
+ //#region src/client/debugbar/index.ts
4249
+ /**
4250
+ * Subpath `nodefony/debugbar` — point d'entrée de la debug bar Nodefony.
4251
+ *
4252
+ * Entry Rollup SÉPARÉE (tree-shakée, 0 octet si non importée) : ce fichier ne
4253
+ * doit JAMAIS être réexporté depuis `client/index.ts` (sinon le barrel `nodefony`
4254
+ * navigateur tire toute la barre). Cf décision `project_client_lib_subpaths_decision`.
4255
+ *
4256
+ * Usage :
4257
+ * ```ts
4258
+ * import { mountDebugBar } from "nodefony/debugbar";
4259
+ * mountDebugBar(); // dev only
4260
+ * ```
4261
+ */
4262
+ /**
4263
+ * Monte la debug bar sur la page courante. Idempotent (un seul widget par page).
4264
+ * No-op hors navigateur (SSR). Branche un {@link RealtimeClient} sur le WS
4265
+ * realtime et rend l'overlay live.
4266
+ *
4267
+ * @param opts - URL du WS, client partagé, position, ouverture initiale
4268
+ * @returns l'instance {@link DebugBar} (appeler `.unmount()` pour retirer)
4269
+ */
4270
+ function mountDebugBar(opts = {}) {
4271
+ return new DebugBar(opts).mount();
4272
+ }
4273
+ //#endregion
4274
+ export { DebugBar, NetworkModel, computeWaterfall, mountDebugBar as default, mountDebugBar, isError as isNetworkError, phaseTier };