funifier-mcp 0.2.25 → 0.2.27

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 (211) hide show
  1. package/.cursor/rules/funifier.mdc +38 -41
  2. package/.github/copilot-instructions.md +38 -41
  3. package/AGENTS.md +56 -49
  4. package/README.md +40 -22
  5. package/datasource-funifier-docs/.coverage.json +326 -0
  6. package/datasource-funifier-docs/.validation.json +593 -0
  7. package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
  8. package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
  9. package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
  10. package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
  11. package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
  12. package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
  13. package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
  14. package/datasource-funifier-docs/knowledge/index.md +5 -2
  15. package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
  16. package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
  17. package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
  18. package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
  19. package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
  20. package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
  21. package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
  22. package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
  23. package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
  24. package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
  25. package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
  26. package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
  27. package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
  28. package/datasource-funifier-docs/knowledge/modules/folder.md +1011 -77
  29. package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
  30. package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
  31. package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
  32. package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
  33. package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
  34. package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
  35. package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
  36. package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
  37. package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
  38. package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
  39. package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
  40. package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
  41. package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
  42. package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
  43. package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
  44. package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
  45. package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
  46. package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
  47. package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
  48. package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
  49. package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
  50. package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
  51. package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
  52. package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
  53. package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
  54. package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
  55. package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
  56. package/dist/cli/init.d.ts.map +1 -1
  57. package/dist/cli/init.js +42 -1
  58. package/dist/cli/init.js.map +1 -1
  59. package/dist/cli/init.test.js +74 -3
  60. package/dist/cli/init.test.js.map +1 -1
  61. package/dist/cli/persona.d.ts +3 -0
  62. package/dist/cli/persona.d.ts.map +1 -0
  63. package/dist/cli/persona.js +25 -0
  64. package/dist/cli/persona.js.map +1 -0
  65. package/dist/core/api-client.d.ts +21 -1
  66. package/dist/core/api-client.d.ts.map +1 -1
  67. package/dist/core/api-client.js +154 -1
  68. package/dist/core/api-client.js.map +1 -1
  69. package/dist/core/constants.d.ts +14 -0
  70. package/dist/core/constants.d.ts.map +1 -1
  71. package/dist/core/constants.js +14 -0
  72. package/dist/core/constants.js.map +1 -1
  73. package/dist/core/types/Folder.d.ts +16 -0
  74. package/dist/core/types/Folder.d.ts.map +1 -0
  75. package/dist/core/types/Folder.js +3 -0
  76. package/dist/core/types/Folder.js.map +1 -0
  77. package/dist/core/types/FolderContent.d.ts +10 -0
  78. package/dist/core/types/FolderContent.d.ts.map +1 -0
  79. package/dist/core/types/FolderContent.js +3 -0
  80. package/dist/core/types/FolderContent.js.map +1 -0
  81. package/dist/core/types/FolderContentType.d.ts +10 -0
  82. package/dist/core/types/FolderContentType.d.ts.map +1 -0
  83. package/dist/core/types/FolderContentType.js +3 -0
  84. package/dist/core/types/FolderContentType.js.map +1 -0
  85. package/dist/core/types/FolderLog.d.ts +11 -0
  86. package/dist/core/types/FolderLog.d.ts.map +1 -0
  87. package/dist/core/types/FolderLog.js +3 -0
  88. package/dist/core/types/FolderLog.js.map +1 -0
  89. package/dist/core/types/index.d.ts +4 -0
  90. package/dist/core/types/index.d.ts.map +1 -1
  91. package/dist/core/types/index.js +4 -0
  92. package/dist/core/types/index.js.map +1 -1
  93. package/dist/mcp/bundle.js +121 -87
  94. package/dist/mcp/check-update.d.ts +2 -0
  95. package/dist/mcp/check-update.d.ts.map +1 -0
  96. package/dist/mcp/check-update.js +44 -0
  97. package/dist/mcp/check-update.js.map +1 -0
  98. package/dist/mcp/index.js +5 -2
  99. package/dist/mcp/index.js.map +1 -1
  100. package/dist/mcp/resources/documentation.d.ts +1 -1
  101. package/dist/mcp/resources/documentation.d.ts.map +1 -1
  102. package/dist/mcp/resources/documentation.js +39 -3
  103. package/dist/mcp/resources/documentation.js.map +1 -1
  104. package/dist/mcp/tools/_char-guard.js +1 -1
  105. package/dist/mcp/tools/_char-guard.js.map +1 -1
  106. package/dist/mcp/tools/_fetch-current.d.ts +1 -1
  107. package/dist/mcp/tools/_fetch-current.d.ts.map +1 -1
  108. package/dist/mcp/tools/_fetch-current.js +12 -0
  109. package/dist/mcp/tools/_fetch-current.js.map +1 -1
  110. package/dist/mcp/tools/connect.d.ts.map +1 -1
  111. package/dist/mcp/tools/connect.js +18 -8
  112. package/dist/mcp/tools/connect.js.map +1 -1
  113. package/dist/mcp/tools/database.d.ts.map +1 -1
  114. package/dist/mcp/tools/database.js +59 -47
  115. package/dist/mcp/tools/database.js.map +1 -1
  116. package/dist/mcp/tools/database.test.js +2 -2
  117. package/dist/mcp/tools/database.test.js.map +1 -1
  118. package/dist/mcp/tools/delete.d.ts.map +1 -1
  119. package/dist/mcp/tools/delete.js +33 -3
  120. package/dist/mcp/tools/delete.js.map +1 -1
  121. package/dist/mcp/tools/execute.d.ts.map +1 -1
  122. package/dist/mcp/tools/execute.js +20 -9
  123. package/dist/mcp/tools/execute.js.map +1 -1
  124. package/dist/mcp/tools/folder.d.ts +4 -0
  125. package/dist/mcp/tools/folder.d.ts.map +1 -0
  126. package/dist/mcp/tools/folder.js +68 -0
  127. package/dist/mcp/tools/folder.js.map +1 -0
  128. package/dist/mcp/tools/get.d.ts.map +1 -1
  129. package/dist/mcp/tools/get.js +16 -6
  130. package/dist/mcp/tools/get.js.map +1 -1
  131. package/dist/mcp/tools/index.d.ts +1 -1
  132. package/dist/mcp/tools/index.d.ts.map +1 -1
  133. package/dist/mcp/tools/index.js +5 -1
  134. package/dist/mcp/tools/index.js.map +1 -1
  135. package/dist/mcp/tools/list.d.ts.map +1 -1
  136. package/dist/mcp/tools/list.js +38 -14
  137. package/dist/mcp/tools/list.js.map +1 -1
  138. package/dist/mcp/tools/logs.d.ts.map +1 -1
  139. package/dist/mcp/tools/logs.js +15 -5
  140. package/dist/mcp/tools/logs.js.map +1 -1
  141. package/dist/mcp/tools/save.d.ts.map +1 -1
  142. package/dist/mcp/tools/save.js +26 -4
  143. package/dist/mcp/tools/save.js.map +1 -1
  144. package/dist/mcp/tools/save.test.js +192 -1
  145. package/dist/mcp/tools/save.test.js.map +1 -1
  146. package/dist/mcp/tools/search-docs.d.ts +3 -0
  147. package/dist/mcp/tools/search-docs.d.ts.map +1 -0
  148. package/dist/mcp/tools/search-docs.js +102 -0
  149. package/dist/mcp/tools/search-docs.js.map +1 -0
  150. package/package.json +6 -2
  151. package/skills/acquire-funifier-knowledge/SKILL.md +132 -0
  152. package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
  153. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
  154. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
  155. package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
  156. package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
  157. package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
  158. package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +47 -0
  159. package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
  160. package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
  161. package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
  162. package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
  163. package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
  164. package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
  165. package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
  166. package/skills/funifier/SKILL.md +88 -0
  167. package/skills/funifier/references/configure-security.md +96 -0
  168. package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
  169. package/skills/funifier/references/create-aggregate.md +144 -0
  170. package/skills/funifier/references/create-challenge.md +116 -0
  171. package/skills/funifier/references/create-competition.md +98 -0
  172. package/skills/funifier/references/create-crossword.md +574 -0
  173. package/skills/funifier/references/create-custom-object.md +91 -0
  174. package/skills/funifier/references/create-custom-page.md +135 -0
  175. package/skills/funifier/references/create-folder.md +104 -0
  176. package/skills/funifier/references/create-lastmile.md +643 -0
  177. package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
  178. package/skills/funifier/references/create-level.md +94 -0
  179. package/skills/funifier/references/create-lottery.md +913 -0
  180. package/skills/funifier/references/create-mystery.md +769 -0
  181. package/skills/funifier/references/create-notification.md +75 -0
  182. package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
  183. package/skills/funifier/references/create-quiz.md +98 -0
  184. package/skills/funifier/references/create-scheduler.md +141 -0
  185. package/skills/funifier/references/create-story.md +636 -0
  186. package/skills/funifier/references/create-swap.md +95 -0
  187. package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
  188. package/skills/funifier/references/create-virtual-good.md +96 -0
  189. package/skills/funifier/references/create-webhook.md +72 -0
  190. package/skills/funifier/references/create-websocket.md +71 -0
  191. package/skills/funifier/references/create-widget.md +76 -0
  192. package/skills/funifier/references/debug.md +87 -0
  193. package/skills/funifier/references/help.md +81 -0
  194. package/skills/funifier/references/implement-frontend.md +106 -0
  195. package/skills/funifier/references/import-csv.md +75 -0
  196. package/skills/funifier/references/manage-player.md +82 -0
  197. package/skills/funifier/references/manage-team.md +76 -0
  198. package/skills/funifier/references/upload-file.md +91 -0
  199. package/datasource-funifier-docs/.search-index.json +0 -17318
  200. package/datasource-funifier-docs/.skills-map.json +0 -73
  201. package/skills/funifier-create-aggregate/SKILL.md +0 -127
  202. package/skills/funifier-create-challenge/SKILL.md +0 -88
  203. package/skills/funifier-create-custom-page/SKILL.md +0 -127
  204. package/skills/funifier-create-level/SKILL.md +0 -87
  205. package/skills/funifier-create-quiz/SKILL.md +0 -87
  206. package/skills/funifier-create-scheduler/SKILL.md +0 -127
  207. package/skills/funifier-create-virtual-good/SKILL.md +0 -87
  208. package/skills/funifier-debug/SKILL.md +0 -92
  209. package/skills/funifier-help/SKILL.md +0 -86
  210. package/skills/funifier-implement-frontend/SKILL.md +0 -90
  211. package/skills/funifier-index/SKILL.md +0 -58
