# IronDome Operator — Changelog

## 1.0.9 — 2026-05-01

**Sinais de gating (Bloco 2 da Configuração) agora são OR em vez de AND.**

Antes (1.0.7/1.0.8): se `observe_mode != 'off'` E `tail_enabled = true`, o
operator exigia que AMBOS passassem pra apostar — produto de probabilidades
quase zero. Combinação tipo `zgap_streak N=5` + `tail_window=1` ficava
matematicamente impossível (~0.05% chance simultânea).

Agora: cada gate ativo é um GATILHO independente. Se observe_mode liberar
OU se cauda detectar, aposta. Se ambos ativos e ambos bloqueiam, aí sim
não aposta. Reflete a leitura natural do user na UI ("z-gap green por 5
seguidas → aposta" OU "depois de 10× → aposta na próxima").

Logs novos:
- `gatilho cauda ativo ✓ (observe ainda em OBSERVE zgap_streak 3/5)`
- `gatilho OBSERVE zgap_streak 5 ≥ 5 ✓ ✓ (cauda inativa)`
- `ambos gatilhos ativos ✓`
- `OBSERVE ... E cauda inativa ... → não aposta (OR)`

## 1.0.8 — 2026-05-01

**Janela horária + auto-reset diário agora respeitam o fuso LOCAL da
máquina onde o operator roda** (em vez do fuso fixo de São Paulo).

Antes: `_now_sp()` convertia tudo pra UTC-3, então usuário na Califórnia
configurando "08:00–22:00" era interpretado como SP, ficando confuso pra
quem opera fora do Brasil.

Agora: usa `datetime.now()` (timezone-naive, fuso do OS). Cliente que
selecionou Califórnia no relógio do dashboard configura janela em LA,
operator lê em LA. Mesma coisa pra auto-reset à meia-noite — meia-noite
local da máquina, não SP.

Heatmap dia × hora continua em fuso SP fixo no backend (origem dos
dados é a casa brasileira; mexer ali exige re-agrupar query no Postgres).

## 1.0.7 — 2026-05-01

**Consome os 22 campos novos da aba Configuração da cloud (Fase 3).**

Operator agora aplica os gates antes de cada aposta:
1. **Cooldown N misses**: após N ciclos perdidos seguidos, pausa local
   (modo `timer` espera X min, modo `shadow` simula até K acertos em M).
2. **Janela horária + dias da semana**: só aposta dentro de
   `op_window_start`–`op_window_end` (fuso SP) e nos `op_days` ativos.
3. **Cap diário**: conta `daily_bets_today`. Atingiu `cap_diario_limit`
   → POST `enabled=false` silencioso.
4. **Modo OBSERVE → BET**: gating de entrada (`rounds`, `tail`,
   `zgap_streak`).
5. **Detector de cauda standalone**: se `tail_enabled=true`, só aposta
   se houve ≥10× nas últimas `tail_window_n` rodadas.
6. **Auto-reset diário**: thread que zera engine + cap à meia-noite SP
   se `auto_reset_daily=true`.

Heartbeat agora envia `cap_diario_count` pra cloud monitorar.

## 1.0.6 — 2026-04-30

**Fallback CDP `Input.insertText` quando setter+InputEvent não move o
Angular FormControl.**

A 1.0.5 não foi suficiente: log do user mostrou
`tries=[14,14,14,14]` — Angular ignorou todos os InputEvents +
KeyboardEvent virtuais (não-trusted). Os FormControls ficaram presos
no valor antigo (R$14 do P1, R$4 do P0).

Solução: chamar `Input.insertText` direto via CDP — simula
digitação real do teclado, eventos chegam como `isTrusted=true`,
Angular não tem como rejeitar.

Fluxo novo em `set_panel`:
1. Tenta JS-only (rápido). Se passou, clica.
2. Se falhou e `mode=manual` (única causa restante = FormControl):
   chama `_set_panel_via_cdp` que:
   a. JS: focus + setSelectionRange(0, len) — seleciona tudo
   b. CDP `Input.dispatchKeyEvent` Delete — apaga selecionado
   c. CDP `Input.insertText` — escreve novo valor
   d. JS: blur + change event — finaliza Angular
   e. Re-valida via button textContent
   f. Repete pra target
   g. Clica betEl com jitter

Log mostra `[cdp]` quando o fallback foi usado:
`(True mode=manual jitter=320ms [cdp])`.

## 1.0.5 — 2026-04-30

**Fix: validação de FormControl bloqueava todas as apostas com Angular
em estado pós-reset (botão renderizando stake antigo de sessão anterior).**

Sintoma: log do operator com sucessivos
`(False mode=manual jitter=Nonems) ... bet NÃO marcado como pending`.
Causa: ao resetar sessão, o Spribe deixou o FormControl com stake da
sessão anterior (ex: R$14). Quando set_panel escrevia "3.00" no input
nativo + InputEvent, o Angular ainda renderizava 14 no botão. A
validação pós-write da 1.0.3 abortava com ok=false sempre.

Fix:
1. **Retry com busy-wait** dentro do JS (50ms, 100ms, 150ms = até 600ms
   total). Cada retry faz `reForceWrite` = sequência de KeyboardEvents
   Backspace + setter('') + setter(novo valor) + InputEvent
   insertReplacementText. Isso força Angular a invalidar o FormControl
   antigo antes de escrever o novo.
2. Operator agora **printa a mensagem de erro** detalhada (`r.get('msg')`)
   quando set_panel falha — antes era só `(False)` opaco.
3. Mensagem de erro inclui `tries=[14, 14, 3, 3]` mostrando como o
   stake_rendered evoluiu nos retries.

## 1.0.4 — 2026-04-30

**Suporte a "nova sessão" via dashboard cloud.**

Quando o user clica "Iniciar nova sessão" no modal do dashboard, a cloud
zera o operator_state (P&L, round, ciclos) via `POST /operator/reset`. O
operator detecta no `state_poll_loop`:
- Se `bet_pending=False`: zera engine local imediatamente.
- Se `bet_pending=True`: agenda o reset (`_session_reset_pending=True`) e
  aplica após o próximo `process_crash` resolver a aposta atual. Honra a
  regra "espera a rodada terminar antes de zerar".

Logs novos:
- `[engine] nova sessão solicitada — aguardando rodada atual resolver...`
- `[engine] nova sessão iniciada pelo dashboard → reset local`
- `[engine] nova sessão aplicada após resolução da rodada anterior → reset local`

## 1.0.3 — 2026-04-30

**Fix: Angular FormControl da Spribe não atualizava com setter nativo —
stake do P1 ficava preso em valor antigo (ex: R$9) mesmo com input visível
mostrando R$3. Botão renderiza do FormControl, então a Spribe apostava o
valor antigo.**

Sintoma: estratégia indicava R$3 / 10×, mas operator apostava R$9 e saía
em ~3.33×. Probe DOM confirmou divergência: `stake_input: "3.00"` mas
`button_text: "Aposta 9.00 BRL"`. O cashout em 3.33× era consequência:
mult real chegava a 3.33×, botão mostrava "Cashout 29.97 BRL" (= 9 × 3.33),
JS calculava `mult = 29.97/3 = 9.99` (achando que stake era 3), disparava
click → Spribe processava no mult corrente (3.33).

Causa: `dispatchEvent(new Event('input'))` genérico não dispara
`valueAccessor` do Angular reactiveForms. Precisa de `InputEvent` com
`inputType: 'insertReplacementText'` + `data: novo_valor`.

Fix:
- Helper `setInputValue` aplica selectionRange, setter nativo, e
  dispatcha `InputEvent('insertReplacementText', data=val)`.
- Validação pós-write: lê o texto do botão de aposta e confere se o stake
  renderizado bate com o que tentamos setar (tolerância 0.01). Se não
  bater, **aborta a rodada** retornando `skipped: true` em vez de apostar
  com stake fantasma.

## 1.0.2 — 2026-04-30

**Fix: cashout disparando cedo demais por resíduo do DOM da rodada anterior.**

Sintoma: P1 saindo em 3.33× (ou outro valor baixo) quando target era 10×.

Causa: ao final da rodada anterior, o botão Spribe permanece com texto
"Cashout (stake × mult_anterior) BRL" no DOM por alguns ms até a próxima
rodada começar. O `arm_cashout` armava o `setInterval(20ms)` imediatamente
no `handle_crash`. A primeira tick lia esse texto residual, calculava
`mult = val / stake = mult_anterior` e, se >= target, disparava `b.click()`.
A Spribe recebia esse click, mas o botão já estava em estado da nova rodada
com mult corrente baixo — Spribe processava o cashout no mult atual (3.33),
operator logava "clicked @ 9.99" (acreditando no que viu no DOM). Resultado:
saída antecipada com pagamento real menor que o registrado.

Fix: state machine no watcher (`check_initial` → `wait_reset` → `live`).
- `check_initial`: 1ª tick. Se já está em Cashout, é resíduo → vai pra
  `wait_reset`. Se não está, vai direto pra `live`.
- `wait_reset`: ignora ticks até o botão sair de Cashout. Aí vira `live`.
- `live`: comportamento original.

Também grava `target` e `stake` no debug e no resultado de timeout, pra
diagnóstico futuro.

## 1.0.1 — 2026-04-30

**Fix crítico de P&L: hit/miss agora baseado no clique real do watcher.**

`handle_crash` agora lê o resultado do watcher (`peek_cashout_state`) e passa
`p0_cashed_at` / `p1_cashed_at` para `process_crash`. Se o watcher não clicou
em "Cashout" (timeout ou click descartado pela Spribe), o bet é contabilizado
como **miss real** — independente do crash final ter passado do target.

Antes: rodada que estourava em 5× com target 4× era marcada como ganho (+R$30)
mesmo se o b.click() do watcher falhou — operator perdia dinheiro real mas o
P&L local subia. Engine caía no "modo legado" (linhas 597–615) que decide só
por `crash >= target`.

Agora: se watcher.clicked == false, `delta_pnl = -stake` real, refletindo o
que aconteceu na Spribe.

Logs novos a cada rodada resolvida:
- `[P0 cashout] clicked @ 4.04× (target=4.0)` — sucesso
- `[P0 cashout] TIMEOUT — last_mult=1.16 ticks=2999` — não clicou

## 1.0.0 — 2026-04-30

Snapshot click-perfect: watcher de cashout funcionando após fix do filtro
`.navigation-switcher` (filtra os 3 elementos para pegar só os switchers de
painel, não o nav do topo). Auto-click respeita rigorosamente os targets
(saída em 4.04 vs 4.00 e 10.04 vs 10.00).
