funifier-mcp 0.2.26 → 0.2.28

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 (181) 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 +4 -1
  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 +935 -280
  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/mcp/bundle.js +119 -93
  66. package/dist/mcp/check-update.d.ts +5 -0
  67. package/dist/mcp/check-update.d.ts.map +1 -1
  68. package/dist/mcp/check-update.js +21 -10
  69. package/dist/mcp/check-update.js.map +1 -1
  70. package/dist/mcp/check-update.test.d.ts +2 -0
  71. package/dist/mcp/check-update.test.d.ts.map +1 -0
  72. package/dist/mcp/check-update.test.js +33 -0
  73. package/dist/mcp/check-update.test.js.map +1 -0
  74. package/dist/mcp/index.js +2 -2
  75. package/dist/mcp/index.js.map +1 -1
  76. package/dist/mcp/prompts/templates.d.ts.map +1 -1
  77. package/dist/mcp/prompts/templates.js +35 -0
  78. package/dist/mcp/prompts/templates.js.map +1 -1
  79. package/dist/mcp/resources/documentation.d.ts +1 -1
  80. package/dist/mcp/resources/documentation.d.ts.map +1 -1
  81. package/dist/mcp/resources/documentation.js +39 -3
  82. package/dist/mcp/resources/documentation.js.map +1 -1
  83. package/dist/mcp/tools/connect.d.ts.map +1 -1
  84. package/dist/mcp/tools/connect.js +18 -8
  85. package/dist/mcp/tools/connect.js.map +1 -1
  86. package/dist/mcp/tools/database.d.ts.map +1 -1
  87. package/dist/mcp/tools/database.js +59 -47
  88. package/dist/mcp/tools/database.js.map +1 -1
  89. package/dist/mcp/tools/database.test.js +2 -2
  90. package/dist/mcp/tools/database.test.js.map +1 -1
  91. package/dist/mcp/tools/delete.d.ts.map +1 -1
  92. package/dist/mcp/tools/delete.js +13 -3
  93. package/dist/mcp/tools/delete.js.map +1 -1
  94. package/dist/mcp/tools/execute.d.ts.map +1 -1
  95. package/dist/mcp/tools/execute.js +20 -9
  96. package/dist/mcp/tools/execute.js.map +1 -1
  97. package/dist/mcp/tools/folder.d.ts.map +1 -1
  98. package/dist/mcp/tools/folder.js +22 -12
  99. package/dist/mcp/tools/folder.js.map +1 -1
  100. package/dist/mcp/tools/get.d.ts.map +1 -1
  101. package/dist/mcp/tools/get.js +16 -6
  102. package/dist/mcp/tools/get.js.map +1 -1
  103. package/dist/mcp/tools/index.d.ts +1 -1
  104. package/dist/mcp/tools/index.d.ts.map +1 -1
  105. package/dist/mcp/tools/index.js +28 -1
  106. package/dist/mcp/tools/index.js.map +1 -1
  107. package/dist/mcp/tools/list.d.ts.map +1 -1
  108. package/dist/mcp/tools/list.js +38 -14
  109. package/dist/mcp/tools/list.js.map +1 -1
  110. package/dist/mcp/tools/logs.d.ts.map +1 -1
  111. package/dist/mcp/tools/logs.js +15 -5
  112. package/dist/mcp/tools/logs.js.map +1 -1
  113. package/dist/mcp/tools/save.d.ts.map +1 -1
  114. package/dist/mcp/tools/save.js +14 -4
  115. package/dist/mcp/tools/save.js.map +1 -1
  116. package/dist/mcp/tools/save.test.js +3 -3
  117. package/dist/mcp/tools/save.test.js.map +1 -1
  118. package/dist/mcp/tools/search-docs.d.ts +3 -0
  119. package/dist/mcp/tools/search-docs.d.ts.map +1 -0
  120. package/dist/mcp/tools/search-docs.js +102 -0
  121. package/dist/mcp/tools/search-docs.js.map +1 -0
  122. package/package.json +6 -2
  123. package/skills/acquire-funifier-knowledge/SKILL.md +155 -0
  124. package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
  125. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
  126. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
  127. package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
  128. package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
  129. package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
  130. package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +86 -0
  131. package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
  132. package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
  133. package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
  134. package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
  135. package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
  136. package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
  137. package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
  138. package/skills/funifier/SKILL.md +88 -0
  139. package/skills/funifier/references/configure-security.md +96 -0
  140. package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
  141. package/skills/funifier/references/create-aggregate.md +144 -0
  142. package/skills/funifier/references/create-challenge.md +116 -0
  143. package/skills/funifier/references/create-competition.md +98 -0
  144. package/skills/funifier/references/create-crossword.md +574 -0
  145. package/skills/funifier/references/create-custom-object.md +91 -0
  146. package/skills/funifier/references/create-custom-page.md +135 -0
  147. package/skills/funifier/references/create-folder.md +104 -0
  148. package/skills/funifier/references/create-lastmile.md +643 -0
  149. package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
  150. package/skills/funifier/references/create-level.md +94 -0
  151. package/skills/funifier/references/create-lottery.md +913 -0
  152. package/skills/funifier/references/create-mystery.md +769 -0
  153. package/skills/funifier/references/create-notification.md +75 -0
  154. package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
  155. package/skills/funifier/references/create-quiz.md +98 -0
  156. package/skills/funifier/references/create-scheduler.md +141 -0
  157. package/skills/funifier/references/create-story.md +636 -0
  158. package/skills/funifier/references/create-swap.md +95 -0
  159. package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
  160. package/skills/funifier/references/create-virtual-good.md +96 -0
  161. package/skills/funifier/references/create-webhook.md +72 -0
  162. package/skills/funifier/references/create-websocket.md +71 -0
  163. package/skills/funifier/references/create-widget.md +76 -0
  164. package/skills/funifier/references/debug.md +87 -0
  165. package/skills/funifier/references/help.md +81 -0
  166. package/skills/funifier/references/implement-frontend.md +106 -0
  167. package/skills/funifier/references/import-csv.md +75 -0
  168. package/skills/funifier/references/manage-player.md +82 -0
  169. package/skills/funifier/references/manage-team.md +76 -0
  170. package/skills/funifier/references/upload-file.md +91 -0
  171. package/skills/funifier-create-aggregate/SKILL.md +0 -127
  172. package/skills/funifier-create-challenge/SKILL.md +0 -88
  173. package/skills/funifier-create-custom-page/SKILL.md +0 -127
  174. package/skills/funifier-create-level/SKILL.md +0 -87
  175. package/skills/funifier-create-quiz/SKILL.md +0 -87
  176. package/skills/funifier-create-scheduler/SKILL.md +0 -127
  177. package/skills/funifier-create-virtual-good/SKILL.md +0 -87
  178. package/skills/funifier-debug/SKILL.md +0 -92
  179. package/skills/funifier-help/SKILL.md +0 -86
  180. package/skills/funifier-implement-frontend/SKILL.md +0 -90
  181. package/skills/funifier-index/SKILL.md +0 -58
@@ -1,58 +1,1111 @@
1
- # Scheduler (Scheduler)
1
+ # `scheduler`
2
2
 
3
3
  **Acesso Studio:** `/studio/scheduler`
4
- **API Endpoint:** `/v3/scheduler`
4
+ **API Endpoint:** `/v3/scheduler` (gamificação) · `/v3/system/scheduler/*` (system) · `/v3/util/cron/status` (status agregado)
5
+ **Coleção MongoDB:** `scheduler` (configurações por gamificação) · `scheduler_log` (logs de execução) · `scheduler` no banco do sistema (índice de totais por apiKey)
5
6
 
6
- ## O que é
7
+ ---
7
8
 
8
- Execução de códigos JAVA em datas e horários agendados (expressões CRON). Permite agendar tarefas e automações para rodar em horários ou intervalos pré-definidos.
9
+ ## 1. Visão Geral
9
10
 
10
- ## Quando usar
11
+ O módulo `scheduler` executa rotinas em horários determinados por expressões cron Quartz. Cada gamificação tem suas próprias configurações de scheduler armazenadas em MongoDB e os jobs são instalados em um único `Scheduler` Quartz (StdScheduler) mantido na JVM pelo `SchedulerSystem` (singleton system-level).
11
12
 
12
- - Para gerar relatórios automatizados (ex: toda sexta às 10h)
13
- - Para debitar pontos de jogadores inativos
14
- - Para enviar lembretes automáticos de metas
15
- - Para campanhas temporais com início/fim automático
13
+ O scheduler opera em dois modos mutuamente exclusivos:
16
14
 
17
- ## Script Runtime Environment (Wrapper Class)
15
+ - **Script mode** executa um bloco Groovy arbitrário dentro de uma sandbox (`SecureASTCustomizer` + `TriggerExpressionChecker`). Selecionado quando os campos `entity`, `event` e `item` **não** estão todos preenchidos.
16
+ - **Entity mode** — despacha para um `SchedulerCallback` registrado por entidade na `ManagerFactory`. Selecionado quando `entity` E `event` E `item` estão preenchidos simultaneamente. O `script` é ignorado neste modo.
18
17
 
19
- O script do scheduler **não é standalone** — ele é inserido dentro de uma classe Java wrapper gerada pelo Funifier, idêntica à do Public Endpoint (ver `public.md` para a lista completa de imports).
18
+ O Quartz `Scheduler` é compartilhado pelo processo. Ele hospeda três tipos de job:
20
19
 
21
- ### Regras
20
+ - `FunifierJob` — schedulers de gamificação (group = `apiKey`)
21
+ - `ReportSchedulerJob` — relatórios system-level (group = `report._id`)
22
+ - `ServerPingJob` — ping do servidor para o servidor central (group = `server._id`)
22
23
 
23
- 1. **NÃO use `import`** todos os imports estão no wrapper (Unirest, Groovy JSON, Funifier entities/utils, Apache HTTP, Simple Java Mail, etc.)
24
- 2. Se precisar de uma classe não importada, use o nome completo: `com.example.MinhaClasse`
25
- 3. **`manager`** (ManagerFactory) está disponível como campo da classe
26
- 4. **Método principal:** `void execute()` (sem parâmetros)
27
- 5. **Timeout padrão de 10 segundos** — pode ser customizado via campo `timeout` (em **segundos**) na API: `POST /v3/scheduler` com `{"_id": "scheduler_id", "timeout": 30}`. Este campo não aparece no Studio.
28
- 6. Scripts rodam em **Groovy** — cuidado com `$` em GStrings
29
- 7. Use apenas **ASCII em comentários**
24
+ Não há persistência de estado do Quartz: o MongoDB é a fonte de verdade. Em cada `contextInitialized` o Quartz é repovoado a partir do banco. Reiniciar o processo perde os contadores de limite diário (`SchedulerStatistics` é em memória).
30
25
 