@@ -1,41 +1,474 @@
1
- # WebSocket (WebSocket)
1
+ # `websocket`
2
2
 
3
3
  **Acesso Studio:** `/studio/websocket`
4
4
  **API Endpoint:** `/v3/websocket`
5
+ **Endpoint WS (runtime):** `ws://<host>/ws/{socket}`
6
+ **Coleção MongoDB:** `websocket`
5
7
 
6
- ## O que é
8
+ ---
7
9
 
8
- Entrega de feedbacks em tempo real e interação instantânea entre jogadores. Permite configurar rotinas para enviar e receber informações instantaneamente entre jogadores e o sistema, sem precisar recarregar a página.
10
+ ## 1. Visão Geral
9
11
 
10
- ## Quando usar
12
+ O módulo `websocket` é um **motor de execução de scripts Groovy acionados por eventos de conexão WebSocket**. Cada documento `websocket` guarda um script Groovy que é compilado, cacheado em memória e executado quando um cliente abre, envia mensagem ou fecha uma conexão WebSocket cujo path aponta para o `_id` daquele documento.
11
13
 
12
- - Para comunicação em tempo real durante competições
13
- - Para feedback instantâneo ao completar desafios
14
- - Para chats ao vivo entre jogadores
15
- - Para atualização de status em tempo real
14
+ É um irmão mais simples do módulo [`trigger`](trigger.md): compartilha exatamente a mesma infraestrutura de sandbox (`SecureASTCustomizer` + `TriggerExpressionChecker`), o mesmo padrão de cache de classe compilada invalidado por `updated`, e o mesmo executor isolado com timeout. A diferença é o gatilho: em vez de eventos de ciclo de vida de entidades, o gatilho são os eventos de socket `onOpen` / `onMessage` / `onClose`.
16
15
 
17
- ## Checklist de Configuração no Studio
16
+ Papel arquitetural:
18
17
 
19
- - [ ] Configurar rotinas de websocket
20
- - [ ] Definir eventos que disparam mensagens
21
- - [ ] Configurar canais de comunicação
18
+ - Permite implementar lógica de comunicação em tempo real (chat, broadcast, notificação push para o navegador) sem recompilar o serviço.
19
+ - O endpoint físico WebSocket (`WebSocketServer`, JSR-356 `@ServerEndpoint("/ws/{socket}")`) recebe a conexão, resolve o tenant pela apiKey extraída do token e delega cada evento ao `WebSocketManager`.
20
+ - O `WebSocketManager` mantém **em memória** o mapa de sessões abertas por socket e o cache de classes Groovy compiladas.
21
+ - O envio efetivo de bytes ao navegador só acontece **de dentro do próprio script**, via o helper `broadcast(...).send()` (classe `Message`).
22
22
 
23
- ## API Endpoints
23
+ Relação com outros módulos:
24
24
 
25
- ### Listar WebSockets
26
- **Método:** GET
27
- **Endpoint:** `/v3/websocket`
25
+ - **`trigger`** — reutiliza `TriggerExpressionChecker` (mesma lista de classes/métodos proibidos no AST).
26
+ - **`ai`** — `POST /v3/ai/build/websocket` (`AIRest.buildWebSocket`) gera um rascunho de `WebSocket` via GPT-3.5. Ver seção 4 e seção 7 (contém bugs de copy-paste).
27
+ - **`auth`** — autenticação da conexão WS via token na querystring (`AuthBean`, `TokenUtil`).
28
28
 
29
- ### Criar WebSocket
30
- **Método:** POST
31
- **Endpoint:** `/v3/websocket`
29
+ > ⚠️ **Aviso de runtime, confirmado no código:** o registro programático do endpoint WebSocket está **comentado** em `Configuration.java:544-553`. Ver seção 7 e seção 8 — esta é a informação operacional mais importante deste módulo.
32
30
 
33
- ### Deletar WebSocket
34
- **Método:** DELETE
35
- **Endpoint:** `/v3/websocket/:id`
31
+ ---
36
32
 
37
- ## Validações e Testes
33
+ ## 2. Arquitetura e Fluxos
38
34
 