31
- > 📖 Ver `public.md` → **Script Runtime Environment** para a lista completa de imports e bibliotecas disponíveis.
26
+ Integrações principais:
32
27
 
33
- ## Checklist de Configuração no Studio
28
+ - **LastMile** (`LastMileManager`) único callback registrado para `entity = last_mile`. Cria/remove scheduler entity-bound automaticamente.
29
+ - **Bonus** (`BonusManager`) — código de criação de scheduler está totalmente comentado; apenas o cleanup no `delete` continua ativo.
30
+ - **App** (`AppManager.funifierCreateScheduler`) — instala schedulers a partir de templates ao publicar um app.
31
+ - **GameManager.delete** — chama `getSchedulerManager().deleteAll()` em cascata ao excluir uma gamificação.
32
+ - **ReportManager / ServerManager** — usam o Quartz compartilhado via `SystemFactory.getSchedulerManager().getQuartzScheduler()`, mas com jobs e coleções próprios (não passam pela coleção `scheduler`).
33
+ - **StatisticManager** — gerencia o limite diário (`GamificationLimits.dailySchedulerExecutions`, default 100).
34
34
 
35
- - [ ] Definir nome e descrição do scheduler
36
- - [ ] Definir expressão CRON
37
- - [ ] Escrever script Java
38
- - [ ] Testar em ambiente de homologação
35
+ ---
39
36
 
40
- ## API Endpoints
37
+ ## 2. Arquitetura e Fluxos
41
38
 
42
- ### Listar Schedulers
43
- **Método:** GET
44
- **Endpoint:** `/v3/scheduler`
39
+ ### 2.1 Inicialização do servidor — `SchedulerSystem`
45
40
 
46
- ### Criar Scheduler
47
- **Método:** POST
48
- **Endpoint:** `/v3/scheduler`
41
+ Construtor (executado quando `SystemFactory` é instanciado):
49
42
 
50
- ### Deletar Scheduler
51
- **Método:** DELETE
52
- **Endpoint:** `/v3/scheduler/:id`
43
+ ```
44
+ new StdSchedulerFactory().getScheduler()
45
+ quartzScheduler.startDelayed(10) // delay de 10 segundos
46
+ ```
53
47
 
54
- ## Validações e Testes
48
+ A inicialização em si dos jobs ocorre em `Configuration.contextInitialized` (web listener / boot do JBoss), que chama `SystemFactory.getInstance().getSchedulerManager().contextInitialized()`:
55
49
 