39
- - [ ] WebSocket aparece na lista
40
- - [ ] Mensagens são entregues em tempo real
41
- - [ ] Conexão se mantém estável
35
+ ### 2.1 Classes envolvidas
36
+
37
+ | Classe | Arquivo | Papel |
38
+ |---|---|---|
39
+ | `WebSocket` | `engine/websocket/WebSocket.java` | Entidade persistida — script + metadados. |
40
+ | `WebSocketManager` | `engine/websocket/WebSocketManager.java` | Orquestrador — CRUD, mapa de sessões, cache `compiled`/`dates`, compilação e execução. |
41
+ | `Message` | `engine/websocket/Message.java` | Helper de broadcast — filtra sessões por player e envia texto. |
42
+ | `WebSocketServer` | `ws/WebSocketServer.java` | Endpoint físico JSR-356 `@ServerEndpoint("/ws/{socket}")`. |
43
+ | `WebSocketRest` | `rest/v3/rest/WebSocketRest.java` | Endpoints REST `/v3/websocket` (CRUD da definição). |
44
+ | `TriggerExpressionChecker` | `engine/integration/trigger/TriggerExpressionChecker.java` | `SecureASTCustomizer.ExpressionChecker` reaproveitado do `trigger`. |
45
+ | `ManagerFactory` | `controller/ManagerFactory.java` | Instancia **um** `WebSocketManager` por tenant (`getWebSocketManager()`). |
46
+ | `Entity.WEBSOCKET` | `engine/util/Entity.java:242` | Mapeia a coleção `websocket` → `WebSocket.class`. |
47
+
48
+ ### 2.2 Pipeline de cadastro (REST `POST /v3/websocket`)
49
+
50
+ Sequência real (`WebSocketRest.insert`):
51
+
52
+ ```
53
+ 1. [Entrada] insert(authBean, obj) — obj == null → retorna {} com HTTP 201 e nada acontece
54
+ 2. [Resolve] getManagerFactory().getWebSocketManager()
55
+ 3. [Monta script] manager.getScript(obj) — embrulha obj.script na classe FunifierWebSocket
56
+ 4. [Valida] manager.compile(script) — compila com SecureASTCustomizer
57
+ ├─ CompilationFailedException → result = {websocket, status:"ERROR", message} → HTTP 201
58
+ └─ OK →
59
+ 5. [Persiste] manager.save(obj) — upsert por _id na coleção `websocket`
60
+ 6. [Resposta] result = {websocket, status:"OK"} → HTTP 201
61
+ ```
62
+
63
+ Pontos não-convencionais (ver seção 4):
64
+ - O HTTP **201 é retornado mesmo quando a compilação falha** — o campo `status` no corpo é o sinal real de sucesso/erro.
65
+ - `compile()` é chamado **antes** de `save()`: um script inválido nunca é persistido.
66
+
67
+ ### 2.3 Pipeline de execução de evento (runtime WS)
68
+
69
+ Disparado por `WebSocketServer.onOpen/onMessage/onClose` → `WebSocketManager.onOpen/onMessage/onClose` → `run(...)` → `executor(...)`.
70
+
71
+ Pseudocódigo de `WebSocketManager.run`:
72
+
73
+ ```
74
+ se websocket.updated == null:
75
+ save(websocket) # força criação e define updated
76
+ lastCompilation = dates[websocket._id]
77
+ clazz = compiled[websocket._id]
78
+ se clazz != null E lastCompilation != null E lastCompilation > websocket.updated:
79
+ executor(clazz, ...) # reaproveita classe em cache
80
+ senão:
81
+ script = getScript(websocket)
82
+ clazz = compile(script)
83
+ compiled[websocket._id] = clazz
84
+ dates[websocket._id] = agora # marca data da compilação
85
+ executor(clazz, ...)
86
+ ```
87
+
88
+ ### Fluxo de compilação e cache — `WebSocketManager.run`
89
+
90
+ ```mermaid
91
+ flowchart TD
92
+ A[Evento de socket] --> B{websocket.updated == null?}
93
+ B -- sim --> C[save: define updated]
94
+ B -- nao --> D{cache valido?<br/>lastCompilation > updated}
95
+ C --> D
96
+ D -- sim --> E[reutiliza classe em compiled]
97
+ D -- nao --> F[getScript + compile]
98
+ F --> G[guarda em compiled e dates]
99
+ G --> H[executor]
100
+ E --> H[executor: thread isolada + timeout]
101
+ H --> I{metodo}
102
+ I -- onOpen --> J[script.onOpen session, config]
103
+ I -- onMessage --> K[script.onMessage session, message]
104
+ I -- onClose --> L[script.onClose session, reason]
105
+ ```
106
+
107
+ `WebSocketManager.executor` (síncrono, bloqueante por evento):
108
+
109
+ 1. Cria `Executors.newSingleThreadExecutor()`.
110
+ 2. `timeout = websocket.timeout` se `> 0`, senão `30` segundos.
111
+ 3. Instancia a classe compilada (`clazz.newInstance()`).
112
+ 4. Injeta `setId`, `setContext`, `setManager`, `setSessions` no objeto Groovy.
113
+ 5. Despacha **apenas** `onOpen` | `onMessage` | `onClose` conforme o evento (⚠️ `onError` não é despachado — seção 7).
114
+ 6. Chama `addAllOutput` para coletar os `println` do script.
115
+ 7. `future.get(timeout, SECONDS)` — se estourar, `future.cancel(true)` + `executor.shutdownNow()`.
116
+ 8. Qualquer exceção (compilação, segurança, timeout, execução) é coletada na lista `exceptions` — que é **descartada** pelos chamadores `onOpen/onMessage/onClose` (não há retorno ao cliente). Falhas de script são, na prática, silenciosas em runtime (no máximo um `printStackTrace`).
117
+
118
+ ### 2.4 Interação entre componentes (push para o navegador)
119
+
120
+ ```mermaid
121
+ sequenceDiagram
122
+ participant B as Browser
123
+ participant S as WebSocketServer<br/>(/ws/{socket})
124
+ participant M as WebSocketManager
125
+ participant G as Script Groovy<br/>(FunifierWebSocket)
126
+ participant Msg as Message
127
+ B->>S: WS connect ?authorization=Bearer&player=alice
128
+ S->>S: getApiKey(token) / getPlayer(token|?player)
129
+ S->>S: session.userProperties["id"] = player
130
+ S->>M: onOpen(socket, session, config)
131
+ M->>M: getSessions(socket).add(session) (via script)
132
+ B->>S: send("texto")
133
+ S->>M: onMessage(socket, session, "texto")
134
+ M->>G: onMessage(session, message)
135
+ G->>Msg: broadcast(message).to("alice").send()
136
+ Msg->>M: getSessions(socket)
137
+ Msg-->>B: session.getAsyncRemote().sendText(message)
138
+ ```
139
+
140
+ > O `WebSocketManager.send(socket, message)` **não** envia diretamente — ele delega para `onMessage(socket, null, message)`, ou seja, re-executa o script do socket com `session = null`. O laço de broadcast direto está comentado no código. O único caminho que efetivamente escreve bytes no cliente é `Message.send()`, invocado de dentro do script via `broadcast(...)`.
141
+
142
+ ---
143
+
144
+ ## 3. Estrutura dos Objetos
145
+
146
+ ### 3.1 `WebSocket` — documento raiz
147
+
148
+ Coleção `websocket`. Anotada com `@JsonIgnoreProperties(ignoreUnknown=true)` → **qualquer campo não listado abaixo é silenciosamente descartado na desserialização**.
149
+
150
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
151
+ |---|---|---|---|---|
152
+ | `_id` | String | auto (`Guid.shortTimeMillis()` se vazio/branco) | — | Identificador. **É usado como `{socket}` na URL** `ws://<host>/ws/{_id}`. Defina um valor legível (ex. `reward`, `chat`). |
153
+ | `title` | String | — | não | Nome de exibição no Studio. **Não usado em runtime.** |
154
+ | `description` | String | — | não | Descrição. **Não usado em runtime.** |
155
+ | `script` | String | — | sim (de fato) | Código Groovy com os métodos `onOpen` / `onMessage` / `onClose`. Sem ele, a compilação gera uma classe vazia e nenhum evento faz nada. |
156
+ | `timeout` | Long | 30 (fallback no `executor`) | não | Timeout em **segundos** do executor por evento. Ver dupla-camada de timeout abaixo. |
157
+ | `updated` | Date | auto (`new Date()` a cada `save`) | — | Timestamp da última gravação **e** invalidador de cache cross-cluster (ver 3.2). |
158
+
159
+ #### Campos aceitos e silenciosamente ignorados
160
+
161
+ - `aggregate`, `collection` — **não existem** na classe `WebSocket`. São sugeridos pelo prompt do builder de IA (`AIRest.buildWebSocket`) por copy-paste do builder de *find/aggregate*, e descartados por `@JsonIgnoreProperties` ao desserializar a resposta do GPT. Ver seção 7.
162
+ - Qualquer outro campo enviado no POST é descartado sem erro.
163
+
164
+ #### Campos sem persistência (apenas runtime, injetados no script)
165
+
166
+ Não ficam no documento — são variáveis disponíveis **dentro** do script Groovy (ver 3.3): `_id`, `sessions`, `context`, `manager`, `output`.
167
+
168
+ ### 3.2 Semântica de `updated` (cache cross-cluster)
169
+
170
+ `updated` tem papel duplo:
171
+
172
+ 1. Timestamp de auditoria da última gravação.
173
+ 2. **Invalidador de cache distribuído.** Cada nó do cluster mantém seu próprio `dates[_id]` (data da última compilação local). A condição de reuso de cache é `lastCompilation > websocket.updated`. Quando o nó A salva o script (`save()` atualiza `updated`), o nó B — ao reler o documento do Mongo no próximo evento — verá `updated > lastCompilation` local e **recompilará**, propagando a nova versão sem reinício. Os comentários `//cluster:` no código confirmam esse mecanismo.
174
+
175
+ ### 3.3 Classe gerada `FunifierWebSocket` (runtime)
176
+
177
+ `WebSocketManager.getScript()` embrulha o `script` do usuário numa classe Groovy anotada com `@TimedInterrupt(value = 30L, unit = TimeUnit.SECONDS)` e com um amplo conjunto de imports (HTTP/Unirest, e-mail, Thumbnailator, entidades Funifier, `ManagerFactory`, classes `javax.websocket.*`, `Message`). Helpers injetados disponíveis para o script:
178
+
179
+ | Símbolo | Tipo | Significado |
180
+ |---|---|---|
181
+ | `_id` | String | Id do socket atual. |
182
+ | `broadcast(String message)` | `Message` | Cria um `Message` ligado a este socket — encadeie `.to(...)` / `.tos(...)` e finalize com `.send()`. |
183
+ | `sessions` | `Set<Session>` | Sessões abertas deste socket (em memória, neste nó). |
184
+ | `context` | `HashMap` | Mapa **efêmero por evento** — alocado novo a cada `onOpen`/`onMessage`/`onClose`. **Não persiste entre eventos.** |
185
+ | `manager` | `ManagerFactory` | Acesso a todos os managers Funifier (player, action, etc.). |
186
+ | `println(msg)` | void (override) | Não imprime no stdout — acumula em `output` (coletado em `addAllOutput`, mas descartado pelos chamadores). |
187
+
188
+ ### 3.4 `Message` — helper de broadcast
189
+
190
+ | Método | Efeito |
191
+ |---|---|
192
+ | `to(String player)` | Adiciona um player à lista de destinatários (ignora null/branco). |
193
+ | `tos(List<String> players)` | Adiciona vários players. |
194
+ | `send()` | Se a lista `to` estiver preenchida, envia **apenas** às sessões cujo `session.getUserProperties().get("id")` casa com um player da lista; caso contrário, faz **broadcast a todas** as sessões do socket. Envio via `getAsyncRemote().sendText(message)`. |
195
+
196
+ > O filtro por player só funciona se o cliente conectou com `player=<id>` na querystring **ou** com um token cujo claim `player` seja `<id>` (definido em `WebSocketServer.onOpen`).
197
+
198
+ ---
199
+
200
+ ## 4. Endpoints
201
+
202
+ ### `POST /v3/websocket` — criar ou substituir (upsert)
203
+
204
+ | Aspecto | Detalhe |
205
+ |---|---|
206
+ | Finalidade | Compila e persiste a definição do websocket. |
207
+ | Autenticação | `@BeanParam AuthBean` (Basic/Bearer). |
208
+ | Full replace ou patch | **Full replace / upsert por `_id`** (`col.save(obj)` do Jongo). Não há merge de campos. |
209
+ | Código HTTP | Sempre `201 CREATED`, inclusive em erro de compilação. |
210
+
211
+ **Comportamento real:**
212
+ - `obj == null` → resposta `{}` (corpo vazio após remoção de nulls) com HTTP 201; nada é persistido.
213
+ - Compila **antes** de salvar. Em `CompilationFailedException`, retorna `status:"ERROR"` + `message` e **não salva**.
214
+ - `_id` ausente/branco → gerado por `Guid.shortTimeMillis()` (string base-encoded de `System.currentTimeMillis()`).
215
+ - `updated` é sempre sobrescrito com `new Date()`.
216
+
217
+ **Request:**
218
+
219
+ ```json
220
+ {
221
+ "_id": "chat",
222
+ "title": "Chat ao vivo",
223
+ "description": "Canal de chat da competição",
224
+ "timeout": 30,
225
+ "script": "void onMessage(session, message) {\n broadcast(message).send();\n}"
226
+ }
227
+ ```
228
+
229
+ **Response (sucesso):**
230
+
231
+ ```json
232
+ {
233
+ "websocket": {
234
+ "_id": "chat",
235
+ "title": "Chat ao vivo",
236
+ "description": "Canal de chat da competição",
237
+ "timeout": 30,
238
+ "script": "void onMessage(session, message) { broadcast(message).send(); }",
239
+ "updated": { "$date": "2026-05-20T12:00:00.000Z" }
240
+ },
241
+ "status": "OK"
242
+ }
243
+ ```
244
+
245
+ **Response (erro de compilação) — ainda HTTP 201:**
246
+
247
+ ```json
248
+ {
249
+ "websocket": { "_id": "chat", "script": "void onMessage(... }" },
250
+ "status": "ERROR",
251
+ "message": "startup failed: Script1.groovy: 1: unexpected token ..."
252
+ }
253
+ ```
254
+
255
+ ### `GET /v3/websocket/{id}` — buscar uma definição
256
+
257
+ | Aspecto | Detalhe |
258
+ |---|---|
259
+ | Finalidade | Retorna o documento `WebSocket` por `_id`. |
260
+ | Autenticação | `@BeanParam AuthBean`. |
261
+ | Não encontrado | `find()` faz `findOne("{_id:#}", id)`; se não existir, o corpo serializado é vazio/nulo, HTTP 200. |
262
+
263
+ ```
264
+ GET /v3/websocket/chat
265
+ ```
266
+
267
+ ### `DELETE /v3/websocket/{id}` — remover
268
+
269
+ | Aspecto | Detalhe |
270
+ |---|---|
271
+ | Finalidade | Remove o documento e limpa o estado em memória. |
272
+ | Autenticação | `@BeanParam AuthBean`. |
273
+ | Código HTTP | `204 NO_CONTENT`. |
274
+
275
+ Efeitos: `remove("{_id:#}", id)` na coleção, `compiled.remove(id)`, `dates.remove(id)`, `sessions.remove(id)` (descarta o mapa de sessões em memória deste nó).
276
+
277
+ ### `POST /v3/ai/build/websocket` — rascunho via IA (não persiste)
278
+
279
+ | Aspecto | Detalhe |
280
+ |---|---|
281
+ | Finalidade | Usa GPT-3.5 (function calling `build_websocket`) para gerar um rascunho de `WebSocket` a partir de uma descrição em linguagem natural. |
282
+ | Autenticação | `@BeanParam AuthBean`. |
283
+ | Persistência | **Nenhuma.** Retorna um objeto `WebSocket` (HTTP 200) que o cliente deve então enviar ao `POST /v3/websocket`. |
284
+
285
+ **Request:** `{ "content": "um chat onde a mensagem vai só para o player citado" }`
286
+
287
+ > ⚠️ Contém dois bugs de copy-paste confirmados no código — ver seção 7.
288
+
289
+ ### Endpoint WebSocket (runtime) — `ws://<host>/ws/{socket}`
290
+
291
+ JSR-356 `@ServerEndpoint("/ws/{socket}")` em `WebSocketServer`. `{socket}` = `_id` do documento `websocket`.
292
+
293
+ **Query params da conexão:**
294
+
295
+ | Param | Tipo | Descrição |
296
+ |---|---|---|
297
+ | `authorization` | String | `Bearer <token>`. Usado para extrair a apiKey (`AuthBean.getApiKey`) → resolve o tenant/banco, e o claim `player` (`TokenUtil.getValue(..., "player")`). |
298
+ | `player` | String | Fallback do id do player, usado **apenas** se o token não trouxer o claim `player`. |
299
+
300
+ No `onOpen`, se um player for resolvido, `session.getUserProperties().put("id", player)` — é esse valor que o `Message.to(...)` usa para filtrar destinatários.
301
+
302
+ ```
303
+ ws://service.funifier.com/ws/chat?authorization=Bearer%20<JWT>&player=alice
304
+ ```
305
+
306
+ ---
307
+
308
+ ## 5. Regras de Negócio
309
+
310
+ Regras presentes no código (não no schema):
311
+
312
+ - **Compilação obrigatória no cadastro** — script inválido nunca chega ao banco (mas a resposta ainda é 201; cheque `status`).
313
+ - **Isolamento por tenant via apiKey** — não há campo de organização no documento. O isolamento vem do roteamento `FrontController.getInstance(apiKey).getManagerFactory()`: cada apiKey resolve uma conexão/banco Mongo distinto. O `find(socket)` ocorre no banco do tenant dono do token usado na conexão WS.
314
+ - **Dupla camada de timeout** — (1) `@TimedInterrupt(value = 30L, SECONDS)` é injetado **hardcoded** na classe gerada por `getScript()` e instrumenta laços/iterações em nível de AST; (2) `websocket.timeout` (ou 30s) limita o `future.get(...)` do executor. Definir `timeout > 30` **não** contorna o `@TimedInterrupt` de 30s para laços apertados.
315
+ - **Cache invalidado por gravação** — salvar uma definição (`save()`) atualiza `updated` e remove a classe do `compiled` deste nó; demais nós recompilam quando detectam `updated > lastCompilation` (ver 3.2).
316
+ - **Sessões em memória** — o mapa `sessions` é um `HashMap` simples no `WebSocketManager`, vivo apenas no processo. Ver limitação de cluster na seção 7/8.
317
+ - **Execução síncrona e bloqueante por evento** — cada evento roda em uma thread dedicada e o `WebSocketServer` aguarda o resultado.
318
+
319
+ ---
320
+
321
+ ## 6. Comportamentos Automáticos
322
+
323
+ | Comportamento | Trigger | Impacto | Persistência |
324
+ |---|---|---|---|
325
+ | Geração de `_id` | `save()` com `_id` vazio/branco | `Guid.shortTimeMillis()` define o id (que vira a URL do socket) | Sim (`websocket`) |
326
+ | Atualização de `updated` | Todo `save()` | Marca timestamp e invalida cache cross-cluster | Sim |
327
+ | Invalidação de cache local | `save()` e `delete()` | `compiled.remove(_id)` (e `dates`/`sessions` no delete) | Memória (por nó) |
328
+ | Auto-criação no 1º evento | `run()` com `websocket.updated == null` | Chama `save()` antes de executar — persiste a definição que ainda não tinha sido gravada | Sim |
329
+ | Recompilação automática | `run()` quando cache inválido | Recompila o script e atualiza `dates[_id]` | Memória |
330
+ | `context` efêmero | Cada `onOpen`/`onMessage`/`onClose` | `HashMap` novo por evento; não há estado entre eventos | Não |
331
+ | Coleta de `println` | `executor` → `addAllOutput` | Saídas do script acumuladas em lista e **descartadas** pelos chamadores | Não |
332
+
333
+ ---
334
+
335
+ ## 7. Suportado vs NÃO Suportado
336
+
337
+ ### ✅ Suportado
338
+
339
+ - CRUD da definição: `POST /v3/websocket` (upsert), `GET /v3/websocket/{id}`, `DELETE /v3/websocket/{id}`.
340
+ - Eventos de script: `onOpen(session, config)`, `onMessage(session, message)`, `onClose(session, reason)`.
341
+ - Broadcast a todas as sessões do socket (`broadcast(msg).send()`).
342
+ - Envio direcionado por player (`broadcast(msg).to("alice").send()` / `.tos([...])`).
343
+ - Acesso aos managers Funifier dentro do script (`manager.getPlayerManager()`, etc.).
344
+ - Geração de rascunho por IA (`POST /v3/ai/build/websocket`).
345
+ - Sandbox de segurança herdado do `trigger` (ver seção 8).
346
+ - Pong automático em mensagens `PongMessage` (`WebSocketServer.onPong`).
347
+
348
+ ### ❌ NÃO Suportado
349
+
350
+ - **`GET /v3/websocket` (listagem)** — **não existe**. Não há endpoint de lista; só busca por `{id}`. (A documentação anterior afirmava o contrário — estava incorreta.)
351
+ - **PATCH / update parcial** — `POST` é sempre full replace/upsert; não há merge de campos.
352
+ - **Evento `onError` no script** — `executor` despacha **apenas** `onOpen`/`onMessage`/`onClose`. O `WebSocketServer.onError` só faz `printStackTrace()`. O comentário em `WebSocket.java:19` ("onOpen, onMessage, onClose, orError") é enganoso: um método `onError` no script **nunca** é chamado.
353
+ - **Campos `aggregate` e `collection`** — sugeridos pelo prompt do builder de IA, mas **inexistentes** na entidade; descartados silenciosamente por `@JsonIgnoreProperties`.
354
+ - **Broadcast cross-cluster** — `sessions` é um `HashMap` em memória por JVM. Um `broadcast` no nó A **não alcança** sessões conectadas ao nó B. Limitação arquitetural confirmada (não há backplane/pub-sub).
355
+ - **Push a partir de outros módulos** — nenhum manager fora de `engine/websocket` constrói `Message` ou chama `getWebSocketManager().send/onMessage` (verificado por grep no `src/main/java`). O único caminho de push é de dentro do próprio script do socket.
356
+ - **Estado persistente entre eventos** — `context` é recriado a cada evento; não há sessão de estado server-side além do `Set<Session>`.
357
+ - **Helpers `socket()` / `message()` no `WebSocketManager`** — comentados (deprecated).
358
+ - **Registro programático do endpoint WS** — `serverContainer.addEndpoint(WebSocketServer.class)` está **comentado** em `Configuration.java:544-553`. O endpoint `/ws/{socket}` depende inteiramente do auto-scan de anotações do container servlet; se o container não escanear `@ServerEndpoint`, o WebSocket **não sobe**. Confirme a ativação no ambiente antes de assumir que o módulo está operacional.
359
+
360
+ ---
361
+
362
+ ## 8. Segurança e Permissões
363
+
364
+ - **Autenticação REST** — endpoints `/v3/websocket` usam `@BeanParam AuthBean` (Basic/Bearer), como o restante da API v3.
365
+ - **Autenticação WS** — feita pela querystring: `authorization=Bearer <token>` → `AuthBean.getApiKey` resolve o tenant; o claim `player` (ou `?player=`) identifica o usuário. Não há challenge handshake adicional — quem tiver o token entra.
366
+ - **Isolamento multi-tenant** — garantido pelo roteamento por apiKey no `FrontController` (banco Mongo distinto por tenant). O documento `websocket` não carrega campo de organização.
367
+ - **Sandbox de execução (Groovy)** — `compile()` usa `SecureASTCustomizer` + `TriggerExpressionChecker` (compartilhado com `trigger`):
368
+ - **Tipos bloqueados:** `System`, `ProcessBuilder`, `File`, `GroovyShell`, `GroovyObject`.
369
+ - **Textos de expressão bloqueados:** `.execute`, `.getDB`, `.getMongo`, `.dropDatabase`.
370
+ - **Whitelist de tokens** aritméticos/lógicos/comparação e colchetes (ver `compile()`).
371
+ - Limite de tempo via `@TimedInterrupt` (30s, hardcoded) + timeout do executor.
372
+ - **Superfícies de risco confirmadas no código:**
373
+ - O sandbox é **parcial** (igual ao `trigger`): a lista de classes proibidas é curta e baseada em correspondência textual da expressão (`indexOf`), contornável por reflexão indireta/aliases. Não trate scripts de fontes não confiáveis como seguros.
374
+ - O script tem acesso ao `ManagerFactory` completo do tenant — pode ler/alterar qualquer entidade da gamificação daquele tenant.
375
+ - A apiKey e o player são **logados em stdout** (`System.out.println`) em cada evento (`WebSocketServer`). Evite logs em ambiente compartilhado / cuidado com vazamento.
376
+ - Falhas de script são silenciosas para o cliente (exceções coletadas e descartadas) — dificulta detecção de abuso/erro.
377
+
378
+ ---
379
+
380
+ ## 9. Observabilidade e Troubleshooting
381
+
382
+ ### Como verificar se o módulo está funcionando
383
+
384
+ 1. **A definição existe?**
385
+ ```
386
+ GET /v3/websocket/<socket>
387
+ ```
388
+ Corpo vazio = documento inexistente (crie via POST).
389
+
390
+ 2. **O script compila?** Reenvie via `POST /v3/websocket` e cheque `status`:
391
+ - `status: "OK"` → compilou e foi salvo.
392
+ - `status: "ERROR"` → veja `message` (erro de compilação Groovy). **Lembre:** HTTP é 201 nos dois casos.
393
+
394
+ 3. **O endpoint WS está ativo?** Tente conectar `ws://<host>/ws/<socket>?authorization=Bearer <token>`. Se a conexão não estabelece, verifique primeiro se o `@ServerEndpoint` foi registrado pelo container — o registro programático está comentado (`Configuration.java`).
395
+
396
+ ### Queries úteis (Mongo / coleção `websocket`)
397
+
398
+ ```
399
+ // listar todos os sockets do tenant (não há endpoint REST de lista)
400
+ db.websocket.find({}, {title:1, updated:1})
401
+
402
+ // inspecionar um script
403
+ db.websocket.findOne({_id: "chat"})
404
+ ```
405
+
406
+ ### Erros comuns e causas
407
+
408
+ | Sintoma | Causa provável |
409
+ |---|---|
410
+ | Conexão WS recusada / 404 | Endpoint não registrado (registro comentado em `Configuration.java`); container não fez auto-scan. |
411
+ | `status: "ERROR"` no POST | Sintaxe Groovy inválida ou uso de classe/expressão bloqueada pelo sandbox. |
412
+ | Mensagem não chega ao destinatário | Player do destinatário não bateu (`to("x")` exige conexão com `player=x` ou claim `player=x`). |
413
+ | Mensagem não chega em ambiente com vários nós | Destinatário conectado a outro nó do cluster (sessões são por JVM). |
414
+ | Script "trava" e nada acontece | Timeout do executor/`@TimedInterrupt` (30s) abateu a execução; exceção foi descartada silenciosamente. |
415
+ | `onError` no script nunca executa | `onError` não é despachado — comportamento esperado (seção 7). |
416
+ | Campos `aggregate`/`collection` "somem" | Descartados por `@JsonIgnoreProperties` — não existem na entidade. |
417
+
418
+ ---
419
+
420
+ ## 10. Exemplos Práticos
421
+
422
+ ### 10.1 Mínimo funcional — eco/broadcast
423
+
424
+ ```json
425
+ {
426
+ "_id": "echo",
427
+ "title": "Echo",
428
+ "script": "void onMessage(session, message) {\n broadcast(message).send();\n}"
429
+ }
430
+ ```
431
+
432
+ Toda mensagem recebida é reenviada a todas as sessões conectadas em `/ws/echo`.
433
+
434
+ ### 10.2 Avançado — mensagem direcionada por player + uso de manager
435
+
436
+ ```json
437
+ {
438
+ "_id": "reward",
439
+ "title": "Notificação de recompensa",
440
+ "description": "Avisa um player específico quando algo acontece",
441
+ "timeout": 30,
442
+ "script": "void onMessage(session, message) {\n def msg = JsonUtil.fromJson(message, HashMap.class);\n String player = msg.get(\"player\");\n Player p = manager.getPlayerManager().findById(player);\n String payload = JsonUtil.toJson([player: player, name: p?.name, text: msg.get(\"text\")]);\n broadcast(payload).to(player).send();\n}"
443
+ }
444
+ ```
445
+
446
+ A mensagem só chega à(s) sessão(ões) cujo `player` casa com o id informado (exige conexão com `?player=<id>` ou claim `player`).
447
+
448
+ ### 10.3 Anti-pattern — o que NÃO fazer
449
+
450
+ ```json
451
+ {
452
+ "_id": "ruim",
453
+ "script": "void onOpen(session, config) {\n context.put(\"count\", 0);\n}\nvoid onMessage(session, message) {\n context.count = context.count + 1; // NUNCA persiste entre eventos\n broadcast(\"total: \" + context.count).send();\n}\nvoid onError(session, t) {\n broadcast(\"erro\").send(); // NUNCA é chamado\n}"
454
+ }
455
+ ```
456
+
457
+ Por que é errado:
458
+ - `context` é recriado a cada evento — `count` sempre será `null`/0 no `onMessage`. Para contadores, use o `manager`/banco.
459
+ - `onError` **não é despachado** — esse método é código morto.
460
+ - Confiar em broadcast global em produção multi-nó: só atinge sessões do nó local.
461
+
462
+ ---
463
+
464
+ ## Checklist de Configuração
465
+
466
+ - [ ] `_id` definido com valor legível e estável (vira a URL `/ws/{_id}`) — se omitido, vira um hash gerado.
467
+ - [ ] `script` contém ao menos um de `onOpen` / `onMessage` / `onClose` (não use `onError` — não é despachado).
468
+ - [ ] POST retornou `status: "OK"` (não confie só no HTTP 201).
469
+ - [ ] Para envio direcionado, confirme que os clientes conectam com `player=<id>` ou token com claim `player`.
470
+ - [ ] `timeout` ≤ 30s é efetivo; valores maiores não contornam o `@TimedInterrupt` de 30s para laços.
471
+ - [ ] **Armadilha:** `context` não persiste entre eventos — use o banco/manager para estado.
472
+ - [ ] **Armadilha:** `aggregate`/`collection` (vindos do builder de IA) são ignorados — remova antes de salvar.
473
+ - [ ] **Pré-requisito de ambiente:** confirme que o `@ServerEndpoint` `/ws/{socket}` está de fato ativo (registro programático comentado em `Configuration.java`).
474
+ - [ ] Em deploy com múltiplos nós, lembre que broadcast não cruza o cluster.