56
- - [ ] Scheduler aparece na lista
57
- - [ ] Expressão CRON é válida
58
- - [ ] Script executa no horário configurado
50
+ ```
51
+ 1. coleção scheduler do banco do sistema:
52
+ distinct("_id").query({total: {$gt: 0}}) ← retorna lista de apiKeys com schedulers
53
+ 2. Para cada apiKey:
54
+ ManagerFactory.getInstance(apiKey).getJongoConnection().getCollection("scheduler")
55
+ .find({active: true})
56
+ → para cada SchedulerConfig:
57
+ JobDetail = FunifierJob com identidade (scheduler.id, apiKey)
58
+ CronTrigger com identidade (scheduler.id + "trigger", apiKey)
59
+ com timezone aplicado se não vazio
60
+ quartzScheduler.scheduleJob(job, trigger)
61
+ 3. system_report: find({active: true})
62
+ → ReportSchedulerJob com identidade (report._id, report._id)
63
+ 4. system_server: find()
64
+ → se server.active: ServerPingJob com identidade (server._id, server._id)
65
+ ```
66
+
67
+ > **Atenção operacional:** schedulers de uma gamificação só são recarregados após reboot se houver um documento `{_id: <apiKey>, total: N}` (N > 0) no banco do **sistema**, coleção `scheduler`. Esse documento é mantido por `SchedulerSystem.addTotal(apiKey, count)` chamado dentro de `SchedulerManager.add` / `delete` / `deleteAll` / `deleteAllByEntity*`. Se essa coleção for limpa manualmente, os schedulers da gamificação ficam órfãos no banco e não voltam ao Quartz.
68
+
69
+ `Configuration.contextDestroyed` chama `quartzScheduler.shutdown(true)`.
70
+
71
+ ### 2.2 Pipeline principal — `FunifierJob → SchedulerRunner.run`
72
+
73
+ ```mermaid
74
+ flowchart LR
75
+ Q([Quartz fires CronTrigger]) --> FJ[FunifierJob.execute]
76
+ FJ -->|group=apiKey<br/>name=scheduler.id| MF[ManagerFactory.getInstance]
77
+ MF --> FB[schedulerManager.findById]
78
+ FB --> RUN[SchedulerRunner.run]
79
+ RUN --> SM[StatisticManager.newSchedulerExecution]
80
+ SM --> LIMIT{limite<br/>diário?}
81
+ LIMIT -->|excedido| ERR[exception<br/>+log+return]
82
+ LIMIT -->|ok| MODE{entity+event<br/>+item != null?}
83
+ MODE -->|sim| ENTITY[runEntity]
84
+ MODE -->|não| SCRIPT[runScript]
85
+ ENTITY --> LOG[SchedulerManager.addLog]
86
+ SCRIPT --> LOG
87
+ LOG --> R[(scheduler_log)]
88
+ ```
89
+
90
+ Sequência detalhada (números = ordem de execução):
91
+
92
+ ```
93
+ 1. FunifierJob.execute(context)
94
+ - apiKey = context.getJobDetail().getKey().getGroup()
95
+ - schedulerId = context.getJobDetail().getKey().getName()
96
+ - manager = ManagerFactory.getInstance(apiKey)
97
+ - scheduler = manager.getSchedulerManager().findById(schedulerId)
98
+ - new SchedulerRunner().run(scheduler, manager)
99
+
100
+ 2. SchedulerRunner.run(scheduler, manager)
101
+ - start = currentTimeMillis()
102
+ - sm = SystemFactory.getStatisticManager(apiKey)
103
+ - limitOk = sm.newSchedulerExecution(scheduler.id) // incrementa contadores e devolve true/false
104
+ - if !limitOk:
105
+ exceptions += "You have exceeded your scheduler daily executions limits"
106
+ (NÃO chama addLog, NÃO executa script/entity)
107
+ - else:
108
+ if (scheduler.entity != null && scheduler.event != null && scheduler.item != null):
109
+ runEntity(...)
110
+ else:
111
+ runScript(...)
112
+ addLog(new SchedulerLog(id=String(dailyCount), item=scheduler.id, out, err, ms))
113
+
114
+ 3. Retorno
115
+ - Map { scheduler, exceptions, outputs, millis }
116
+ ```
117
+
118
+ > **Inconsistência confirmada no código:** quando o limite diário é excedido, `addLog` **não** é chamado — não fica rastro em `scheduler_log` dessa execução pulada. Apenas o `System.out.println` é emitido.
119
+ >
120
+ > **Inconsistência confirmada no código:** se `scheduler == null` na chamada `FunifierJob.execute` (ID removido do banco mas job ainda no Quartz), `SchedulerRunner.run` recebe null e lança `NullPointerException` na chamada `sm.newSchedulerExecution(scheduler.id)`. Há um `//TODO excluir a scheduler` em `FunifierJob.java:31` reconhecendo esse caso sem tratamento.
121
+
122
+ ### 2.3 Fluxo `runEntity` (modo entity)
123
+
124
+ ```
125
+ 1. callback = manager.getSchedulerCallback(scheduler.entity)
126
+ 2. if callback != null:
127
+ callback.schedulerCallback(scheduler, exceptions, outputs)
128
+ 3. else: // entidade sem callback registrado
129
+ exceptions += "SchedulerCallback does not exist for entity " + scheduler.entity
130
+ s = manager.getSchedulerManager().findById(scheduler.id)
131
+ if s != null:
132
+ exceptions += "SchedulerConfig " + scheduler.id + " is now disabled"
133
+ s.active = false
134
+ manager.getSchedulerManager().add(s) // re-save com active=false
135
+ // efeito colateral: deleteJob(scheduler.id) no Quartz; updated atualizado
136
+ ```
137
+
138
+ Callbacks resolvidos por `ManagerFactory.getSchedulerCallback(entity)`:
139
+
140
+ | `entity` recebido | Callback retornado | Origem |
141
+ |-------------------|--------------------|--------|
142
+ | `last_mile` (= `Entity.LAST_MILE.collection`) | `LastMileManager` | Ativo |
143
+ | `bonus` (= `Entity.BONUS.collection`) | — | **Bloco comentado em `ManagerFactory.java:240-243`**; retorna `null` |
144
+ | qualquer outro | `null` | Falha-aberto: auto-desabilita o scheduler |
145
+
146
+ ### 2.4 Fluxo `runScript` — cache de classes compiladas
147
+
148
+ ```mermaid
149
+ flowchart LR
150
+ A[runScript] --> B{scheduler.updated<br/>== null?}
151
+ B -->|sim| C[add scheduler<br/>seta updated=now]
152
+ B -->|não| D[lê lastCompilation<br/>de dates map]
153
+ C --> D
154
+ D --> E{clazz cached<br/>E<br/>lastCompilation > scheduler.updated?}
155
+ E -->|sim| F[executor com clazz cached]
156
+ E -->|não| G[getScript + compile]
157
+ G --> H[compiled.put + dates.put now]
158
+ H --> F
159
+ ```
160
+
161
+ Pseudocódigo:
162
+
163
+ ```
164
+ runScript(scheduler, manager, exceptions, outputs):
165
+ if scheduler.updated == null:
166
+ manager.getSchedulerManager().add(scheduler) // upsert + seta updated
167
+ lastCompilation = manager.getSchedulerManager().dates.get(scheduler.id)
168
+ clazz = manager.getSchedulerManager().compiled.get(scheduler.id)
169
+ if clazz != null AND lastCompilation != null AND lastCompilation > scheduler.updated:
170
+ executor(clazz, scheduler, manager, exceptions, outputs)
171
+ else:
172
+ script = getScript(scheduler) // gera wrapper Groovy + imports
173
+ try:
174
+ clazz = compile(script) // SecureASTCustomizer + GroovyClassLoader
175
+ manager.getSchedulerManager().compiled.put(scheduler.id, clazz)
176
+ manager.getSchedulerManager().dates.put(scheduler.id, now)
177
+ executor(...)
178
+ catch CompilationFailedException:
179
+ exceptions += e.getMessage()
180
+ ```
181
+
182
+ `compiled` e `dates` são `Map<String, Class>` e `Map<String, Date>` em `SchedulerManager` (instância por gamificação, em memória JVM). Em cluster multi-node cada JVM mantém o seu cache; o campo `updated` no documento serve como sinal cross-node: salvar em outro nó incrementa `updated`, o que invalida o cache local na próxima execução.
183
+
184
+ ### 2.5 Geração do wrapper Groovy — `SchedulerRunner.getScript`
185
+
186
+ O wrapper inclui imports e infraestrutura padrão. Pseudocódigo da string montada:
187
+
188
+ ```groovy
189
+ import groovyx.net.http.*
190
+ import groovy.json.*
191
+ import groovy.transform.TimedInterrupt
192
+ import com.mashape.unirest.http.* // Unirest
193
+ import com.funifier.engine.action.Action, ActionLog
194
+ import com.funifier.engine.achievement.Achievement
195
+ import com.funifier.engine.lottery.Lottery, LotteryTicket
196
+ import com.funifier.engine.challenge.Challenge, ChallengeRule, ChallengeRuleFilter, Requirement, RewardPoint
197
+ import com.funifier.engine.constant.Operator, TimeScale
198
+ import com.funifier.engine.guid.Guid
199
+ import com.funifier.engine.level.Level
200
+ import com.funifier.engine.notify.Item, Notification, NotificationDefinition
201
+ import com.funifier.engine.player.Player
202
+ import com.funifier.engine.point.Point
203
+ import com.funifier.engine.team.Team
204
+ import com.funifier.engine.mail.FunifierMail
205
+ import com.funifier.engine.util.DateUtil, JsonUtil, ExcelUtil, HttpUtil, HttpClientFunifier, MustacheUtils
206
+ import com.funifier.engine.integration.trigger.GameDao
207
+ import com.funifier.controller.ManagerFactory
208
+
209
+ @TimedInterrupt(value = 2000L, unit = TimeUnit.SECONDS)
210
+ class FunifierScheduler {
211
+ <SCRIPT DO USUÁRIO> // colado literalmente
212
+
213
+ ManagerFactory manager = null
214
+ void setManager(ManagerFactory c) { manager = c }
215
+
216
+ GameDao database = null
217
+ void setDatabase(GameDao c) { database = c }
218
+
219
+ List output = new ArrayList()
220
+ void println(msg) { output.add(msg) }
221
+ void addAllOutput(List out) { out.addAll(output) }
222
+ }
223
+ ```
224
+
225
+ > **`@TimedInterrupt(value=2000L)`** é um teto absoluto de **2000 segundos** aplicado pelo Groovy. O timeout operacional via `scheduler.timeout` (default 30s) é menor e atua via `Future.cancel(true)` no executor.
226
+
227
+ ### 2.6 Compilação — `SchedulerRunner.compile`
228
+
229
+ ```
230
+ CompilerConfiguration conf
231
+ SecureASTCustomizer customizer
232
+ customizer.addExpressionCheckers(new TriggerExpressionChecker()) // blacklist
233
+ customizer.setTokensWhitelist([ EQUAL, PLUS, MINUS, MULTIPLY, DIVIDE, MOD, POWER,
234
+ PLUS_PLUS, PLUS_EQUAL, MINUS_MINUS,
235
+ COMPARE_EQUAL, COMPARE_NOT_EQUAL,
236
+ COMPARE_LESS_THAN, COMPARE_LESS_THAN_EQUAL,
237
+ COMPARE_GREATER_THAN, COMPARE_GREATER_THAN_EQUAL,
238
+ LOGICAL_OR, LOGICAL_OR_EQUAL,
239
+ LOGICAL_AND, LOGICAL_AND_EQUAL,
240
+ LEFT_SQUARE_BRACKET, RIGHT_SQUARE_BRACKET ])
241
+ conf.addCompilationCustomizers(customizer)
242
+ new GroovyClassLoader(Thread.currentThread().getContextClassLoader(), conf).parseClass(script)
243
+ ```
244
+
245
+ A whitelist de tokens exclui operadores comuns como `<<`, `>>`, `&`, `|`, `^`, `?:`, `?.`, `..`, etc. Isso restringe drasticamente as construções permitidas — qualquer uso desses operadores no script causa `SecurityException`.
246
+
247
+ ### 2.7 Execução isolada — `SchedulerRunner.executor`
248
+
249
+ ```
250
+ timeout = (scheduler.timeout != null && scheduler.timeout > 0) ? scheduler.timeout : 30
251
+ ExecutorService executor = Executors.newSingleThreadExecutor() // NOVO executor por execução
252
+ future = executor.submit(Callable {
253
+ GroovyObject obj = clazz.newInstance()
254
+ obj.invokeMethod("setManager", [manager])
255
+ obj.invokeMethod("setDatabase", [new GameDao(jongo, manager)])
256
+ obj.invokeMethod("execute", [scheduler]) // contrato do usuário
257
+ obj.invokeMethod("addAllOutput", [outputs])
258
+ return scheduler.id
259
+ })
260
+
261
+ try future.get(timeout, TimeUnit.SECONDS)
262
+ catch TimeoutException, InterruptedException, ExecutionException, SecurityException,
263
+ MultipleCompilationErrorsException, Exception:
264
+ exceptions += "Timeout after N seconds (timeout allowed " + timeout + " seconds)" // se Timeout/Interrupted
265
+ exceptions += e.getMessage()
266
+ future.cancel(true)
267
+ executor.shutdownNow()
268
+ ```
269
+
270
+ > **Bug latente:** se `clazz.newInstance()` lançar exception **antes** do `executor.submit`, `future` continua `null` e o `catch` chamará `future.cancel(true)` → `NullPointerException` que será capturado pelo último `catch (Exception)`.
271
+
272
+ > **Bug latente:** `executor` (ExecutorService) é instanciado por execução mas o `executor.shutdownNow()` só roda quando há exception. No caminho feliz nada chama `shutdown`/`shutdownNow`, então a thread fica em "core size = 0" e expira sozinha — não há vazamento de threads, mas há criação repetitiva de pools.
273
+
274
+ ### 2.8 `SchedulerManager.add` — criar ou atualizar
275
+
276
+ ```
277
+ add(scheduler):
278
+ if scheduler.id == null: scheduler.id = Guid.newShortGuid()
279
+ if scheduler.creation == null: scheduler.creation = new Date()
280
+ scheduler.updated = new Date() // SEMPRE atualizado
281
+
282
+ compile(scheduler): // valida script + cron
283
+ runner = new SchedulerRunner()
284
+ script = runner.getScript(scheduler)
285
+ runner.compile(script) // CompilationFailedException
286
+ new CronExpression(scheduler.cron) // ParseException
287
+
288
+ jongo.getCollection("scheduler").save(scheduler) // upsert por _id
289
+ quartzScheduler.deleteJob(jobKey(scheduler.id, apiKey)) // SEMPRE deleta job antigo
290
+
291
+ if scheduler.active:
292
+ job = newJob(FunifierJob.class).withIdentity(scheduler.id, apiKey)
293
+ cron = scheduler.timezone non-empty
294
+ ? cronSchedule(scheduler.cron).inTimeZone(scheduler.timezone)
295
+ : cronSchedule(scheduler.cron) // timezone padrão da JVM
296
+ trigger = newTrigger().withIdentity(scheduler.id + "trigger", apiKey).withSchedule(cron)
297
+ quartzScheduler.scheduleJob(job, trigger)
298
+
299
+ SystemFactory.getSchedulerManager().addTotal(apiKey, c.count()) // ÍNDICE de totais
300
+ compiled.remove(scheduler.id) // invalida cache local
301
+ // OBS: dates NÃO é removido aqui — apenas em delete()
302
+ ```
303
+
304
+ > **Compilação dupla observada:** `SchedulerRest.insert` chama `manager.compile(scheduler)` E em seguida `manager.add(scheduler)` (que recompila internamente). Não é um bug funcional, apenas trabalho duplicado.
305
+
306
+ ### 2.9 Diagrama de interação entre módulos
307
+
308
+ ```mermaid
309
+ sequenceDiagram
310
+ participant Boot as Configuration<br/>contextInitialized
311
+ participant Sys as SchedulerSystem
312
+ participant Quartz as Quartz Scheduler
313
+ participant Mgr as SchedulerManager<br/>(per apiKey)
314
+ participant Run as SchedulerRunner
315
+
316
+ Boot->>Sys: contextInitialized()
317
+ Sys->>Sys: lê scheduler<br/>{total:{$gt:0}}
318
+ loop cada apiKey
319
+ Sys->>Mgr: ManagerFactory.getInstance(apiKey)
320
+ Mgr->>Quartz: scheduleJob(FunifierJob, CronTrigger)
321
+ end
322
+ Sys->>Quartz: scheduleJob(ReportSchedulerJob)
323
+ Sys->>Quartz: scheduleJob(ServerPingJob)
324
+
325
+ Note over Quartz: ... cron dispara ...
326
+
327
+ Quartz->>Run: FunifierJob.execute(context)
328
+ Run->>Mgr: findById(scheduler.id)
329
+ Run->>Run: StatisticManager.newSchedulerExecution
330
+ alt entity+event+item preenchidos
331
+ Run->>Mgr: getSchedulerCallback(entity)
332
+ Mgr-->>Run: LastMileManager (ou null)
333
+ Run->>Mgr: callback.schedulerCallback
334
+ else script mode
335
+ Run->>Run: getScript + compile (com cache)
336
+ Run->>Run: executor.submit(invokeMethod)
337
+ end
338
+ Run->>Mgr: addLog(SchedulerLog)
339
+ ```
340
+
341
+ ### 2.10 Integração automática LastMile → Scheduler
342
+
343
+ ```mermaid
344
+ flowchart LR
345
+ A([POST /v3/last_mile]) --> B[LastMileManager.insert]
346
+ B --> C[deleteAllByEntityItemEvent<br/>last_mile, id, notify]
347
+ C --> Q1[(remove jobs antigos<br/>do Quartz)]
348
+ B --> D{lastmile.scheduler<br/>!= null?}
349
+ D -->|sim| E[clone scheduler do payload<br/>FORÇA: id=lastmile.id,<br/>active=true,<br/>timezone=securityManager.find.timezone,<br/>name=lastmile.title,<br/>entity=last_mile, event=notify,<br/>item=lastmile.id,<br/>script=&quot;&quot;]
350
+ E --> F[schedulerManager.add]
351
+ F --> Q2[(scheduleJob no Quartz)]
352
+ D -->|não| G[c.save lastmile]
353
+ F --> G
354
+ ```
355
+
356
+ `LastMileManager.insert` **sobrescreve** os seguintes campos do scheduler vindo do cliente, ignorando os valores enviados:
357
+
358
+ - `scheduler.id ← lastmile.id`
359
+ - `scheduler.active ← true`
360
+ - `scheduler.timezone ← securityManager.find().getTimeZone()` (timezone da gamificação)
361
+ - `scheduler.name ← lastmile.title`
362
+ - `scheduler.entity ← "last_mile"`
363
+ - `scheduler.event ← "notify"`
364
+ - `scheduler.item ← lastmile.id`
365
+ - `scheduler.script ← ""` (string vazia)
366
+
367
+ `SchedulerConfig.clone()` copia apenas: `id, name, cron, script, creation, active, timezone, extra, entity, event, item`. Os campos `time, timeout, updated, techniques, style, references` são **silenciosamente descartados** no clone — passar qualquer um deles no payload `lastmile.scheduler` é inerte. Em seguida `LastMileManager.insert` sobrescreve `id, active, timezone, name, entity, event, item, script`. Resultado: do payload original sobrevivem apenas `cron`, `extra` e `creation`. O `event` enviado pelo cliente é descartado em favor de `"notify"`.
368
+
369
+ `LastMileManager.delete` chama `deleteAllByEntityItem(Entity.LAST_MILE.collection, id)` — apaga TODOS os schedulers daquele lastmile, independente de `event`.
370
+
371
+ `BonusManager.delete` igualmente chama `deleteAllByEntityItem(Entity.BONUS.collection, id)` — cleanup vestigial; já não há código que crie esses schedulers (o bloco em `BonusManager.insert:44-67` está totalmente comentado).
372
+
373
+ ---
374
+
375
+ ## 3. Estrutura dos Objetos
376
+
377
+ ### 3.1 `SchedulerConfig` — documento principal (coleção `scheduler` da gamificação)
378
+
379
+ `@JsonIgnoreProperties(ignoreUnknown=true)` — campos desconhecidos no payload são **silenciosamente descartados** pelo Jackson.
380
+
381
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
382
+ |-------|------|--------|-------------|-----------|
383
+ | `_id` | String | `Guid.newShortGuid()` no `add` se null | — | Identificador único na gamificação |
384
+ | `name` | String | — | não | Nome descritivo |
385
+ | `cron` | String | — | **sim** | Expressão cron Quartz (6 campos). Validada por `new CronExpression(...)` |
386
+ | `script` | String | — | condicional | Código Groovy. Ignorado em modo entity. Obrigatório em modo script |
387
+ | `creation` | Date | `new Date()` no `add` se null | — | Data de criação |
388
+ | `active` | boolean | `false` | não | Quando `false`, documento existe mas job não é instalado no Quartz |
389
+ | `timezone` | String | — | não | Timezone IANA (ex: `America/Sao_Paulo`). Se null ou vazio, usa timezone padrão da JVM |
390
+ | `extra` | `Map<String,Object>` | `{}` | não | Dados arbitrários acessíveis em `scheduler.extra` no script |
391
+ | `entity` | String | — | condicional | Nome da coleção da entidade dona do callback (ex: `last_mile`). Ativa modo entity quando E `event` E `item` também preenchidos |
392
+ | `event` | String | — | condicional | Evento dentro da entidade (ex: `notify`) |
393
+ | `item` | String | — | condicional | ID do item específico (passado em `scheduler.item` ao callback) |
394
+ | `time` | Date | — | — | Horário de referência da execução. Persistido mas só preenchido pelo endpoint `/execute/{id}?time=...`; em execuções Quartz normais chega `null` |
395
+ | `timeout` | Long | `30` | não | Timeout do script em **segundos**. Se null ou `<= 0`, usa 30 |
396
+ | `updated` | Date | `new Date()` em todo `add()` | — | Timestamp do último save; usado como sinal de invalidação de cache cross-node |
397
+ | `techniques` | `List<String>` | `[]` | não | Códigos de técnicas de jogo associadas (GT codes) |
398
+ | `style` | `ObjectStyle` | null | não | Atributos visuais para o Studio: `{background: String, visible: Boolean}` |
399
+ | `references` | `List<Reference>` | null | não | Relacionamentos com outros componentes da estratégia (gamification graph) |
400
+
401
+ #### Campos computados (não persistem como tais)
402
+
403
+ - **Modo de operação** — derivado de `(entity, event, item)`. Os três precisam estar simultaneamente não-null (`!= null`) para acionar `runEntity`. Qualquer um null cai em `runScript`. Não há validação prévia: um scheduler com `entity = "bonus"` e os outros campos é aceito pelo POST e só falha na execução (auto-desabilita).
404
+
405
+ #### Campos silenciosamente sobrescritos no save
406
+
407
+ - `_id` recebe ShortGuid se null.
408
+ - `creation` recebe `new Date()` se null.
409
+ - `updated` recebe `new Date()` em **toda** chamada `add()`, sobrescrevendo o valor do payload.
410
+
411
+ #### Diferença schema vs runtime
412
+
413
+ - `time` é persistido no MongoDB e pode ser definido via payload no POST, mas no fluxo Quartz normal nunca é lido — o `SchedulerRunner.run` não popula `scheduler.time` antes da execução. Único caminho operacional: `GET /v3/scheduler/execute/{id}?time=...` faz `scheduler.time = date` antes de chamar `run`. Salvar `time` num POST regular é portanto inerte na cron normal.
414
+ - `style` e `references` existem para o Studio e nunca são lidos pelo `SchedulerRunner`.
415
+ - `techniques` é uma lista de códigos GT mas não há lógica de execução dentro do módulo scheduler que processe esse campo — é metadado para a UI.
416
+
417
+ #### Enums e constantes referenciados
418
+
419
+ - `Entity.SCHEDULER.collection = "scheduler"`
420
+ - `Entity.SCHEDULER_LOG.collection = "scheduler_log"`
421
+ - `Entity.LAST_MILE.collection = "last_mile"` (valor aceito como `entity`)
422
+ - `Entity.BONUS.collection = "bonus"` (valor aceito como `entity` mas sem callback registrado)
423
+ - `LastMileManager.EVENT_NOTIFY = "notify"`
424
+ - `GamificationLimits.DEFAULT_DAILY_SCHEDULER_EXECUTIONS_LIMIT = 100`
425
+ - `Reference.WAY_FROM = "from"`, `Reference.WAY_TO = "to"`
426
+
427
+ ### 3.2 `SchedulerLog` — documento de log (coleção `scheduler_log`)
428
+
429
+ `@JsonIgnoreProperties(ignoreUnknown=true)`.
430
+
431
+ | Campo | Tipo | Padrão | Descrição |
432
+ |-------|------|--------|-----------|
433
+ | `_id` | String | `String.valueOf(sm.getSchedulerStatistics().dailySchedulerExecutions)` | **Não é UUID** — é o valor inteiro do contador diário em memória, convertido para string. Após reinício do servidor o contador reinicia em 1 — IDs colidem entre dias/restarts e o `c.save()` faz upsert por `_id` (sobrescreve log anterior com o mesmo número). |
434
+ | `item` | String | — | ID do scheduler que foi executado |
435
+ | `time` | Date | `new Date()` no construtor | Momento do registro |
436
+ | `out` | `List<String>` | `null` quando lista vazia | Outputs de `println()` durante execução. Construtor faz: `out = (out != null && out.size() > 0) ? out : null` |
437
+ | `err` | `List<String>` | `null` quando lista vazia | Mensagens de exceção. Mesma regra de `out` |
438
+ | `ms` | long | — | Tempo total de `SchedulerRunner.run` em milissegundos |
439
+
440
+ > Como `out` e `err` são gravados como `null` quando vazios (não como `[]`), para filtrar execuções com erros use `q={err:{$ne:null}}`.
441
+
442
+ ### 3.3 `Reference` — subentidade de relacionamento
443
+
444
+ Compartilhada com `Trigger`. Documenta como o scheduler se relaciona com outros componentes (consumo de pontos, vínculo a achievement, etc).
445
+
446
+ | Campo | Tipo | Descrição |
447
+ |-------|------|-----------|
448
+ | `way` | String | `"from"` ou `"to"`; sentido da seta no grafo do Studio |
449
+ | `linkLabel` | String | Texto sobre a seta |
450
+ | `_id` | String | ID do componente referenciado |
451
+ | `type` | String | Coleção do componente (ex: `Entity.POINT.collection`) |
452
+ | `title` | String | Título descritivo |
453
+
454
+ ### 3.4 `ObjectStyle` — subentidade de visual
455
+
456
+ | Campo | Tipo | Descrição |
457
+ |-------|------|-----------|
458
+ | `background` | String | Cor de fundo / asset visual no Studio |
459
+ | `visible` | Boolean | Visibilidade na UI |
460
+
461
+ ### 3.5 `SchedulerStatistics` — estado em memória (não persistido autonomamente)
462
+
463
+ Mantido por `StatisticManager` (um por gamificação). Persistido apenas como parte de `STATISTIC_LOG` (coleção do sistema).
464
+
465
+ | Campo | Tipo | Descrição |
466
+ |-------|------|-----------|
467
+ | `lastSchedulerExecution` | Date | Última execução registrada |
468
+ | `hourlySchedulerExecutions` | long | Contador na hora corrente. Reset ao mudar `HOUR_OF_DAY` |
469
+ | `dailySchedulerExecutions` | long | Contador no dia corrente. Reset ao mudar `DAY_OF_YEAR` |
470
+ | `monthlySchedulerExecutions` | long | Contador no mês corrente. Reset ao mudar `MONTH` |
471
+ | `dailySchedulerExecutionsLimit` | long | Cópia de `GamificationLimits.dailySchedulerExecutions` (default 100) |
472
+ | `triggers` | `Map<String, SchedulerDetailStatistics>` | Contadores por scheduler.id |
473
+
474
+ > O comparador no `newSchedulerExecution` usa `<=`: incrementa o contador primeiro, depois testa `dailySchedulerExecutions <= dailySchedulerExecutionsLimit`. Com limite 100, a 100ª chamada incrementa para 100, passa no teste (`100 <= 100`) e retorna `true`; a 101ª retorna `false`. Permite exatamente `limit` execuções por dia.
475
+
476
+ ---
477
+
478
+ ## 4. Endpoints
479
+
480
+ ### `GET /v3/scheduler`
481
+
482
+ Listagem com filtros via aggregate pipeline (não `find` direto).
483
+
484
+ | Aspecto | Detalhe |
485
+ |---------|---------|
486
+ | Finalidade | Listar configurações de scheduler da gamificação |
487
+ | Autenticação | Bearer token |
488
+ | Implementação | `MongoCollection.aggregate(...)` com `$match` + `$sort` + `$limit` |
489
+ | Resposta | Array JSON com campos null removidos (`JsonUtil.toJsonRemoveNullFields`) |
490
+
491
+ **Query params:**
492
+
493
+ | Param | Tipo | Descrição |
494
+ |-------|------|-----------|
495
+ | `id` | String | Filtra `_id` exato |
496
+ | `q` | String | Cláusula MongoDB **raw injetada** no `$match` |
497
+ | `published_min` | String | Lower bound aplicado em `creation`. RFC3339 ou keyword (`-1d`, `-30m`, `-1w`, `y/M/d/h/m/s/w`) |
498
+ | `published_max` | String | Upper bound em `creation` |
499
+ | `orderby` | String | Campo de `$sort` (também injetado raw) |
500
+ | `reverse` | String→boolean | `"true"` → `-1` (DESC); senão `1` (ASC) |
501
+ | `max_results` | String→int | Default `100` quando `<= 0` |
502
+
503
+ **Comportamento real:**
504
+
505
+ - `q` é concatenado literalmente: `query.append(", " + q)` — qualquer operador MongoDB válido funciona, inclusive `$where`. Não há sanitização. **Surface de injeção.**
506
+ - `orderby` também é concatenado literalmente em `{$sort: {<orderby>: #}}`. Não há lista permitida.
507
+ - Parsing de `published_min`/`published_max` tenta primeiro `parseZonedDateToLocalDate` (RFC3339) e cai em `fromKeyword` se null.
508
+ - O `findAll(id, q, ...)` aceita id no path do match, mas a rota também aceita id como query param; se ambos forem usados o id do query param prevalece.
509
+
510
+ ---
511
+
512
+ ### `GET /v3/scheduler/log`
513
+
514
+ Logs de execução com paginação.
515
+
516
+ | Aspecto | Detalhe |
517
+ |---------|---------|
518
+ | Finalidade | Listar `SchedulerLog` com paginação por `Range` header |
519
+ | Implementação | `PaginationUtil.getPageResult(aggregate, range, 0, 100)` |
520
+
521
+ **Query params:**
522
+
523
+ | Param | Tipo | Descrição |
524
+ |-------|------|-----------|
525
+ | `item` | String | Filtra logs de um scheduler específico (`log.item == schedulerId`) |
526
+ | `q` | String | Cláusula MongoDB raw (mesmo padrão de injeção do `GET /v3/scheduler`) |
527
+ | `published_min` | String | Lower bound aplicado em `time` (não `creation`) |
528
+ | `published_max` | String | Upper bound em `time` |
529
+ | `orderby` | String | Campo de ordenação raw |
530
+ | `reverse` | String→boolean | DESC quando `true` |
531
+ | `max_results` | String→int | Default `100` |
532
+
533
+ **Header:**
534
+
535
+ | Header | Descrição |
536
+ |--------|-----------|
537
+ | `Range` | Formato `items=A-B`, ex `items=0-99` |
538
+
539
+ **Resposta:** payload paginado padrão da Funifier (`PaginationUtil`). Resposta contém o campo `Content-Range` apropriado quando o handler é `Callback.paginatedCallback`.
540
+
541
+ ---
542
+
543
+ ### `GET /v3/scheduler/{id}`
544
+
545
+ Retorna `SchedulerConfig` por `_id`. Campos null removidos. Sem 404 explícito: se `findById` retornar null, a resposta é vazia / JSON `{}`.
546
+
547
+ ---
548
+
549
+ ### `GET /v3/scheduler/execute/{id}`
550
+
551
+ Execução manual para debug.
552
+
553
+ | Aspecto | Detalhe |
554
+ |---------|---------|
555
+ | Finalidade | Disparar o scheduler imediatamente, fora da agenda Quartz |
556
+ | Efeito colateral | Consome quota diária; grava `scheduler_log` |
557
+ | Diferença vs. cron | Não respeita `active=false` — executa mesmo desativado |
558
+
559
+ **Query params:**
560
+
561
+ | Param | Tipo | Descrição |
562
+ |-------|------|-----------|
563
+ | `time` | String | Sobrescreve `scheduler.time` antes da execução. Aceita RFC3339, keyword (`-1d`), ou epoch millis (`Long.parseLong`). Tentativas em cascata; se todas falharem, `scheduler.time` mantém o valor original (não é sobrescrito) |
564
+
565
+ **Resposta:** `{ scheduler, exceptions, outputs, millis }` com null removidos.
566
+
567
+ > Se `scheduler == null` (id inexistente), `scheduler.time = date` lança `NullPointerException` antes mesmo da chamada do `run`.
568
+
569
+ ---
570
+
571
+ ### `GET /v3/scheduler/next/{id}`
572
+
573
+ Retorna as **próximas 10 datas** de execução após uma referência.
574
+
575
+ **Query params:**
576
+
577
+ | Param | Tipo | Descrição |
578
+ |-------|------|-----------|
579
+ | `reference` | String | Data de referência. RFC3339, keyword, ou `null`. Se null, usa `new Date()` |
580
+
581
+ **Comportamento real:** `nextExecutionTimesAfter` faz um loop fixo de 10 iterações chamando `CronExpression.getNextValidTimeAfter`. Se em alguma iteração retornar `null` (sem próxima ocorrência), o loop quebra e retorna a lista parcial. Datas vêm formatadas via `DateUtil.formatDateToZonedDate`.
582
+
583
+ ---
584
+
585
+ ### `POST /v3/scheduler`
586
+
587
+ Criar ou atualizar um scheduler.
588
+
589
+ | Aspecto | Detalhe |
590
+ |---------|---------|
591
+ | Finalidade | Upsert por `_id` |
592
+ | Full replace ou patch | **Full replace** — `Jongo.save()` substitui o documento |
593
+ | Validação | Compila script Groovy **+** valida cron antes de salvar |
594
+ | HTTP status | **Sempre `201 Created`**, inclusive em erro de compilação |
595
+ | Content-Type | `application/json` |
596
+
597
+ **Resposta sucesso:**
598
+
599
+ ```json
600
+ { "scheduler": { ... }, "status": "OK" }
601
+ ```
602
+
603
+ **Resposta erro de compilação ou cron inválido:**
604
+
605
+ ```json
606
+ { "scheduler": { ... }, "status": "ERROR", "message": "<mensagem>" }
607
+ ```
608
+
609
+ **Comportamento real:**
610
+
611
+ 1. `manager.compile(scheduler)` valida script + cron. Se falhar com `CompilationFailedException` ou `ParseException`, retorna `status: ERROR` no body com HTTP 201.
612
+ 2. **Não captura** `SchedulerException` (de operações Quartz) — esta sobe pela stack e produz HTTP 500.
613
+ 3. `manager.add(scheduler)` recompila o script (compilação dupla) e:
614
+ - Gera `_id` ShortGuid se não fornecido
615
+ - Seta `creation = now` se null
616
+ - Seta `updated = now` (sempre)
617
+ - Upsert no MongoDB
618
+ - Remove job antigo do Quartz e recria se `active=true`
619
+ - Invalida `compiled[scheduler.id]`
620
+ - Atualiza índice de totais do sistema
621
+
622
+ **Exemplo:**
623
+
624
+ ```json
625
+ POST /v3/scheduler
626
+ {
627
+ "active": true,
628
+ "name": "Notificação diária",
629
+ "cron": "0 0/20 * * * ?",
630
+ "timezone": "America/Sao_Paulo",
631
+ "script": "void execute(scheduler){ println 'every 20 minutes for ' + scheduler.extra.company; }",
632
+ "extra": { "company": "funifier" }
633
+ }
634
+ ```
635
+
636
+ ---
637
+
638
+ ### `DELETE /v3/scheduler/{id}`
639
+
640
+ Remove documento + job Quartz + entradas de `compiled` E `dates` + atualiza índice de totais.
641
+
642
+ Resposta: `204 No Content`. Não há verificação se o ID existe — `c.remove()` é idempotente.
643
+
644
+ ---
645
+
646
+ ### `PUT /v3/scheduler/compile/{id}`
647
+
648
+ Limpa **apenas** o cache `compiled` (não `dates`).
649
+
650
+ | Path | Efeito |
651
+ |------|--------|
652
+ | `/compile/all` | Itera `findAll()` e chama `manager.clear()` em cada — remove cada `compiled[id]` |
653
+ | `/compile/<id>` | `findById(id)` e se não-null chama `clear()` |
654
+
655
+ > A próxima execução ainda comparará `dates[id] > scheduler.updated`. Como `clear()` não toca em `dates`, se `dates[id]` permanecer maior que `updated`, o teste `clazz != null && lastCompilation > updated` falha (clazz é null), forçando recompilação — comportamento desejado. Mas o `dates` permanece "sujo": após recompilar, `dates.put(id, now)` sobrescreve.
656
+
657
+ Resposta: `200 OK` com `{ "_id": "<id>" }`.
658
+
659
+ ---
660
+
661
+ ### `GET /v3/system/scheduler/status` (system-only)
662
+
663
+ Expõe `SchedulerSystem.getStatus()`. Retorna estado agregado:
664
+
665
+ ```json
666
+ {
667
+ "isStarted": true,
668
+ "jobGroupNames": [ "<apikey>", "<reportId>", "<serverId>" ],
669
+ "triggerGroupNames": [ "..." ],
670
+ "pausedTriggers": [],
671
+ "metadata": { ... },
672
+ "schedulers": [
673
+ { "_id": "<schedulerId>trigger", "next": "...", "prev": "..." }
674
+ ],
675
+ "references": [
676
+ "report_<reportId>",
677
+ "gamification_<apikey>_sheduler_<schedulerId>"
678
+ ]
679
+ }
680
+ ```
681
+
682
+ > **Typo no payload de produção:** `"gamification_<apikey>_sheduler_<id>"` — "sheduler" sem 'c'. Está hardcoded em `SchedulerSystem.getStatus:153`.
683
+
684
+ ---
685
+
686
+ ### `GET /v3/system/scheduler/jobs` (system-only)
687
+
688
+ Expõe `SchedulerSystem.getAllJobKeys()`. Itera todos os jobs do Quartz, resolve cada `group` na coleção `game` (campo `_id == group`) para anexar nome da gamificação, apiKey e nome do scheduler. Retorna lista de:
689
+
690
+ ```json
691
+ [
692
+ {
693
+ "groupName": "<apikey ou reportId ou serverId>",
694
+ "jobName": "<schedulerId>",
695
+ "gamification": "<game.name>",
696
+ "account": "<game.accountId>",
697
+ "schedulerName": "<scheduler.name ou 'SCHEDULER DONT EXIST'>",
698
+ "nextExecution": "Mon Jan 01 09:00:00 UTC 2025",
699
+ "isActive": "Yes"
700
+ }
701
+ ]
702
+ ```
703
+
704
+ > Se o job está no Quartz mas foi removido do MongoDB, `schedulerName` aparece como `"SCHEDULER DONT EXIST"`. Útil para detectar drift.
705
+
706
+ ---
707
+
708
+ ### `GET /v3/util/cron/status` (gamificação, com Bearer)
709
+
710
+ Alias acessível para o mesmo `SchedulerSystem.getStatus()` — duplicação intencional para acesso por consumidores de gamificação.
711
+
712
+ ---
713
+
714
+ ## 5. Regras de Negócio
715
+
716
+ **Modo de operação derivado, não declarado.** Não há campo `mode` — é uma derivação implícita: `(entity != null && event != null && item != null)` → entity; senão → script. Schedulers parcialmente preenchidos (ex. `entity` mas sem `event`) caem em script silenciosamente.
717
+
718
+ **Modo entity ignora `script`.** No fluxo `runEntity` o campo `script` nunca é avaliado. O `LastMileManager.insert` aproveita isso para gravar `script = ""` (vazio) em schedulers entity-bound — economiza memória mas confunde quem inspeciona a coleção.
719
+
720
+ **Validação dupla no upsert.** `SchedulerRest.insert` chama `compile()` e depois `add()` (que recompila internamente). Em fluxos via `AppManager.funifierCreateScheduler`, `LastMileManager.insert` ou outros caminhos internos, apenas `add()` é chamado — também valida.
721
+
722
+ **Auto-disable silencioso por callback ausente.** Se `entity` for uma string para a qual `ManagerFactory.getSchedulerCallback` retorna null, o scheduler é re-salvo com `active=false` na primeira execução. **A primeira execução consome quota** mas executa um `runEntity` no-op que termina com exception. O usuário não é notificado fora do `err` do log.
723
+
724
+ **Limite diário.** `dailySchedulerExecutionsLimit` vem de `GamificationLimits` (coleção `gamification_limits` no banco do sistema). Default `100/dia`. Limite só bloqueia execuções de schedulers — não bloqueia o agendamento. Se atingido, `runEntity`/`runScript` nem é chamado, mas o log também não é gravado.
725
+
726
+ **Contadores em memória.** `SchedulerStatistics` reinicia ao detectar mudança de hora/dia/mês na próxima execução (não no relógio). Reiniciar o processo zera os contadores antes do banco persistir (apenas `STATISTIC_LOG` é gravado periodicamente em outros caminhos — verificar com [statistic_log]).
727
+
728
+ **Upsert por `_id`.** Tanto `add()` quanto `c.save()` fazem upsert. Reusar `_id` em um POST substitui o documento inteiro — não é PATCH.
729
+
730
+ **Cron Quartz, não Unix.** 6 campos (`seg min hor dia mês dia-semana`). Diferenças relevantes vs cron Unix:
731
+ - Dia-da-semana usa códigos `MON–SUN` ou números 1–7 (1=Domingo no Quartz).
732
+ - `?` é obrigatório em um dos campos `dia` ou `dia-semana` (não pode-se especificar ambos).
733
+ - Suporta `/` para incremento, `L` para "last", `W` para weekday, `#` para "nth day-of-week".
734
+
735
+ **Timezone.** Se `scheduler.timezone` é `null` ou string vazia, o cron usa o timezone padrão da JVM. Em containers/produção isso é normalmente UTC — o que muda o instante de disparo vs. o esperado pelo usuário do Studio.
736
+
737
+ **Identidade Quartz.** Jobs de gamificação têm `JobKey(scheduler.id, apiKey)`. Triggers têm `TriggerKey(scheduler.id + "trigger", apiKey)`. Não há risco de colisão entre gamificações; há risco de colisão com schedulers system se um `apiKey` coincidir com `server._id` ou `report._id` (improvável na prática).
738
+
739
+ **Cluster.** O Quartz é local (`StdSchedulerFactory.getScheduler()` sem `quartz.properties` para JDBC store). Em múltiplos nós, **cada nó executa o cron independentemente** — o scheduler vai disparar N vezes em uma topologia de N nós. O campo `updated` apenas serve para sincronização do cache de compilação.
740
+
741
+ ---
742
+
743
+ ## 6. Comportamentos Automáticos
744
+
745
+ | Comportamento | Trigger | Impacto | Persistência |
746
+ |---------------|---------|---------|--------------|
747
+ | Auto `_id` via ShortGuid | `add()` sem `_id` | Gera identificador | MongoDB |
748
+ | Auto `creation` | `add()` sem `creation` | Seta `new Date()` | MongoDB |
749
+ | Auto `updated` | Toda chamada `add()` | Sobrescreve `updated` com `new Date()` | MongoDB + invalida cache cross-node |
750
+ | Recompilação e re-agendamento do job | Toda `add()` | `quartzScheduler.deleteJob(...)` SEMPRE; `scheduleJob` apenas se `active=true` | Memória Quartz |
751
+ | Atualização do índice de totais | `add()`, `delete()`, `deleteAll()`, `deleteAllByEntity*` | `{_id: apiKey, total: count}` na coleção `scheduler` do sistema | MongoDB sistema |
752
+ | Invalidação de `compiled` | `add()` e `delete()` | Remove entrada da `Map<String, Class>` | Memória JVM |
753
+ | Invalidação de `dates` | `delete()` apenas | Remove entrada da `Map<String, Date>` | Memória JVM |
754
+ | Auto-disable por callback ausente | Execução em modo entity quando `getSchedulerCallback(entity) == null` | Re-save com `active=false` + job removido do Quartz | MongoDB + Quartz |
755
+ | Cascade delete no game | `GameManager.delete(apiKey)` | `schedulerManager.deleteAll()` | MongoDB + Quartz + sistema |
756
+ | Cascade no LastMile.insert | `LastMileManager.insert(lastmile)` | `deleteAllByEntityItemEvent("last_mile", id, "notify")` + `add(scheduler clonado e sobrescrito)` | MongoDB + Quartz |
757
+ | Cascade no LastMile.delete | `LastMileManager.delete(id)` | `deleteAllByEntityItem("last_mile", id)` (qualquer `event`) | MongoDB + Quartz |
758
+ | Cleanup vestigial no Bonus.delete | `BonusManager.delete(id)` | `deleteAllByEntityItem("bonus", id)` — limpa schedulers que o `insert` não mais cria | MongoDB + Quartz |
759
+ | Instalação a partir de app template | `AppManager.funifierCreateScheduler(payload)` | Desserializa JSON em `SchedulerConfig` e chama `add()` | MongoDB + Quartz |
760
+ | Boot bootstrap | `Configuration.contextInitialized` → `SchedulerSystem.contextInitialized` | Recarrega todos schedulers ativos no Quartz | Quartz (memória) |
761
+ | Shutdown | `Configuration.contextDestroyed` → `quartzScheduler.shutdown(true)` | Para todos os jobs aguardando finalização | — |
762
+
763
+ ### 6.1 Fluxo de cascade ao deletar uma gamificação
764
+
765
+ ```mermaid
766
+ flowchart LR
767
+ A([DELETE game]) --> B[GameManager.delete]
768
+ B --> C[schedulerManager.deleteAll]
769
+ C --> D[(loop SchedulerConfigs)]
770
+ D --> E[quartz.deleteJob<br/>por scheduler]
771
+ D --> F[c.drop scheduler collection]
772
+ F --> G[SystemFactory.deleteTotal apiKey]
773
+ G --> H[(remove de scheduler<br/>do sistema)]
774
+ ```
775
+
776
+ ---
777
+
778
+ ## 7. Suportado vs NÃO Suportado
779
+
780
+ ### ✅ Suportado
781
+
782
+ - Criar/atualizar/deletar scheduler via REST (`POST`, `DELETE`).
783
+ - Listar com filtros via aggregate pipeline (`q`, `published_min/max`, `orderby`, `reverse`, `max_results`).
784
+ - Listar logs com paginação por header `Range`.
785
+ - Execução manual via `/v3/scheduler/execute/{id}` com sobrescrita opcional de `time`.
786
+ - Cálculo das próximas 10 datas via `/v3/scheduler/next/{id}`.
787
+ - Limpeza manual de cache de compilação via `/v3/scheduler/compile/{id|all}`.
788
+ - Timezone IANA por scheduler.
789
+ - Timeout configurável por scheduler (em segundos, default 30).
790
+ - Sandbox Groovy via `SecureASTCustomizer` (blacklist + tokens whitelist).
791
+ - Limite diário por gamificação (default 100/dia, configurável em `gamification_limits`).
792
+ - Cache de classes Groovy compiladas com invalidação por `updated`.
793
+ - Modo entity para `last_mile` (único callback ativo).
794
+ - Status agregado do Quartz via `/v3/util/cron/status` e `/v3/system/scheduler/status`.
795
+ - Listagem completa dos JobKeys do Quartz via `/v3/system/scheduler/jobs`.
796
+ - Instalação programática via `AppManager.funifierCreateScheduler` (a partir de templates de app).
797
+ - Cascade automático em `LastMileManager.insert/delete` e `GameManager.delete`.
798
+
799
+ ### ❌ NÃO Suportado
800
+
801
+ - **PATCH parcial**: `POST` substitui o documento inteiro. Não há endpoint de update parcial.
802
+ - **Modo entity para `bonus`**: o bloco em `ManagerFactory.getSchedulerCallback` está totalmente comentado (linhas 240-243). Qualquer scheduler com `entity = "bonus"` é auto-desabilitado na primeira execução. O método `schedulerCallback` em `BonusManager.java:354-359` também está dentro de bloco comentado.
803
+ - **Modo entity para qualquer entidade fora `last_mile`**: o dispatcher tem só uma entrada ativa.
804
+ - **Retry em falha**: execuções com exception ou timeout não são re-tentadas; aguardam o próximo disparo cron.
805
+ - **Persistência dos contadores de limite**: `SchedulerStatistics` é em memória; restart zera. O snapshot vai parar em `STATISTIC_LOG` apenas em outros caminhos do `StatisticManager`.
806
+ - **HTTP non-201 em erro de validação**: o `POST` sempre retorna `201 Created` mesmo com `status: "ERROR"` no body. Falhas de compilação Groovy e parse de cron NÃO produzem `400 Bad Request`.
807
+ - **Idempotência do `SchedulerLog._id`**: o `_id` é o contador diário em memória como string (`"1"`, `"2"`, ...). Após restart o contador reinicia; o `save()` faz upsert — logs com mesmo `_id` se sobrescrevem entre dias/restarts.
808
+ - **404 quando scheduler não existe**: `GET /v3/scheduler/{id}` para id inexistente retorna body vazio (JSON `{}`) com HTTP 200, não `404`. `GET /v3/scheduler/execute/{id}` lança `NullPointerException` (HTTP 500) — o `null` do `findById` não é tratado.
809
+ - **Validação de combinação `entity` + `event` + `item`**: aceita combinações parciais que caem silenciosamente em script mode.
810
+ - **Lock cluster-wide**: em múltiplos nós, cada nó executa o cron independentemente. Não há leader election — para evitar duplicação use lógica no script.
811
+ - **Cron Unix (5 campos)**: apenas Quartz (6 campos). Expressões Unix-style são rejeitadas no `new CronExpression(...)`.
812
+ - **Tokens excluídos da whitelist do sandbox**: `<<`, `>>`, `&`, `|`, `^`, `?:`, `?.`, `..` causam erro de compilação. Operações de bit, navegação null-safe e ranges (`1..10`) não estão liberadas.
813
+ - **`@TimedInterrupt` configurável**: o teto absoluto de 2000 segundos no wrapper é hardcoded em `SchedulerRunner.getScript:200`.
814
+ - **Snapshot de `time` por execução cron**: `scheduler.time` só é preenchido pelo endpoint `/execute` — em execuções Quartz é `null`. Para usar o horário corrente no script, use `new Date()` diretamente.
815
+ - **Pausar / despausar via REST**: não há endpoint. Setar `active=false` via POST + re-salvar é o caminho.
816
+ - **Limpeza orfan do Quartz**: se o documento for removido do MongoDB diretamente (via Database REST `/v3/database/scheduler`), o job no Quartz permanece até reboot ou intervenção manual.
817
+
818
+ ---
819
+
820
+ ## 8. Segurança e Permissões
821
+
822
+ ### Autenticação
823
+
824
+ Todas as rotas de `/v3/scheduler/*` usam `@BeanParam AuthBean` → Bearer token. O `apiKey` derivado do token é usado para:
825
+
826
+ - Buscar o `ManagerFactory` da gamificação (`FrontController.getInstance(apiKey).getManagerFactory()`).
827
+ - Identidade do job Quartz: `JobKey(scheduler.id, apiKey)` — isolamento por gamificação.
828
+ - Conexão MongoDB já namespaced em `manager.getJongoConnection()`.
829
+
830
+ As rotas `/v3/system/scheduler/*` não exigem `AuthBean` por código no `SchedulerRest` (system-side), mas dependem da configuração do servidor (geralmente atrás de filtro de IP/role).
831
+
832
+ ### Sandbox Groovy
833
+
834
+ Duas camadas:
835
+
836
+ **1. `TriggerExpressionChecker` — blacklist (compartilhado com Trigger):**
837
+
838
+ Classes proibidas (match em `expression.getType().getName()`):
839
+ - `java.lang.System`
840
+ - `java.lang.ProcessBuilder`
841
+ - `java.io.File`
842
+ - `groovy.lang.GroovyShell`
843
+ - `groovy.lang.GroovyObject`
844
+
845
+ Expressões proibidas (match por **substring** em `expression.getText()`):
846
+ - `.execute`
847
+ - `.getDB`
848
+ - `.getMongo`
849
+ - `.dropDatabase`
850
+
851
+ **2. Whitelist de tokens** (apenas operadores listados são permitidos):
852
+
853
+ `=`, `+`, `-`, `*`, `/`, `%`, `**`, `++`, `+=`, `--`, `==`, `!=`, `<`, `<=`, `>`, `>=`, `||`, `||=`, `&&`, `&&=`, `[`, `]`
854
+
855
+ ### Limitações conhecidas do sandbox
856
+
857
+ - **Match por substring**, não por AST semântico: uma expressão que evite literalmente `.execute` (ex.: indireção via reflexão ou `eval`) pode passar pelo filtro. Concatenação de strings (`'.exec' + 'ute'`) não é resolvida em tempo de compilação por este checker.
858
+ - **Apenas tipos diretos** são bloqueados — referências por string e reflexão geral (`Class.forName`) não são impedidas por essa lista.
859
+ - O `GroovyClassLoader` usa `Thread.currentThread().getContextClassLoader()` — scripts compilam contra todo o classpath da aplicação. Qualquer classe Funifier ou de terceiros disponível em runtime é acessível desde que não passe pelos filtros acima.
860
+ - O `executor.shutdownNow()` envia `Thread.interrupt()`, mas em loops Groovy puros sem checagem de `Thread.interrupted()`, a thread pode continuar até o `@TimedInterrupt` (2000s).
861
+
862
+ ### Superfícies de injeção
863
+
864
+ - **`q` em `GET /v3/scheduler`** — concatenado raw no `$match`. Operadores como `$where` (JavaScript eval no Mongo) podem ser usados para exfiltrar dados ou consumir recursos.
865
+ - **`q` em `GET /v3/scheduler/log`** — mesmo padrão.
866
+ - **`orderby` em ambos** — concatenado raw em `{$sort: {<orderby>: #}}`. Não permite escrita mas permite enumerar campos.
867
+ - **`script` no `POST`** — sandbox protege contra acesso direto a APIs perigosas, mas todo o classpath fica acessível.
868
+
869
+ ### Isolamento por tenant
870
+
871
+ - Conexão MongoDB e `JobKey` por `apiKey` garantem isolamento horizontal.
872
+ - `compiled`/`dates` são por `SchedulerManager` (uma instância por `ManagerFactory`, ou seja, por gamificação).
873
+
874
+ ---
875
+
876
+ ## 9. Observabilidade e Troubleshooting
877
+
878
+ ### Diagnóstico rápido
879
+
880
+ ```
881
+ # Existência e estado:
882
+ GET /v3/scheduler/<id>
883
+
884
+ # Quartz vê o job? (lista próximas datas):
885
+ GET /v3/scheduler/next/<id>?reference=-0d
886
+
887
+ # Estado agregado do Quartz para esta gamificação:
888
+ GET /v3/util/cron/status
889
+ → checar references["gamification_<apikey>_sheduler_<id>"]
890
+ → checar schedulers[].next != null
891
+
892
+ # Executar manualmente para reproduzir:
893
+ GET /v3/scheduler/execute/<id>
894
+ → inspecionar response.exceptions e response.outputs
895
+ ```
896
+
897
+ ### Consultas úteis a logs
898
+
899
+ ```
900
+ # Últimas 10 execuções:
901
+ GET /v3/scheduler/log?item=<id>&orderby=time&reverse=true&max_results=10
902
+
903
+ # Execuções com erro:
904
+ GET /v3/scheduler/log?item=<id>&q={"err":{$ne:null}}&orderby=time&reverse=true
905
+
906
+ # Execuções no último dia:
907
+ GET /v3/scheduler/log?item=<id>&published_min=-1d&orderby=time&reverse=true
908
+
909
+ # Execuções lentas (>5s):
910
+ GET /v3/scheduler/log?item=<id>&q={"ms":{$gte:5000}}&orderby=ms&reverse=true
911
+ ```
912
+
913
+ ### Erros comuns e causas
914
+
915
+ | Sintoma | Causa provável | Verificação |
916
+ |---------|----------------|-------------|
917
+ | Scheduler não executa após criar | `active=false`, cron inválida apesar do POST aceitar, ou exception silenciosa no `scheduleJob` | `GET /v3/scheduler/<id>` (active?); `GET /v3/scheduler/next/<id>` (lista vazia indica não-agendado) |
918
+ | Scheduler parou após N execuções | Limite diário atingido | Comparar com `dailySchedulerExecutions` em `getSchedulerStatistics()` (não exposto em REST — usar log: `q={"err":{$ne:null}}`) |
919
+ | Scheduler executa em horário errado | `timezone` null ou JVM em UTC | Conferir campo `timezone`; usar `/next/{id}` para validar |
920
+ | `POST` retornou `status: OK` mas job não roda | `active=false` no payload OU `scheduleJob` falhou silenciosamente após `c.save` | Sempre passar `"active": true` quando quiser execução; verificar `/next/{id}` |
921
+ | Scheduler some após reboot | Documento `{_id: apiKey, total: N}` ausente em `scheduler` do sistema | Inserir manualmente (`{_id: <apiKey>, total: <count>}`) ou refazer um POST que reativa o índice via `addTotal` |
922
+ | `runEntity` falha com `SchedulerCallback does not exist` | `entity` referencia coleção sem callback (`bonus`, custom, typo) | Mover para `script` ou usar `entity: "last_mile"` |
923
+ | HTTP 500 em `/v3/scheduler/execute/<id>` | `findById` retornou null (ID inexistente) | Verificar com `GET /v3/scheduler/<id>` antes de executar |
924
+ | Script executa código antigo | Cache de `compiled` desatualizado vs `updated` em outro nó | `PUT /v3/scheduler/compile/<id>` em todos os nós; alternativamente `POST` para forçar `updated = now` |
925
+ | Compilação falha com mensagem `Token X is not allowed` | Uso de operador fora da whitelist (`<<`, `?.`, `..`, etc.) | Reescrever sem o operador |
926
+ | Compilação falha com `Type X is not authorized` | Uso direto de `System`, `File`, `ProcessBuilder`, `GroovyShell`, `GroovyObject` | Usar APIs do `manager`/`database` |
927
+ | Logs com `_id` duplicado entre dias | Contador `dailySchedulerExecutions` reiniciou após reboot | Usar `time` + `item` para identificar; aceitar como limitação |
928
+
929
+ ### Verificar drift Quartz vs MongoDB (system-only)
930
+
931
+ ```
932
+ GET /v3/system/scheduler/jobs
933
+ → procurar entradas com schedulerName = "SCHEDULER DONT EXIST"
934
+ → indica job no Quartz sem documento no banco (drift)
935
+ ```
936
+
937
+ ---
938
+
939
+ ## 10. Exemplos Práticos
940
+
941
+ ### Exemplo mínimo funcional
942
+
943
+ ```json
944
+ POST /v3/scheduler
945
+ {
946
+ "active": true,
947
+ "name": "Hello daily",
948
+ "cron": "0 0 9 * * ?",
949
+ "timezone": "America/Sao_Paulo",
950
+ "script": "void execute(scheduler){ println 'ran at ' + new Date() }"
951
+ }
952
+ ```
953
+
954
+ ### Exemplo avançado com `extra`, `timeout` e uso de `manager`/`database`
955
+
956
+ ```json
957
+ POST /v3/scheduler
958
+ {
959
+ "_id": "debit_inactive_xp",
960
+ "active": true,
961
+ "name": "Debitar XP de inativos (30 dias)",
962
+ "cron": "0 0 2 * * ?",
963
+ "timezone": "America/Sao_Paulo",
964
+ "timeout": 120,
965
+ "extra": {
966
+ "diasInatividade": 30,
967
+ "pontoTipo": "xp",
968
+ "pontosDebitar": 10
969
+ },
970
+ "script": "void execute(scheduler){\n def dias = scheduler.extra.diasInatividade\n def pontos = scheduler.extra.pontosDebitar\n def tipo = scheduler.extra.pontoTipo\n def ref = DateUtil.addDays(new Date(), -dias)\n def players = database.find('player', '{\"lastAction\":{\"$lt\":#}}', ref)\n for(def p : players){\n manager.getPointManager().add(p._id, tipo, -pontos, null)\n }\n println 'processed ' + players.size() + ' players'\n}"
971
+ }
972
+ ```
973
+
974
+ ### Debug com tempo simulado
975
+
976
+ ```
977
+ # Simular execução como se fosse 2024-12-31 23:59 BRT
978
+ GET /v3/scheduler/execute/debit_inactive_xp?time=2024-12-31T23:59:00-03:00
979
+
980
+ # Resposta:
981
+ {
982
+ "scheduler": { "_id": "debit_inactive_xp", "time": "2024-12-31T23:59:00-03:00", ... },
983
+ "exceptions": [],
984
+ "outputs": ["processed 42 players"],
985
+ "millis": 1834
986
+ }
987
+ ```
988
+
989
+ ### Listar próximas execuções a partir de uma data
990
+
991
+ ```
992
+ GET /v3/scheduler/next/debit_inactive_xp?reference=2025-01-01T00:00:00-03:00
993
+
994
+ [
995
+ "2025-01-02T02:00:00-03:00",
996
+ "2025-01-03T02:00:00-03:00",
997
+ "2025-01-04T02:00:00-03:00",
998
+ "..."
999
+ ]
1000
+ ```
1001
+
1002
+ ### Forçar recompilação após mudança em cluster
1003
+
1004
+ ```
1005
+ PUT /v3/scheduler/compile/debit_inactive_xp
1006
+ ```
1007
+
1008
+ ### Anti-pattern: script sem assinatura `void execute(scheduler)`
1009
+
1010
+ ```groovy
1011
+ // ERRADO — o executor faz invokeMethod("execute", [scheduler]) literalmente.
1012
+ // Sem este método exato, GroovyObject lança MissingMethodException.
1013
+ println 'hello'
1014
+
1015
+ // CORRETO
1016
+ void execute(scheduler){ println 'hello' }
1017
+ ```
1018
+
1019
+ ### Anti-pattern: declarar `import` no script
1020
+
1021
+ ```groovy
1022
+ // ERRADO — getScript() já injeta imports padrão antes da class FunifierScheduler.
1023
+ // imports adicionais dentro da class causam CompilationFailedException.
1024
+ import com.funifier.engine.player.Player
1025
+ void execute(scheduler){ ... }
1026
+
1027
+ // CORRETO — classes Funifier comuns (Player, Achievement, Lottery, Action, etc.)
1028
+ // já estão importadas pelo wrapper. Use diretamente.
1029
+ void execute(scheduler){
1030
+ Player p = manager.getPlayerManager().findById("playerId")
1031
+ }
1032
+ ```
1033
+
1034
+ ### Anti-pattern: scheduler entity com entidade não suportada
1035
+
1036
+ ```json
1037
+ // ERRADO — primeira execução vai gravar exception + setar active=false
1038
+ {
1039
+ "active": true,
1040
+ "cron": "0 0 9 * * ?",
1041
+ "entity": "bonus",
1042
+ "event": "notify",
1043
+ "item": "bonus_id_123"
1044
+ }
1045
+ // Erro registrado: "SchedulerCallback does not exist for entity bonus"
1046
+ // Estado pós-execução: active=false (persistido) + job removido do Quartz
1047
+ ```
1048
+
1049
+ ### Anti-pattern: cron Unix (5 campos)
1050
+
1051
+ ```
1052
+ // ERRADO — Quartz precisa de 6 campos
1053
+ "cron": "0 9 * * *"
1054
+
1055
+ // CORRETO — Quartz com campo de segundos e `?` em um dos dia/dia-semana
1056
+ "cron": "0 0 9 * * ?"
1057
+ ```
1058
+
1059
+ ### Anti-pattern: depender de `scheduler.time` em execução cron
1060
+
1061
+ ```groovy
1062
+ // ERRADO — scheduler.time é null em execuções Quartz normais
1063
+ void execute(scheduler){
1064
+ def ref = scheduler.time // null
1065
+ // ...
1066
+ }
1067
+
1068
+ // CORRETO — use new Date() ou scheduler.extra.referenceTime
1069
+ void execute(scheduler){
1070
+ def ref = new Date()
1071
+ // ...
1072
+ }
1073
+ ```
1074
+
1075
+ ### Anti-pattern: usar operador fora da whitelist
1076
+
1077
+ ```groovy
1078
+ // ERRADO — operadores ?., .., ?:, << não estão na whitelist
1079
+ void execute(scheduler){
1080
+ def name = scheduler?.extra?.user?.name // ?. bloqueado
1081
+ for(int i in 1..10) { ... } // .. bloqueado
1082
+ }
1083
+
1084
+ // CORRETO — usar checagem null explícita e for clássico
1085
+ void execute(scheduler){
1086
+ def name = (scheduler != null && scheduler.extra != null && scheduler.extra.user != null) ? scheduler.extra.user.name : null
1087
+ for(int i = 1; i <= 10; i++) { ... }
1088
+ }
1089
+ ```
1090
+
1091
+ ---
1092
+
1093
+ ## Checklist de Configuração
1094
+
1095
+ - [ ] `active: true` definido (default é `false` — o documento é salvo mas o job NÃO é agendado)
1096
+ - [ ] `cron` é Quartz com 6 campos (`seg min hor dia mês diasem`) e usa `?` em um dos campos `dia`/`dia-semana`
1097
+ - [ ] `timezone` IANA preenchido — não confiar no default da JVM (geralmente UTC em containers)
1098
+ - [ ] **Script mode**: `script` contém exatamente `void execute(scheduler){ ... }` (assinatura literal)
1099
+ - [ ] **Script mode**: nenhum `import` no script (já injetados pelo wrapper)
1100
+ - [ ] **Script mode**: nenhum dos operadores fora da whitelist (`<<`, `>>`, `&`, `|`, `^`, `?:`, `?.`, `..`)
1101
+ - [ ] **Entity mode**: três campos preenchidos (`entity`, `event`, `item`) e `entity` referencia uma coleção COM callback registrado em `ManagerFactory.getSchedulerCallback`
1102
+ - [ ] **Entity mode**: para `last_mile`, sempre criado pelo `LastMileManager.insert` — não criar manualmente (ele sobrescreve campos)
1103
+ - [ ] `timeout` ajustado se a operação puder demorar mais de 30s
1104
+ - [ ] Validar POST: ler o `status` no body — HTTP é sempre 201, mesmo em erro de compilação
1105
+ - [ ] Após criar, validar via `GET /v3/scheduler/next/<id>` que retorna lista não-vazia
1106
+ - [ ] Armadilha: scheduler com `entity` definido mas sem callback é **auto-desabilitado na primeira execução**, consumindo quota
1107
+ - [ ] Armadilha: scheduler somem após restart se o índice `{_id: apiKey, total: N}` da coleção `scheduler` do sistema for limpo
1108
+ - [ ] Armadilha: `c.save()` no POST é full-replace — preserve campos que não quiser perder
1109
+ - [ ] Armadilha: parâmetros `q` e `orderby` nos GET são concatenados raw no MongoDB; trate-os como confiáveis apenas com produtores também confiáveis
1110
+ - [ ] Armadilha: em cluster multi-node, cada nó executa o cron — dedupe na lógica do script
1111
+ - [ ] Armadilha: `SchedulerLog._id` é um contador diário em memória; após restart pode haver upsert sobre logs antigos