feat(components): ActionButton stopAlert + typography type fixes

This commit is contained in:
fightrate-bot committed 2026-06-16 01:50:26 -03:00
1 parent 2c51fd2623
commit bbe2919c2c
89 files changed
+75993 -9961

No files matched your search

@@ -0,0 +1,147 @@
# FightRate Android MVP Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build the first Android-native FightRate app scaffold for live combat-sport heart-rate tracking with Magene H003 BLE support.
**Architecture:** Android-native Kotlin app using Jetpack Compose for UI, pure Kotlin domain/parse logic, Android BLE APIs isolated behind a BLE package, and local persistence planned with Room. The first implementation slice creates a buildable app shell plus a tested Heart Rate Measurement parser.
**Tech Stack:** Kotlin, Gradle Kotlin DSL, Android Gradle Plugin, Jetpack Compose Material 3, Coroutines/Flow, Android BLE APIs, JUnit.
---
## File Structure
```text
settings.gradle.kts Project module declaration
build.gradle.kts Root Gradle plugin versions
gradle.properties Android/Kotlin build flags
app/build.gradle.kts Android app dependencies and config
app/src/main/AndroidManifest.xml Permissions, activity, BLE feature flags
app/src/main/java/com/healthtracking/fightrate/MainActivity.kt
app/src/main/java/com/healthtracking/fightrate/ble/HeartRateParser.kt
app/src/main/java/com/healthtracking/fightrate/domain/model/FightSport.kt
app/src/main/java/com/healthtracking/fightrate/domain/model/HeartRateMeasurement.kt
app/src/main/java/com/healthtracking/fightrate/ui/theme/FightRateTheme.kt
app/src/main/java/com/healthtracking/fightrate/ui/theme/Color.kt
app/src/main/java/com/healthtracking/fightrate/ui/screens/HomeScreen.kt
app/src/test/java/com/healthtracking/fightrate/ble/HeartRateParserTest.kt
```
---
## Task 1: Android project scaffold
**Files:**
- Create: `settings.gradle.kts`
- Create: `build.gradle.kts`
- Create: `gradle.properties`
- Create: `app/build.gradle.kts`
- Create: `app/src/main/AndroidManifest.xml`
- Create: `app/src/main/java/com/healthtracking/fightrate/MainActivity.kt`
- [ ] **Step 1: Create Gradle project files**
Use Android app module `:app`, package `com.healthtracking.fightrate`, min SDK 26, target/compile SDK 35, Kotlin 2.x compatible Compose.
- [ ] **Step 2: Create manifest**
Declare Bluetooth permissions for Android 12+, legacy Bluetooth/location permissions for Android 11 and below, BLE feature, foreground service permissions, and `MainActivity`.
- [ ] **Step 3: Create minimal MainActivity**
Use `ComponentActivity`, `setContent`, `FightRateTheme`, and `HomeScreen`.
- [ ] **Step 4: Verify**
Run: `./gradlew :app:assembleDebug`
Expected: build succeeds when Gradle/Android SDK are installed.
---
## Task 2: Heart Rate Measurement parser, TDD first
**Files:**
- Create: `app/src/main/java/com/healthtracking/fightrate/ble/HeartRateParser.kt`
- Create: `app/src/main/java/com/healthtracking/fightrate/domain/model/HeartRateMeasurement.kt`
- Create: `app/src/test/java/com/healthtracking/fightrate/ble/HeartRateParserTest.kt`
- [ ] **Step 1: Write tests**
Cover these Bluetooth Heart Rate Measurement payloads:
```kotlin
byteArrayOf(0x00, 72) // uint8 BPM
byteArrayOf(0x01, 0x2C, 0x01) // uint16 BPM = 300
byteArrayOf(0x08, 90, 0x34, 0x12) // energy expended = 0x1234
byteArrayOf(0x10, 120, 0x00, 0x04) // RR interval = 1024 -> 1000ms
byteArrayOf() // invalid empty payload
```
- [ ] **Step 2: Implement parser**
Parse flags, BPM format, optional energy expended, optional RR intervals. Throw `IllegalArgumentException` for malformed payloads.
- [ ] **Step 3: Verify**
Run: `./gradlew :app:testDebugUnitTest`
Expected: all parser tests pass.
---
## Task 3: FightRate visual shell
**Files:**
- Create: `app/src/main/java/com/healthtracking/fightrate/ui/theme/Color.kt`
- Create: `app/src/main/java/com/healthtracking/fightrate/ui/theme/FightRateTheme.kt`
- Create: `app/src/main/java/com/healthtracking/fightrate/domain/model/FightSport.kt`
- Create: `app/src/main/java/com/healthtracking/fightrate/ui/screens/HomeScreen.kt`
- [ ] **Step 1: Define design tokens**
Use the FightRate palette from the design system: black canvas, charcoal surfaces, fight orange, redline red, recovery green, muted gray text.
- [ ] **Step 2: Build static HomeScreen**
Include static UI cards for sensor status, current BPM, round timer, zone bar, sport chips, and session metrics.
- [ ] **Step 3: Verify**
Run: `./gradlew :app:assembleDebug`
Expected: app compiles and opens to a dark FightRate dashboard.
---
## Task 4: BLE connection layer placeholder
**Files:**
- Create: `app/src/main/java/com/healthtracking/fightrate/ble/HeartRateBleConstants.kt`
- Create: `app/src/main/java/com/healthtracking/fightrate/ble/HeartRateBleScanner.kt`
- Create: `app/src/main/java/com/healthtracking/fightrate/ble/HeartRateBleClient.kt`
- [ ] **Step 1: Define UUID constants**
```kotlin
HEART_RATE_SERVICE = 0000180d-0000-1000-8000-00805f9b34fb
HEART_RATE_MEASUREMENT = 00002a37-0000-1000-8000-00805f9b34fb
BODY_SENSOR_LOCATION = 00002a38-0000-1000-8000-00805f9b34fb
BATTERY_SERVICE = 0000180f-0000-1000-8000-00805f9b34fb
BATTERY_LEVEL = 00002a19-0000-1000-8000-00805f9b34fb
```
- [ ] **Step 2: Scanner contract**
Expose a Kotlin API that can later be wired to `BluetoothLeScanner` and emits scan results.
- [ ] **Step 3: Client contract**
Expose connect/disconnect and `Flow<HeartRateMeasurement>` API.
---
## Current Execution Choice
The user asked to proceed with app creation, so start with **Inline Execution** for Tasks 1–3, stopping after verification evidence and reporting environment blockers if Android SDK/Gradle is unavailable.
@@ -0,0 +1,52 @@
# Implementation Plan: FightRate P1 Essential Features
## Overview
Implement the next FightRate feature batch as incremental, testable slices: workout sessions, round timer, configurable alerts, more robust BLE state, and sensor metadata (battery/body location). Keep PT-BR visible copy and English code identifiers.
## Architecture Decisions
- Keep local-first SQLite as source of truth.
- Add session data without replacing existing sample storage.
- Prefer pure domain logic for round timer and alerts so behavior is unit-testable.
- Keep BLE foreground service as owner of the device connection.
- Expose sensor metadata through existing service broadcasts to avoid direct BLE ownership in the ViewModel.
## Task List
### Phase 1: Domain foundation
- [ ] Add `WorkoutSession`, `WorkoutSessionStatus`, `RoundTimerConfig`, `RoundTimerSnapshot`, and calculators.
- [ ] Add unit tests for timer transitions and session summary calculations.
### Phase 2: Persistence
- [ ] Add `workout_sessions` table and nullable `session_id` on `heart_rate_samples`.
- [ ] Add safe migration from current database version.
- [ ] Add store methods to start/pause/resume/finish active sessions and insert samples with active session id.
### Phase 3: UI/ViewModel integration
- [ ] Add session and round timer state to `LiveHeartRateUiState`.
- [ ] Add ViewModel actions: start/pause/resume/finish session and update timer config.
- [ ] Add visible Compose controls in Live screen.
### Phase 4: Alerts
- [ ] Add persisted alert settings with safe defaults.
- [ ] Add configurable vibration/sound toggles and zone/limit thresholds.
- [ ] Trigger alerts from service/ViewModel without spamming.
### Phase 5: BLE hardening and metadata
- [ ] Add BLE status events for scan/connect/service/descriptor failures and unexpected disconnects.
- [ ] Add conservative reconnect/backoff in foreground service.
- [ ] Read battery level and body sensor location when available.
- [ ] Show metadata in Sensor screen.
## Review/Verification
- [ ] Run unit tests after domain/persistence slices.
- [ ] Run `./gradlew :app:testDebugUnitTest :app:assembleDebug` before final response.
- [ ] Run code review and front/UI review via fresh-context reviewers.
- [ ] Reconcile findings and fix required issues.
## Risks
| Risk | Impact | Mitigation |
|---|---|---|
| Database migration loses samples | High | Add additive migration, no DROP for current versions |
| BLE changes break screen-off tracking | High | Keep service ownership; add metadata/status only |
| UI becomes cluttered | Medium | Compact cards and PT-BR labels |
| Alert spam | Medium | Throttle alerts and use safe defaults |
@@ -0,0 +1,207 @@
# FightRate — Plano Data-First para histórico, gráficos e UI profissional
## Situação crítica
O feedback atual mostra dois bugs de produto, não apenas problema visual:
1. O gráfico tem espaço vazio demais e exige rolar horizontalmente por muito tempo até encontrar leituras.
2. O histórico não está confiável: dados parecem não salvar ou não ficam acessíveis depois.
Conclusão: dados e persistência devem voltar a ser prioridade absoluta antes de polimento visual.
## Objetivo
Transformar o FightRate em um app data-first:
- Cada batimento recebido deve ser persistido localmente.
- Cada treino deve ter uma janela temporal confiável.
- Histórico deve mostrar dados reais recuperáveis.
- Gráficos devem usar domínio baseado apenas nas amostras reais disponíveis.
- Não pode existir scroll infinito/área vazia até achar leitura.
- Interface deve priorizar dados e navegação histórica, seguindo o design system Combat Lab.
## Skills aplicadas
- `fightrate-quality-loop`: spec → auditoria → implementação em fatias → build.
- `systematic-debugging`: encontrar causa raiz antes de corrigir.
- `test-driven-development`: criar testes de regressão para domínio de gráfico/histórico.
- `design-system` e `ui-ux-pro-max`: estado vazio útil, chart/data UX, consistência visual.
## Hipóteses a investigar
### H1 — Domínio X do Vico está incluindo janela cheia, não amostras reais
Sintoma: usuário rola para a direita “para sempre” até encontrar leituras.
Possível causa:
- O gráfico usa `windowStartMillis`/`windowEndMillis` grandes, mas as amostras estão concentradas em pequeno trecho.
- O X é calculado relativo à janela selecionada, então se a leitura está no fim da janela, sobra um enorme vazio à esquerda/direita.
- O scroll/zoom inicial pode estar mostrando início/fim errado.
Correção esperada:
- Domínio padrão do gráfico deve ser `min(sample.timestamp)` → `max(sample.timestamp)`.
- A janela selecionada filtra dados, mas não força o domínio visual a incluir todo o período vazio.
- Exibir texto com a janela consultada e o intervalo real encontrado.
### H2 — Histórico busca janela errada ou estado não atualiza após persistência
Sintoma: dados históricos não aparecem/salvam.
Possíveis causas:
- `publishLiveSample` salva, mas histórico não recarrega após cada nova amostra.
- `selectedPeriod` usa uma janela curta ou errada.
- Sessão finalizada tem samples com `sessionId` nulo.
- `finishCurrentSession` não associa amostras sem sessão dentro da janela.
- UI exibe `historySamples`, mas dados reais ficam apenas em `visibleSamples`/`dailySamples`.
Correções esperadas:
- Confirmar que `HeartRateSampleStore.save` recebe `sessionId` ativo.
- Confirmar que `samplesForSession` funciona depois do finish.
- Confirmar que `samplesBetween(start, end)` recupera dados persistidos.
- Atualizar estado de histórico depois de salvar amostra e depois de finalizar treino.
### H3 — UI não oferece navegação histórica suficiente
Sintoma: “os dados devem poder ser vistos de qualquer maneira”.
Correção esperada:
- Histórico com modos claros:
- Últimos 10 min
- Última hora
- Hoje
- Sessão atual/finalizada
- Se janela consultada não tem dados, mostrar “nenhum dado nessa janela” e sugerir janela que tem dados.
- Se existe sessão finalizada, exibir relatório e gráfico daquela sessão.
## Arquitetura desejada
### Domínio puro novo/revisado
Criar `ChartDataWindow.kt` se necessário.
Responsabilidades:
- Receber samples + janela consultada.
- Retornar:
- `visibleSamples`: somente amostras reais e úteis.
- `dataStartMillis` / `dataEndMillis`: domínio real do gráfico.
- `queryStartMillis` / `queryEndMillis`: janela consultada.
- `status`: Empty, SinglePoint, Ready.
- resumo: duração real, média, min, max, densidade.
- Não depender de Compose/Vico/Android.
### Persistência
Reutilizar `HeartRateSampleStore`.
Necessário auditar/corrigir:
- insert de samples;
- active session id;
- associação de sample com session id;
- queries por sessão;
- queries por período;
- refresh após write.
Não fazer migração destrutiva.
### ViewModel
- Não mover BLE para ViewModel.
- Garantir que o estado de histórico seja atualizado após persistência.
- Expor chart windows já preparados para UI.
- Evitar que UI calcule regras complexas.
### UI / Gráfico
- `HeartRateLineChart` deve receber domínio real de dados.
- X deve ser relativo a `dataStartMillis`, não ao começo da janela vazia.
- Se `samples.size < 2`, não chamar Vico.
- Mostrar estado vazio editorial.
- Mostrar uma linha de contexto:
- “Consultado: Hoje”
- “Dados encontrados: 14:03–14:17”
- Zoom/scroll habilitado, mas sem espaço fantasma.
- Não usar Canvas custom chart.
## Testes obrigatórios
### `ChartDataWindowTest`
1. Janela grande com dados no fim não gera domínio vazio gigante.
2. Sem dados retorna Empty.
3. Um ponto retorna SinglePoint.
4. Dados fora de ordem são ordenados.
5. Dados filtrados preservam timestamp real.
6. Downsampling mantém primeiro e último ponto.
### Store/persistência
Se for viável em teste local unitário:
1. Salvar sample e recuperar por período.
2. Salvar sample em sessão ativa e recuperar por session id.
3. Finalizar sessão e recuperar samples da sessão.
Se não for viável sem Robolectric/instrumented test, adicionar diagnóstico via código e validar pelo fluxo ViewModel/build.
## Implementação em fatias
### Fase 1 — Auditoria e causa raiz
- Ler `HeartRateSampleStore`.
- Ler `LiveHeartRateViewModel`.
- Ler `LiveHeartRateUiState`.
- Ler `HomeScreen` chart/history.
- Localizar onde sample é salvo.
- Localizar onde `historySamples` é atualizado.
- Localizar como Vico define X/domain.
### Fase 2 — Domínio de janela de gráfico
- Criar `ChartDataWindow.kt`.
- Criar `ChartDataWindowTest.kt`.
- Garantir domínio real `dataStartMillis`→`dataEndMillis`.
- Garantir estado Empty/SinglePoint/Ready.
### Fase 3 — Persistência e refresh
- Corrigir sample/session association se necessário.
- Corrigir reload de histórico depois de salvar/finalizar.
- Adicionar método de query se faltar.
### Fase 4 — Gráfico profissional
- Atualizar `HeartRateLineChart` para usar domínio real.
- Remover scroll fantasma.
- Adicionar zoom/scroll Vico com domínio real.
- Remover render de gráfico vazio.
- Mostrar resumo consultado vs encontrado.
### Fase 5 — Interface data-first
- Histórico como centro de dados, não card secundário.
- Sessões e dados recentes com hierarquia clara.
- Evitar dezenas de cards iguais.
- Estados vazios úteis e específicos.
### Fase 6 — Validação final
- `./gradlew :app:testDebugUnitTest :app:assembleDebug`
- Atualizar Obsidian roadmap.
- Enviar APK Telegram.
## Critérios de sucesso
- Usuário abre Histórico e vê dados reais sem procurar em espaço vazio.
- O gráfico começa/termina no intervalo real dos dados encontrados.
- Sem dados: nenhum gráfico é desenhado.
- Dados salvos são recuperáveis por período e por sessão.
- Testes de domínio passam.
- Build completo passa.
- APK enviado.
@@ -0,0 +1,691 @@
# Spec: FightRate P0 — Perfil Avançado e Insights do Lutador
## Objective
Criar a primeira versão do Perfil Avançado do Lutador para coletar dados pessoais, cardiovasculares, subjetivos e de recuperação suficientes para gerar insights úteis de treino sem deixar o app inchado.
O objetivo não é diagnóstico médico. O objetivo é apoio esportivo: entender carga, recuperação, hidratação, intensidade e evolução do atleta usando os batimentos reais do sensor BLE somados a dados simples informados pelo usuário.
Usuário principal: lutador ou praticante de treino de combate que usa o FightRate para acompanhar batimentos durante treinos, rounds e sessões.
## Scope P0
Implementar somente o núcleo inicial:
1. Perfil do Lutador.
2. Check-in pré-treino.
3. Registro pós-treino.
4. Insights básicos do treino e recuperação.
Não implementar neste P0:
- exames médicos avançados;
- integração com wearable externo além do sensor BLE atual;
- Health Connect;
- prescrição médica;
- VO2max avançado;
- IA generativa para recomendação clínica;
- dashboard muito complexo.
## Assumptions
1. O app continua Android nativo Kotlin + Jetpack Compose.
2. Textos visíveis continuam em PT-BR.
3. Identificadores de código continuam em inglês.
4. SQLite local-first continua como fonte de verdade.
5. O sensor BLE continua sendo fonte dos batimentos em tempo real.
6. Cálculos de saúde serão apresentados como estimativas esportivas, não diagnósticos.
7. O P0 deve priorizar campos que geram cálculos imediatos.
## Tech Stack
- Kotlin
- Jetpack Compose Material3
- SQLite via `SQLiteOpenHelper` existente em `HeartRateSampleStore`
- Gradle Kotlin DSL
- JUnit 4 para testes unitários
- Vico para gráficos existentes
- Lucide para ícones existentes
## Commands
Build completo:
```bash
./gradlew :app:testDebugUnitTest :app:assembleDebug
```
Teste unitário focado:
```bash
./gradlew :app:testDebugUnitTest --tests "*AthleteInsights*"
```
Compilação rápida:
```bash
./gradlew :app:compileDebugKotlin
```
Instalação debug:
```bash
./gradlew :app:installDebug
```
## Existing Code to Reuse
Reaproveitar antes de criar novo:
- `AthleteProfile.kt`
- `HeartRateZones.kt`
- `WorkoutSession.kt`
- `HeartRateStats.kt`
- `HeartRateSampleStore.kt`
- `LiveHeartRateViewModel.kt`
- `LiveHeartRateUiState.kt`
- `HomeScreen.kt`
- `FightCard`
- `MetricTile`
- `LiveMetricPill`
- cards de Perfil existentes
- SQLite `fightrate.db`
## Data Model — P0 Fields
### 1. Perfil básico
Campos:
- `age`
- `biologicalSex`
- `heightCm`
- `weightKg`
- `fightSport`
- `weightClassKg`
- `trainingGoal`
Uso:
- personalizar zonas;
- calcular IMC;
- acompanhar categoria de peso;
- contextualizar insights.
### 2. Perfil cardiovascular
Campos:
- `restingHeartRate`
- `measuredMaxHeartRate`
Uso:
- limite cardíaco personalizado;
- zonas por percentual;
- reserva cardíaca;
- alertas de esforço.
### 3. Check-in pré-treino
Campos por sessão:
- `preWorkoutEnergy` escala 1–5
- `preWorkoutFatigue` escala 1–5
- `muscleSoreness` escala 1–5
- `sleepHours`
- `sleepQuality` escala 1–5
- `preWorkoutWeightKg`
- `waterIntakeMl`
- `symptomsNote`
Uso:
- prontidão;
- risco de fadiga;
- contexto para batimentos altos;
- hidratação estimada.
### 4. Pós-treino
Campos por sessão:
- `postWorkoutRpe` escala 1–10
- `postWorkoutFatigue` escala 1–5
- `postWorkoutSoreness` escala 1–5
- `postWorkoutWeightKg`
- `postWorkoutNote`
Uso:
- carga subjetiva;
- perda hídrica;
- recuperação;
- comparação entre percepção e batimentos.
## Calculations — P0
### 1. IMC
Entrada:
- peso
- altura
Resultado:
- `bmi`
- faixa textual simples
### 2. Reserva cardíaca
Entrada:
- limite cardíaco real ou estimado
- FC repouso
Resultado:
- `heartRateReserve`
### 3. Zonas por Karvonen
Entrada:
- limite cardíaco
- FC repouso
Resultado:
- zonas mais personalizadas para o atleta.
### 4. Score de treino
Entrada:
- duração da sessão
- BPM médio
- BPM pico
- tempo em zonas
- RPE pós-treino
Resultado:
- score 0–100
- rótulo: leve, moderado, intenso, extremo
### 5. Carga subjetiva
Entrada:
- RPE pós-treino
- duração em minutos
Resultado:
- `sessionLoad = RPE × durationMinutes`
### 6. Score de recuperação
Entrada:
- sono
- qualidade do sono
- energia antes
- fadiga antes
- dor muscular
- FC repouso opcional
Resultado:
- score 0–100
- rótulo: baixa, ok, boa, excelente
### 7. Hidratação estimada
Entrada:
- peso antes
- peso depois
- água ingerida
Resultado:
- perda de peso no treino
- perda hídrica estimada
- alerta se perda relativa for alta
### 8. Tempo em zona de luta
Entrada:
- samples de BPM da sessão
- zonas cardíacas
Resultado:
- tempo em zona alta/intensa
- percentual do treino em zona de luta
### 9. Alertas de segurança esportiva
Entrada:
- sintomas
- fadiga alta
- sono ruim
- dor alta
- esforço cardíaco alto
Resultado:
- mensagens de atenção no app.
Exemplos:
- “Sono baixo + fadiga alta: considere treino técnico ou leve.”
- “Perda de peso alta no treino: reponha líquidos.”
- “Esforço muito alto para uma recuperação baixa.”
## UI/UX
### Tela Perfil
Adicionar seções recolhíveis ou cards:
1. Básico
2. Cardiovascular
3. Luta
Evitar formulário gigante em uma tela só.
### Tela Check-in
Fluxo simples antes de iniciar sessão:
- energia
- fadiga
- dor
- sono
- peso
- água
- sintomas
Pode ser card ou bottom sheet antes de iniciar treino.
### Tela Pós-treino
Após encerrar sessão:
- RPE
- fadiga
- dor
- peso pós
- observação
### Tela Insights do Lutador
Primeira versão com cards:
- Score do treino
- Recuperação
- Hidratação
- Tempo em zona de luta
- Carga subjetiva
- Alertas
## Project Structure
Novos arquivos prováveis:
```text
app/src/main/java/com/healthtracking/fightrate/domain/AthleteInsights.kt
app/src/main/java/com/healthtracking/fightrate/domain/ReadinessScore.kt
app/src/main/java/com/healthtracking/fightrate/domain/HydrationEstimate.kt
app/src/test/java/com/healthtracking/fightrate/domain/AthleteInsightsTest.kt
```
Arquivos existentes a alterar:
```text
AthleteProfile.kt
WorkoutSession.kt
HeartRateSampleStore.kt
LiveHeartRateUiState.kt
LiveHeartRateViewModel.kt
HomeScreen.kt
```
A criação de novos arquivos deve ser mínima e justificada por responsabilidade clara.
## Code Style
Exemplo esperado:
```kotlin
data class ReadinessInput(
val sleepHours: Double?,
val sleepQuality: Int?,
val energy: Int?,
val fatigue: Int?,
val soreness: Int?,
)
object ReadinessScoreCalculator {
fun calculate(input: ReadinessInput): ReadinessScore {
val score = 100
.minus(input.fatigue.orDefaultPenalty(multiplier = 8))
.minus(input.soreness.orDefaultPenalty(multiplier = 6))
.plus(input.energy.orDefaultBonus(multiplier = 5))
.coerceIn(0, 100)
return ReadinessScore(value = score)
}
}
```
Convenções:
- código em inglês;
- UI em PT-BR;
- cálculo puro no domínio;
- Compose apenas renderiza estado;
- evitar mágica sem teste.
## Testing Strategy
Testar com JUnit:
- IMC
- reserva cardíaca
- Karvonen
- score de recuperação
- carga subjetiva
- hidratação estimada
- tempo em zona de luta
- alertas esportivos
Comandos:
```bash
./gradlew :app:testDebugUnitTest --tests "*AthleteInsights*"
./gradlew :app:testDebugUnitTest :app:assembleDebug
```
## Boundaries
### Always
- Criar cálculo no domínio antes da UI.
- Usar testes para cálculo.
- Manter campos opcionais quando possível.
- Explicar que cálculos são estimativas esportivas.
- Reaproveitar `AthleteProfile`, `WorkoutSession`, `HeartRateSampleStore`.
- Salvar dados localmente.
### Ask first
- Adicionar dependência nova.
- Adicionar Health Connect.
- Criar tela nova separada se puder ser card inicialmente.
- Fazer alteração destrutiva no banco.
- Criar cálculo médico sensível.
- Coletar dados muito pessoais sem justificativa.
### Never
- Apresentar diagnóstico médico.
- Bloquear treino como decisão médica.
- Criar formulário gigante difícil de usar.
- Adicionar campos que não alimentam nenhum cálculo.
- Criar código morto para métricas futuras.
- Misturar todos os cálculos dentro da UI.
## Success Criteria
- Usuário consegue preencher dados P0 sem fricção excessiva.
- App calcula pelo menos:
- IMC;
- reserva cardíaca;
- score de treino;
- carga subjetiva;
- score de recuperação;
- hidratação estimada;
- tempo em zona de luta.
- Dados ficam persistidos localmente.
- Insights aparecem em PT-BR.
- Cálculos puros têm testes.
- Build final passa.
- Nenhuma dependência nova é adicionada sem aprovação.
## Decisions Approved
1. Check-in pré-treino será obrigatório antes de iniciar uma sessão.
2. Insights será uma nova aba no menu inferior.
3. Sexo biológico será coletado como `Masculino` ou `Feminino`.
4. Energia, fadiga, dor e sono usarão escala 0–10.
5. Campos avançados devem poder ficar ocultos/expandidos para manter a interface limpa.
## BLE Reliability and Battery Plan
### Objective
A leitura do sensor deve continuar enquanto o app estiver em treino, inclusive com tela bloqueada, salvo quando:
1. o usuário desconectar manualmente;
2. o usuário encerrar o treino;
3. o Android remover o processo por condição extrema fora do controle do app.
Quando houver queda por distância/sinal, o serviço deve tentar reconectar automaticamente quando o usuário se aproximar novamente. Se o usuário desconectar manualmente, o app não deve reconectar sozinho.
### BLE Rules
- O `HeartRateForegroundService` continua sendo dono da conexão BLE.
- O ViewModel não deve abrir conexão direta com o sensor.
- Toda queda inesperada deve ser registrada como estado de conexão e mensagem visível.
- Desconexão manual deve salvar uma flag `manualDisconnect = true`.
- Queda inesperada deve salvar `manualDisconnect = false` e iniciar retry/backoff.
- O último sensor selecionado deve ser preservado para reconexão automática.
### Reconnect Strategy
Estados sugeridos:
```text
Idle
Connecting
Connected
LostSignal
RetryWaiting
ManualDisconnected
Failed
```
Backoff conservador:
```text
1ª tentativa: 2s
2ª tentativa: 5s
3ª tentativa: 10s
4ª tentativa em diante: 30s
```
Regras:
- Não tentar reconectar se `manualDisconnect = true`.
- Não tentar reconectar se Bluetooth estiver desligado; aguardar próximo ciclo.
- Não tentar reconectar sem permissão Bluetooth.
- Não fazer scan agressivo contínuo.
- Para sensor já conhecido, tentar reconectar por `deviceId` direto antes de scan.
- Scan só deve ser usado quando não houver deviceId válido.
### Battery Strategy
Para menor consumo:
- Foreground service único durante sessão.
- Sem polling pesado no ViewModel a cada batimento.
- SQLite insert apenas por amostra recebida.
- Notificação com throttle.
- Reconexão com backoff, nunca loop apertado.
- Metadata do sensor lida na conexão e depois em intervalo longo, não a cada amostra.
- Gráficos atualizam pelo estado recebido, sem reconsultar o banco inteiro.
- Evitar wake locks permanentes. Usar foreground service e orientação de ignorar otimização de bateria.
### Acceptance BLE
- Tela bloqueada não interrompe leitura durante treino.
- Distância/sinal perdido mostra status de sensor perdido.
- Ao aproximar novamente, o app tenta reconectar sozinho.
- Se usuário tocar em Desconectar, reconexão automática fica desativada.
- Se usuário escolher o sensor/iniciar novamente, reconexão automática volta a valer.
- Build passa.
## Recommended First Implementation Plan
Após aprovação desta spec:
1. Criar domínio puro de insights e testes.
2. Expandir modelo de perfil e sessão com campos P0.
3. Adicionar migração SQLite aditiva.
4. Criar UI de Perfil Avançado com seções expansíveis.
5. Criar check-in pré-treino obrigatório antes de iniciar sessão.
6. Criar pós-treino ao encerrar sessão.
7. Criar nova aba Insights com cards básicos.
8. Melhorar BLE com desconexão manual vs queda inesperada, reconexão automática e backoff.
9. Rodar build/test completo.
---
# Spec Expansion: Gráficos, Janela de Treino e Relatório Científico
## Objective
Transformar a sessão do FightRate em uma janela de coleta confiável com relatório pós-treino detalhado, gráficos Vico mais legíveis/interativos e métricas esportivas fundamentadas em frequência cardíaca real do sensor BLE.
O objetivo é interpretação esportiva para lutas, não diagnóstico médico. Toda métrica estimada deve ser rotulada como estimativa.
## Research Summary
Fontes consultadas antes da implementação:
1. Cole et al., NEJM 1999, PubMed: `https://pubmed.ncbi.nlm.nih.gov/10536127/`
- HR Recovery é queda da FC do pico para 1 minuto pós-exercício.
- Queda <= 12 bpm em 1 min foi considerada anormal no estudo clínico.
2. Brubaker & Kitzman, Circulation review, PMC: `https://pmc.ncbi.nlm.nih.gov/articles/PMC3065291/`
- Resposta cronotrópica é capacidade de aumentar FC com demanda.
- Critérios clínicos exigem teste progressivo e contexto; no app usar só interpretação esportiva.
- HRR 1 min < 12 bpm, 2 min < 42 bpm aparecem como thresholds de risco em contexto clínico, mas o app não diagnostica.
3. Uth et al., Eur J Appl Physiol 2004, PubMed: `https://pubmed.ncbi.nlm.nih.gov/14624296/`
- VO2max estimado pode usar `15.3 * (HRmax / HRrest)` em população treinada.
- Aplicabilidade fora desse perfil deve ser tratada como aproximação.
4. Boxing HR response, PMC: `https://pmc.ncbi.nlm.nih.gov/articles/PMC3871409/`
- Boxe simulado passou grande parte acima de VT2; HR aumenta por round.
- Reforça foco em tempo em zonas altas, recuperação entre rounds e drift.
5. Muay Thai physiological response, Ingenta: `https://www.ingentaconnect.com/content/uwic/ujpa/2012/00000012/00000003/art00004`
- Muay Thai competitivo teve FC média próxima do máximo e alto estresse cardiovascular.
6. Edwards/TRIMP summaries: `https://www.trainingimpulse.com/edwards-trimp` e `https://journals.lww.com/nsca-scj/fulltext/2018/04000/quantification_of_training_load_in_endurance.12.aspx`
- Edwards TRIMP é prático por zonas, mas coeficientes são arbitrários.
- Banister usa reserva cardíaca e ponderação exponencial; ambos devem ser apresentados como carga interna estimada.
7. HRV em Muay Thai, PMC: `https://pmc.ncbi.nlm.nih.gov/articles/PMC12143277/`
- RMSSD/SDNN exigem RR/NN intervals confiáveis. O app hoje coleta BPM, portanto HRV fica fora do escopo até o parser/persistência salvarem RR intervals.
## Implementable Metrics From Current Data
Viáveis agora:
- duração total da sessão;
- duração ativa aproximada e pausada;
- BPM inicial/final/mínimo/médio/máximo;
- desvio padrão;
- qualidade dos dados por densidade, gaps e duração;
- tempo e percentual por zona;
- tempo em zona de luta, RedLine e Limite;
- maior sequência contínua em zona intensa;
- picos e quedas relevantes;
- HR Recovery 1min/2min quando houver amostras pós-pico;
- drift primeira metade vs segunda metade;
- slope de BPM por minuto;
- resposta cronotrópica inicial aproximada;
- tempo até zona vigorosa/luta;
- Edwards TRIMP;
- Banister TRIMP quando houver FC repouso e limite cardíaco;
- carga subjetiva RPE x duração;
- VO2max estimado quando houver FC repouso e limite cardíaco;
- hidratação estimada com peso pré/pós e água;
- interpretação cruzada com energy/fatigue/soreness/sleep/symptoms/RPE.
Não viável agora sem novos dados:
- HRV RMSSD/SDNN/pNN50 real;
- diagnóstico de arritmia;
- limiar ventilatório real;
- VO2 real;
- economia cardíaca precisa entre sessões sem normalizar tipo/duração/intensidade.
## Data Collection / Time Window
Reaproveitar `workout_sessions`:
- `startedAtMillis` define início do range.
- `endedAtMillis` define fim do range.
- `session_id` em `heart_rate_samples` associa amostra à sessão ativa.
- Ao finalizar, buscar amostras por `session_id`; fallback por intervalo se necessário.
- Foreground service continua dono do BLE.
- ViewModel não assume BLE.
- Migração SQLite só aditiva se relatório persistido exigir cache.
## Domain Design
Criar ou expandir domínio puro para relatório:
- `TrainingReportMetrics`
- `SessionReportBuilder`
- `DataQualityAnalyzer`
- `ZoneDistributionCalculator`
- `HeartRateRecoveryCalculator`
- `CardiacDriftCalculator`
- `ChronotropicResponseCalculator`
- `TrainingLoadCalculator`
- `HydrationTrainingInterpreter`
Business rules fora do Compose.
## UI / Charts
Reaproveitar `HeartRateLineChart`, `FightCard`, `MetricTile`.
Adicionar:
- chart summary pills de mínimo/médio/máximo;
- legenda de zonas;
- destaque textual para tempo em zona de luta/limite;
- estados profissionais para poucos dados;
- leitura de eixo X por tempo real;
- relatório em seções expansíveis:
1. Resumo do treino;
2. Gráfico principal;
3. Zonas cardíacas;
4. Recuperação;
5. Carga de treino;
6. Drift e fadiga;
7. Hidratação;
8. Comparação com histórico;
9. Alertas e interpretação.
Evitar Canvas customizado e dependências novas.
## Success Criteria
- Spec atualizada antes do código.
- Código existente auditado e reaproveitado.
- Testes JUnit cobrem todos os cálculos de domínio novos.
- Sessão finalizada gera relatório com amostras do range correto.
- Histórico permite abrir relatório de sessão.
- UI deixa claro estimativa esportiva vs dado medido.
- BLE continua no foreground service.
- Sem migração destrutiva.
- `./gradlew :app:testDebugUnitTest :app:assembleDebug` termina com `BUILD SUCCESSFUL`.
- APK gerado e tentativa de `telegram_attach(localPath)` realizada.
@@ -0,0 +1,27 @@
# FightRate RN — Background Capture + Corner Instrument Redesign
## Objective
Make the bare React Native Android app capture heart-rate data continuously until the athlete manually stops the workout, including when the screen is locked or the app is backgrounded. Replace foreground-JS BLE ownership and generic card UI with native Android capture ownership, local SQLite persistence, and Corner Instrument screens.
## Success Criteria
- Capture is started/stopped from React Native, but BLE scan/connect/monitor runs in an Android foreground service.
- Android manifest declares Bluetooth, notification, foreground service, and battery optimization permissions needed for continuous capture.
- The foreground service repeats scan/reconnect attempts unless stopped manually.
- Heart Rate Service `180D` and Heart Rate Measurement `2A37` are used for real BLE BPM data.
- Each BPM sample is timestamped and persisted locally in SQLite.
- History and insights use persisted real sessions/samples; no mock BPM/history/insight data is displayed.
- UI follows `FIGHTRATE_DESIGN_SYSTEM.md` / Corner Instrument: dark physical instrument surfaces, dominant BPM, technical strips, no nested generic card dashboard.
- Release APK builds as an installable arm64 artifact and is sent to Telegram.
## Boundaries
- React Native bare CLI only; no Expo.
- Android-first implementation; iOS background BLE is out of scope.
- Physical Magene H003 validation remains required on device.
- Manual destructive deletion of non-design project files is avoided.
## Verification
- `npm run typecheck`
- `npm run lint`
- `npm test -- --runInBand`
- `npm audit --audit-level=moderate`
- `cd FightRateRN/android && ./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a`
@@ -0,0 +1,86 @@
# FightRate — Redesign de gráficos e sistema visual
## Objetivo
Corrigir a direção visual do FightRate: gráficos profissionais, úteis e interativos; interface menos genérica/card-heavy; design system original e obrigatório para próximas telas.
## Problemas atuais
- Gráficos aparecem mesmo sem dados úteis.
- Estados vazios parecem gráficos quebrados/amadores.
- Vico está subutilizado: pouca interação, pouco contexto visual, sem zoom/scroll claro.
- UI tem excesso de cards iguais, pouca hierarquia e pouco padrão visual.
- O aplicativo parece montado por IA, não por um produto esportivo premium.
## Decisões
- Manter Vico. Pesquisa em 2026-06 mostra Vico como biblioteca Compose/Kotlin madura com suporte a `scrollState` e `zoomState`.
- Não trocar dependências nesta fase.
- Não desenhar chart customizado em Canvas.
- Não renderizar gráfico se houver menos de 2 pontos reais.
- Se houver poucos dados, mostrar insight/empty state, não eixos vazios.
- O eixo temporal deve continuar baseado em timestamps reais.
- O design system passa a ser a fonte visual do app.
## Design System: FightRate Combat Lab
### Conceito
Interface de laboratório de performance para lutas: escura, precisa, atlética, com sensação de equipamento esportivo premium. Menos cartões genéricos, mais painéis editoriais, faixas de dados, chips compactos, e superfícies com função clara.
### Tokens principais
- Fundo: `#090A0B` — preto quente de OLED.
- Surface: `#111315` — bloco primário.
- Surface Elevated: `#181B1E` — módulo de leitura.
- Ink: `#F6F0E7` — texto principal quente.
- Muted: `#A79C8F` — texto secundário.
- Dim: `#6E665D` — metadados.
- Accent: `#FF6A1A` — FightRate Orange.
- Amber: `#F6B44B` — atenção.
- Redline: `#FF3B30` — limite/risco.
- Cyan: `#48C7D8` — recuperação/ciência.
- Line: `#2A2E32` — divisores sutis.
### Forma
- Telas: blocos editoriais com seções claras, não grade de cards repetidos.
- Painéis principais: radius 28–32 dp, borda hairline, padding 20–24 dp.
- Métricas pequenas: strips/pills, não cards grandes.
- Gráfico: painel amplo, prioridade visual máxima, legenda compacta.
- Empty state: peça editorial com próxima ação, nunca gráfico vazio.
### Tipografia
- Números grandes, tabulares quando possível.
- Títulos curtos, fortes, em PT-BR.
- Corpo pequeno, mas legível. Nada abaixo de 10sp para informação essencial.
- Evitar muitos pesos/tamanhos diferentes.
### Gráficos
- Mostrar gráfico somente se `samples.size >= 2` e duração > 0.
- Para 1 ponto: mostrar leitura única e instrução de continuar medindo.
- Para 0 pontos: mostrar estado vazio contextual.
- Linha com espessura alta, cor FightRate Orange, área/ambiente escuro.
- Scroll/zoom habilitado via Vico quando possível.
- Controles explícitos de janela/zoom fora do gráfico.
- Resumo sempre visível: média, pico, mínima, duração, densidade de amostras.
- Não usar índices como tempo; usar segundos/minutos reais desde o início da janela.
## Fatias de implementação
1. Consolidar tokens em tema/design system.
2. Criar componentes visuais base: `FightSection`, `FightMetricStrip`, `FightEmptyState`, `FightChartPanel`.
3. Atualizar `HeartRateLineChart` para não renderizar vazio e usar zoom/scroll Vico.
4. Reorganizar Home/Histórico para priorizar gráfico e dados úteis.
5. Atualizar documentação e roadmap.
6. Rodar testes/build e gerar APK.
## Critérios de sucesso
- Nenhum gráfico vazio é renderizado.
- Gráfico com dados tem zoom/scroll ou controle equivalente claro.
- Histórico sem dados mostra estado vazio premium.
- UI usa tokens e componentes do design system.
- Build final passa com `./gradlew :app:testDebugUnitTest :app:assembleDebug`.
@@ -0,0 +1,41 @@
# FightRate — Correção de produto: captura contínua, gráfico e sensor
## Contexto
Feedback do usuário: o app serve para monitorar batimento cardíaco enquanto o atleta treina. Durante luta/treino, o usuário não vai pegar o celular para iniciar/parar rounds manualmente.
## Objetivos
1. Tornar captura contínua de treino inteiro o fluxo principal.
2. Manter modo round como secundário/opcional, sem dominar a tela principal.
3. Corrigir gráfico para abrir com a janela selecionada completa, sem zoom inicial estranho.
4. Tornar eixo temporal adaptativo e menos poluído.
5. Melhorar tela de sensor para exibir dados/serviços disponíveis do dispositivo.
6. Melhorar insights com informação densa, útil e explicável sem textão.
## Regras de produto
- Fluxo principal: iniciar monitoramento antes do treino, largar celular, parar ao final, analisar sessão inteira.
- Round é alternativa opcional. Preferir segmentação automática pré-configurada em vez de controles manuais durante treino.
- Histórico precisa ser rico e organizado: sessões, janela, zonas, carga, recuperação, pico, média, densidade de amostras.
- Gráfico deve priorizar legibilidade e fidelidade temporal.
## Critérios de aceitação desta fatia
- Tela Live comunica “captura contínua” como ação principal.
- Botões de sessão usam linguagem de treino inteiro: iniciar/parar captura/treino.
- Round continua disponível/visível como informação secundária, mas não como conceito central.
- Gráfico inicia mostrando a janela de dados selecionada, sem pre-zoom e com `initialZoom = null` quando possível.
- Labels do eixo X usam granularidade por duração:
- até 10 min: HH:mm
- até 1 h: HH:mm
- até 24 h: HH'h'
- zoom/duração muito curta: HH:mm:ss apenas quando necessário.
- Tela Sensor exibe painel de capacidades do sensor: BPM, local do sensor, bateria, metadados, RSSI e serviços BLE conhecidos/disponíveis.
- Build final passa com `./gradlew :app:testDebugUnitTest :app:assembleDebug`.
## Fora desta fatia
- Implementar segmentação automática completa por round.
- Persistir todos os valores de serviços BLE desconhecidos.
- Reescrever toda a arquitetura visual do app.
@@ -0,0 +1,69 @@
# FightRate — monitoramento contínuo, histórico customizado e indicadores BPM
## Objetivo
Corrigir o fluxo para que o FightRate se comporte como um monitor contínuo de sensor cardíaco, e não como um app dependente da janela de treino. A janela de treino deve apenas destacar um intervalo dentro do fluxo contínuo. Também adicionar consulta histórica customizada por data/hora/minuto, estabilizar a UI do histórico e documentar indicadores úteis derivados de BPM.
## Problemas relatados
1. BPM em tempo real só aparece depois de abrir e fechar uma janela de treino.
2. Histórico precisa consultar intervalo preciso por dia, hora e minuto, inclusive múltiplos dias.
3. Histórico tem divisores redundantes entre controles, janelas salvas e registros.
4. Em “Uso Total”, o item expandido colapsa quando entra novo dado em tempo real.
5. É necessário mapear indicadores técnicos/científicos extraíveis de BPM para evolução futura.
## Critérios de sucesso
- Ao abrir o app, se o monitor estava ativo ou há sensor salvo, o app retoma o monitoramento contínuo automaticamente.
- Após conectar ao sensor peitoral, os dados começam a atualizar o app inteiro sem depender de janela de treino.
- Abrir/fechar janela de treino não interrompe nem inicia artificialmente o stream contínuo; só marca um intervalo para histórico.
- Histórico permite modo “Faixa” com início/fim definidos por dia, hora e minuto.
- Faixa customizada funciona no mesmo dia e entre dias diferentes.
- Uso Total mantém o item expandido mesmo quando novas amostras chegam em segundo plano.
- Divisores do histórico ficam lineares: uma linha sutil por seção/registro, sem duplicação visual.
- Pesquisa de indicadores BPM gera relatório com fórmulas, dados necessários, limites e recomendações.
- Sem dados fake; gráficos e métricas continuam derivados de amostras reais.
## Arquitetura proposta
### Monitoramento contínuo
- Manter BLE e background capture no serviço nativo Android.
- No hook RN, diferenciar claramente:
- `isMonitoring`: stream contínuo ativo.
- `isCapturing`: janela de treino ativa.
- `samples`: amostras contínuas em memória recente.
- Na inicialização do hook, depois de `getCaptureSnapshot`, se o snapshot indicar monitor ativo/sensor salvo, retomar/confirmar `startMonitoring` sem abrir janela.
### Histórico customizado
- Criar lógica pura para filtrar amostras por intervalo `[start, end]`.
- Usar a mesma fonte de verdade (`continuous_heart_rate_samples`) para “Uso Total” e faixa customizada.
- Representar a seleção com componentes simples sem dependência nova: campos de data e hora/minuto via `TextInput`, com validação e presets.
### Estabilidade do Uso Total
- Não usar ID derivado de `startedAt-endedAt-count` como única identidade quando blocos vivos mudam com novas amostras.
- Manter expansão por `startedAt` do bloco e re-resolver o bloco depois de cada atualização.
### Indicadores BPM
- Produzir documento separado com métricas implementáveis depois:
- FC média/mínima/máxima;
- zonas de intensidade;
- tempo em zona;
- carga interna/TRIMP;
- recuperação pós-esforço;
- deriva cardiovascular;
- variabilidade de FC quando houver RR intervals;
- alertas de limiar e anomalias.
## Fora de escopo
- Trocar BLE stack.
- Adicionar dependências nativas de calendário/data.
- Alterar design aprovado fora dos pontos pedidos.
- Criar diagnóstico médico automatizado sem validação clínica.
## Testes esperados
- Testes unitários para agrupamento de uso contínuo estável.
- Testes unitários para filtro de intervalo customizado.
- Testes existentes continuam passando.
## Verificação
- `npm run typecheck`
- `npm run lint`
- `npm test -- --runInBand`
- `npm audit --audit-level=moderate`
- `./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a`
@@ -0,0 +1,25 @@
# FightRate RN — Continuous Monitoring + Training Windows
## Objective
Change FightRate so BLE heart-rate monitoring is continuous and locally persisted, while a workout/training session is only a marked time window inside that continuous stream. Starting a workout begins a highlighted window; stopping it closes that window and keeps monitoring alive.
## Success Criteria
- The native Android foreground service can start monitoring without creating a workout window.
- Samples are saved continuously even when no workout window is active.
- Start/stop workout only opens/closes a `workout_sessions` window; it does not stop BLE monitoring.
- History lists saved windows and each item can be opened to inspect details from samples inside that window.
- Insight and history screens use only real timestamped samples.
- Capture/History/Insights UI removes generic card-heavy containers and uses Corner Instrument rails, strips, dividers, and instrument surfaces.
- Existing bare React Native CLI constraint remains; no Expo.
## Boundaries
- No new dependencies unless unavoidable.
- Do not delete existing tables destructively; migrate by adding a continuous sample table and deriving session summaries from timestamps.
- Android physical BLE validation remains required after APK install.
## Verification
- `npm run typecheck`
- `npm run lint`
- `npm test -- --runInBand`
- `npm audit --audit-level=moderate`
- `cd FightRateRN/android && ./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a`
@@ -0,0 +1,81 @@
# FightRate Insights Científicos — Mini-spec
## Objetivo
Implementar uma nova visualização de cálculos e insights na aba `Insights`, usando métricas derivadas do relatório científico `docs/research/bpm-indicators-fightrate-2026-06-10.md`, sem inventar dados e sem abandonar o design Corner Instrument.
## Escopo
### Incluir agora
- Grade/tabela compacta de métricas com melhor aproveitamento vertical.
- Cálculos com dados reais já disponíveis:
- FC atual, média, mínima e máxima;
- duração observada e contagem de amostras;
- amplitude de BPM;
- desvio padrão de BPM;
- coeficiente de variação de BPM;
- classificação operacional de estabilidade/oscilação;
- tempo em zonas já calculadas;
- carga observada existente;
- tendência de deriva de FC quando a janela for longa o suficiente;
- recuperação pós-janela quando houver amostras contínuas após o fim da sessão;
- status de HRV/VFC baseado na presença de `rrIntervalsMs`.
- Estados honestos para métricas que exigem perfil ou RR intervals.
### Não incluir agora
- Cadastro de perfil do atleta.
- Diagnóstico médico.
- LF/HF, frequência espectral, SampEn ou DFA reais sem pipeline de RR validado.
- Mudanças amplas de design fora de `Insights`.
- Novas dependências.
## Arquitetura
- Reutilizar `src/training/trainingMetrics.ts` para cálculos puros.
- Estender `src/training/trainingTypes.ts` com tipos de resumo de insights.
- Atualizar `src/screens/InsightsScreen.tsx` para renderizar uma tabela/grade de instrumentos, não lista simples.
- Atualizar `src/app/FightRateApp.tsx` para passar `getContinuousSamples` ao Insights, permitindo cálculo de recuperação pós-janela com dados persistidos.
- Testar cálculos puros em `__tests__/trainingMetrics.test.ts`.
## Critérios de sucesso
- Insights usa somente amostras reais ou mostra estado indisponível.
- Visual evita cards genéricos; usa trilhos, linhas tabulares, separadores e linguagem Corner Instrument.
- Métricas BPM-only aparecem com valor/unidade/status/leitura curta.
- HRV/VFC aparece como disponível somente quando existirem RR intervals; caso contrário, informa que requer RR.
- Recuperação pós-janela só aparece quando há amostras depois de `endedAt`.
- Typecheck, lint e testes passam.
- Build release Android é gerado e enviado ao Telegram ao final.
## Reuso vs criação
### Reutilizar
- `calculateLiveMetrics`, `calculateZones`, `formatDuration` em `trainingMetrics.ts`.
- `HeartRateSample`, `CompletedTrainingSession` em `trainingTypes.ts`.
- Tokens existentes em `src/theme`.
- Fluxo nativo `getContinuousSamples` já exposto pelo hook.
### Criar/alterar
- Criar funções puras em `trainingMetrics.ts`:
- `buildInsightDashboard()`;
- `calculateBpmVariability()`;
- `calculateHeartRateDrift()`;
- `calculatePostSessionRecovery()`;
- `summarizeRrAvailability()`.
- Criar tipos em `trainingTypes.ts`:
- `InsightMetricStatus`;
- `InsightMetricRow`;
- `InsightDashboard`.
- Reescrever o layout interno de `InsightsScreen.tsx` para matriz tabular.
## Plano de execução
1. Escrever testes para cálculos puros.
2. Implementar funções puras em `trainingMetrics.ts`.
3. Passar `getContinuousSamples` para `InsightsScreen` e carregar amostras persistidas de forma segura.
4. Recriar UI da aba Insights com tabela compacta e blocos de instrumentos.
5. Rodar validação completa.
@@ -0,0 +1,108 @@
# FightRate React Native Bare Rebuild — Mini-spec
Data: 2026-06-10
Status: implementação autorizada
Stack: **React Native Community CLI, sem Expo**
## Objetivo
Criar a nova base do FightRate em React Native bare, sem Expo, usando a direção visual oficial **Corner Instrument**.
A implementação inicial deve provar:
- identidade visual escolhida;
- fluxo principal de captura contínua;
- estrutura de código React Native escalável;
- telas base para Captura, Histórico, Sensor e Insights;
- dados mockados apenas como estado de protótipo local, não como gráfico ou amostra real persistida.
## Fora de escopo nesta primeira fatia
- BLE real Magene H003.
- SQLite real.
- Build de APK final assinado.
- Migração de todos os recursos do Android legado.
- Expo ou qualquer dependência Expo.
## Decisões
- Criar novo app em `FightRateRN/` para não destruir o Android legado antes da base RN compilar.
- Usar React Native Community CLI.
- Usar TypeScript.
- Começar com UI e estado local; BLE/persistência entram em fatias posteriores.
- Fonte visual obrigatória: `FIGHTRATE_DESIGN_SYSTEM.md`.
## Estrutura desejada
```text
FightRateRN/
src/
app/
FightRateApp.tsx
components/
ActionButton.tsx
MetricTile.tsx
SectionPanel.tsx
StatusTag.tsx
TrainingChart.tsx
screens/
CaptureScreen.tsx
HistoryScreen.tsx
SensorScreen.tsx
InsightsScreen.tsx
theme/
colors.ts
radius.ts
spacing.ts
typography.ts
index.ts
data/
demoTraining.ts
```
## Critérios de aceite
1. Projeto React Native bare criado sem Expo.
2. `package.json` não contém dependências Expo.
3. App inicial mostra a direção Corner Instrument.
4. Captura contínua é o fluxo principal.
5. Round manual não domina a interface.
6. Design tokens estão centralizados em `src/theme`.
7. Componentes principais reutilizáveis existem.
8. Telas visíveis:
- Captura;
- Histórico;
- Sensor;
- Insights.
9. Verificação mínima:
```bash
cd FightRateRN
npm install
npm run typecheck
npm run lint
npm test -- --runInBand
```
Build Android debug validado neste ambiente:
```bash
cd FightRateRN/android
./gradlew assembleDebug
```
Observação local: o PATH atual tem JRE, mas não `javac`. Para o build Gradle RN,
use o JBR do Android Studio como `JAVA_HOME`:
```bash
export JAVA_HOME="/c/Users/Evandro/AppData/Local/Programs/Android Studio/jbr"
export PATH="$JAVA_HOME/bin:$PATH"
cd FightRateRN/android
./gradlew assembleDebug
```
## Riscos
- React Native CLI pode mudar template conforme versão atual.
- Ambiente local pode não ter Android SDK/NDK adequado para build RN.
- BLE real exigirá dependência nativa futura e teste físico.
@@ -0,0 +1,93 @@
# FightRate RN — Real BLE / No Mocks Corrective Slice
Data: 2026-06-10
Status: implementado / APK enviado ao Telegram
Stack: React Native Community CLI bare, sem Expo
## Problema
A primeira versão React Native enviada era instalável, mas não funcional como app de treino real:
- usava dados de demonstração;
- não escaneava BLE;
- indicava estados como conectado/pronto sem sensor real;
- gráficos não eram alimentados por amostras reais;
- histórico e insights pareciam conteúdo estático;
- navegação inferior estava visualmente fraca;
- logo/ícone do projeto não aparecia na UI.
## Objetivo desta fatia
Transformar a versão RN em uma base real de captura:
1. Remover dados mockados da UI.
2. Adicionar BLE real com `react-native-ble-plx`.
3. Pedir permissões Android Bluetooth/localização.
4. Escanear sensores com Heart Rate Service `180D`.
5. Conectar ao sensor encontrado.
6. Monitorar Heart Rate Measurement `2A37`.
7. Parsear BPM real a partir do payload BLE.
8. Atualizar BPM, média, pico e amostras em tempo real.
9. Renderizar gráfico somente com amostras reais recebidas.
10. Exibir histórico/insights como estados vazios honestos quando não houver sessão real.
11. Melhorar bottom nav: ícones, barra flutuante, sem faixa preta atrás.
12. Remover indicador `READY` do topo.
13. Usar logo existente do projeto na UI.
14. Gerar APK release e enviar ao Telegram.
## Dependências aprovadas pela necessidade do pedido
- `react-native-ble-plx`: BLE real no React Native bare.
- `react-native-svg`: gráfico real leve e ícones SVG.
- `lucide-react-native`: ícones para navegação/estado.
## Critérios de aceite
- App não mostra BPM mockado.
- Tela de captura mostra estado de permissão/scan/conexão real.
- Botão de captura dispara permissões e scanning BLE.
- Se nenhum sensor for encontrado, a UI diz isso claramente.
- Se sensor conectar, BPM vem do characteristic `2A37`.
- Gráfico só aparece depois de amostras reais.
- Histórico não finge sessões inexistentes.
- Insights não fingem análise sem dados.
- APK release abre sem Metro.
## Validação
```bash
cd FightRateRN
npm run typecheck
npm run lint
npm test -- --runInBand
npm audit --audit-level=moderate
```
Resultado:
```text
tsc OK
ESLint: No issues found
Tests: 2 suites / 4 tests passed
found 0 vulnerabilities
```
Build Android release arm64:
```bash
cd FightRateRN/android
./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a
```
Resultado:
```text
BUILD SUCCESSFUL
APK: FightRateRN/android/app/build/outputs/apk/release/app-release.apk
Tamanho: 29.7 MB
Telegram: enviado
```
## Limitação ainda aberta
A conexão BLE real precisa ser validada no aparelho físico com a cinta Magene H003. O app agora escaneia/conecta/monitora BLE real, mas só o teste com o sensor confirma comportamento final de campo.
@@ -0,0 +1,54 @@
# FightRate — Sensor contínuo e histórico geral
## Objetivo
Separar claramente três conceitos:
1. **Sensor**: descobrir, mostrar disponibilidade e permitir conexão ao sensor BLE.
2. **Monitoramento contínuo**: depois de conectado, receber, mostrar e salvar BPM real o tempo todo no app.
3. **Janela de treino**: ação disponível somente em Captura; marca um intervalo dentro do fluxo contínuo para destaque no histórico.
## Critérios de sucesso
- A tela Sensor não mostra botão de abrir/iniciar janela de treino.
- A tela Sensor mostra se o sensor está disponível/conectado e permite conectar/desconectar/monitorar pelo fluxo de sensor.
- Conectar o sensor inicia o recebimento contínuo via serviço nativo e os dados alimentam Captura, Sensor, Histórico e Insights.
- A janela de treino continua existindo apenas na tela Captura e não interrompe o monitoramento ao fechar.
- Histórico mantém a lista de janelas de treino e adiciona uma visão de **uso total** baseada em `continuous_heart_rate_samples`.
- O histórico geral não usa dados falsos: se não houver amostras contínuas, mostra estado vazio honesto.
- BLE services/characteristics ficam mais legíveis, com hierarquia compacta e sem espaçamento excessivo.
- Código visível ao usuário em PT-BR; identificadores em inglês.
## Arquitetura recomendada
- Reusar `FightRateCaptureStore` como fonte local-first.
- Reusar `continuous_heart_rate_samples` como verdade para histórico geral.
- Criar uma pequena camada de domínio TS para agrupar amostras contínuas em blocos de uso por lacunas de tempo. Essa lógica é pura e testável.
- Expor do módulo nativo um método de leitura paginada/resumida das amostras contínuas, sem mudar o contrato BLE.
- Manter `workout_sessions` apenas para janelas destacadas.
## Responsabilidades
### Reusar
- `useHeartRateMonitor`: estado app-wide recebido do módulo nativo.
- `TrainingChart`: gráfico real e interativo com amostras timestampadas.
- `FightRateCaptureStore`: persistência SQLite local.
- `HistoryScreen`: entrada natural para alternar entre janelas e uso total.
### Criar/alterar
- `src/training/continuousUsage.ts`: agrupar amostras contínuas em blocos de uso e sumarizar BPM/duração.
- Teste Jest para `continuousUsage`.
- API nativa `getContinuousSamples(limit?)`.
- Wrapper TS para `getContinuousSamples`.
- UI de Histórico com seção/alternância para “Janelas” e “Uso total”.
- UI de Sensor sem ação de treino e com diagnóstico BLE mais claro.
## Fora de escopo
- Não trocar biblioteca BLE.
- Não adicionar dependências.
- Não mudar o design aprovado fora dos pontos pedidos.
- Não criar dados demo/fake.
- Não mexer no app Android legado.
## Verificação
- `npm run typecheck`
- `npm run lint`
- `npm test -- --runInBand`
- `npm audit --audit-level=moderate`
- `./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a`
@@ -0,0 +1,190 @@
# FightRate RN — Gráfico dinâmico: zoom, pan, pin e auto-follow
## Objetivo
Substituir o `TrainingChart` atual (estático, 120 últimos samples, sem zoom) por um gráfico dinâmico com viewport temporal, zoom por pinch, pan, granularidade adaptativa do eixo X, e seleção pinada que **sobrevive a novas amostras em tempo real**. O comportamento de auto-follow deve retomar automaticamente quando o usuário arrasta o viewport até a borda direita (último ponto conhecido).
Aplica-se aos três call sites atuais: `CaptureScreen` (tempo real), `HistoryScreen` (janela salva) e `HistoryScreen` (faixa customizada).
## Problema atual
`src/components/TrainingChart.tsx` (versão vigente):
```ts
const chart = useMemo(() => buildChart(samples.slice(-120)), [samples]);
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
```
- `samples.slice(-120)` corta o array por contagem, não por janela temporal. Janelas longas (1h+) ficam visualmente compactadas a 120 pontos; janelas curtas (10s) ficam visualizadas com 120 pontos (impossível na prática, mas o problema é simétrico).
- `selectedIndex` é a **posição no array de pontos**. Quando novas amostras chegam, `buildChart` re-cria o array `points`, e o índice anteriormente válido passa a apontar para uma amostra diferente — o ponto pinado "pula" e, no limite, some do viewport. Em tempo real isso é especialmente grave: o atleta clica num pico, e o gráfico "come" o pin a cada BLE notify.
- Sem zoom (não há pinch). O gráfico é uma janela fixa do início ao fim do dataset.
- Eixo X fixo, em segundos totais. Granularidade dos labels nunca se adapta.
- Hit-testing em `onResponderMove` usa `event.nativeEvent.locationX` dividido por `layoutWidth` para mapear ao viewBox 320-wide — perde precisão fora do viewBox e não tolera gestos de 2 dedos.
## Decisões
### Stack
- Manter `react-native-svg` como renderer.
- Adicionar `react-native-gesture-handler` (~v2) e `react-native-reanimated` (~v3) para pinch/pan nativos. Setup padrão de RN bare: import `react-native-gesture-handler` no topo de `index.js`, envolver `App` com `GestureHandlerRootView`. Habilitar reanimated via Babel plugin (`react-native-reanimated/plugin` em `babel.config.js`).
- Sem troca de chart lib. `react-native-svg` continua sendo o motor de render.
### Modelo conceitual
Substituir "render dos últimos N samples" por **viewport temporal**:
```ts
type Viewport = {
readonly startMs: number; // primeira timestamp visível
readonly endMs: number; // última timestamp visível (inclusive)
readonly liveTail: boolean; // true quando endMs === lastSampleTimestamp
};
```
```ts
type ChartPin = {
readonly timestampMs: number;
readonly sample: HeartRateSample;
} | null;
```
Estado interno do `TrainingChart` (hooks, não Redux):
```ts
const [viewport, setViewport] = useState<Viewport>(initialViewport);
const [pin, setPin] = useState<ChartPin>(null);
```
`viewport` é o único "contrato" entre zoom/pan e o render. O SVG é derivado do `viewport` + samples. `pin` é independente de posição no array — é por `timestampMs`. Amostras novas entram, o `viewport` é recalculado somente se `liveTail` estiver `true`; o `pin` continua válido.
### Granularidade adaptativa do eixo X
A duração visível do viewport (`endMs - startMs`) decide a granularidade dos ticks:
| Duração visível | Granularidade dos ticks principais | Label |
|---|---|---|
| `>= 5 min` | 1 min | `HH:mm` |
| `>= 1 min` | 30 s | `HH:mm:ss` |
| `>= 30 s` | 15 s | `HH:mm:ss` |
| `>= 10 s` | 10 s | `HH:mm:ss` |
| `>= 2 s` | 1 s | `HH:mm:ss` |
| `< 2 s` | 0.5 s | `HH:mm:ss.S` |
Regras:
- Mostrar no máximo 4–5 ticks para não poluir.
- Tick 0 (início do viewport) sempre renderizado.
- Não usar ticks decorativos. Se o dataset não tem amostra no tick, o tick vira referência de eixo apenas.
### Zoom
- **Pinch** (dois dedos): escala o viewport em torno do ponto focado (ponto médio entre os dois dedos em coordenadas de tempo).
- **Botões `+` / `−`** no canto superior direito do painel gráfico: zoom in/out de 2x mantendo o centro do viewport.
- **Double-tap**: zoom in de 2x no ponto clicado; três taps em 1.2 s volta ao fit da janela selecionada (de acordo com o `windowSamples` que o pai passou).
- Limites: mínimo 1 s visível; máximo = janela total selecionada (fit).
- Os gestos de pinch e pan **não devem disparar tap**; gesto horizontal no gráfico move viewport; gesto vertical isolado é ignorado (a altura do gráfico não muda).
### Pan
- 1 dedo arrastando = pan. `pan` ajusta `startMs` e `endMs` proporcionalmente ao movimento em pixels (relação `pixelPorMs = layoutWidth / (endMs - startMs)`).
- Quando o usuário inicia pan, `liveTail` vai para `false` imediatamente.
- `liveTail` retorna a `true` **somente** quando, ao final do gesto, o `endMs` do viewport estiver a menos de `5%` da duração visível da última amostra conhecida (snap zone).
- Quando `liveTail` está `true` e novas amostras chegam, o viewport estende o `endMs` automaticamente (a "esteira" do tempo real).
- Quando `liveTail` está `false`, novas amostras **não** movem o viewport (mas o `lastPoint` continua sendo o último da coleção, para render do "tail off-screen indicator").
### Pin (clique em ponto)
- Tap em um ponto = `setPin({ timestampMs, sample })`.
- Pin sobrevive a:
- novas amostras chegarem (o `timestampMs` continua existindo no array de samples, a menos que o ponto esteja fora do novo viewport);
- zoom in/out (o pin continua naquele `timestampMs`);
- pan (idem).
- Pin some se:
- o usuário tocar fora do gráfico;
- o usuário tocar de novo no mesmo ponto pinado (toggle);
- o `timestampMs` do pin ficar **fora do viewport** e o usuário não voltar para ele (auto-clear opcional, mas default é manter — fica "off-screen" e um pequeno indicador pode avisar).
- Render do pin: círculo âmbar + linha tracejada vertical até o eixo X (visual atual mantido, refinado).
- O `readout` (painel inferior) mostra o sample pinado. Sem pin, mostra "TOQUE/ARRASTE PARA INSPECIONAR" e mantém o BPM atual.
### Hit-testing
- O gesto de tap é resolvido por `Gesture.Tap().onEnd((e) => pickPointAtX(e.x, viewport, samples))`.
- `pickPointAtX` mapeia `e.x` (em pixels no SVG) para `timestampMs = startMs + (e.x - PAD_X) / (CHART_WIDTH - 2*PAD_X) * (endMs - startMs)`, e então busca a amostra de `samples` mais próxima no tempo (busca binária por timestamp, fallback linear para datasets pequenos).
- Tolerância de snap: se a distância para a amostra mais próxima for maior que `10%` da duração visível, **não** seta pin (considera tap em área vazia).
## Estrutura de código
### Novos arquivos
- `src/training/chartViewport.ts` — funções puras (testáveis) para:
- `granularityForSpan(spanMs: number): Granularity`
- `formatTickLabel(timestampMs: number, granularity: Granularity): string`
- `buildChartViewport(samples, options)` — calcula o viewport inicial
- `applyZoomIn(viewport, focusMs)` / `applyZoomOut(viewport, focusMs)`
- `applyPan(viewport, dxPixels, pixelsPerMs)`
- `isLiveTailSnap(viewport, lastSampleTimestampMs)` — regra do snap zone
- `pickSampleAtTimestamp(samples, timestampMs, toleranceMs)`
- tipos: `Viewport`, `ChartPin`, `Granularity`
- `src/training/__tests__/chartViewport.test.ts` — testes TDD das funções puras.
### Modificados
- `src/components/TrainingChart.tsx` — reescrito. Mantém:
- viewBox 320×148 (compatibilidade com tokens);
- cor de linha `colors.action`, fundo `colors.surfaceHot`, raio `radius.md`;
- painel `readout` (label + valor) abaixo do gráfico;
- pin: círculo âmbar + linha tracejada.
Acrescenta:
- pinch + pan via `react-native-gesture-handler`;
- tap handler para pin;
- eixo X com ticks adaptativos (função pura);
- barra/cluster de botões `+` / `−` no canto;
- indicador "off-screen latest" quando `!liveTail` (uma seta/marca à direita mostrando que há dados mais novos).
- `src/components/ActionButton.tsx` — pode receber uso novo (botão pequeno icon-only para +/−). Avaliar reuso antes de criar componente novo.
- `FightRateRN/index.js` — adicionar `import 'react-native-gesture-handler'` no topo.
- `FightRateRN/babel.config.js` — adicionar `react-native-reanimated/plugin` (deve ser o último da lista).
- `FightRateRN/App.tsx` — envolver conteúdo em `GestureHandlerRootView` (verificar se já está).
- `package.json` — adicionar `react-native-gesture-handler` e `react-native-reanimated`.
- `android/app/src/main/java/com/healthtracking/fightrate/MainActivity.kt` — verificar se herda de `ReactActivity` (gesture-handler detecta automaticamente; nada a fazer em geral).
- `docs/FIGHTRATE_DEVELOPMENT_WORKFLOW.md` — atualizar a nota de que a skill `fightrate-quality-loop` recomenda aprovação antes de adicionar dependências (já cobre).
### Não modificados
- `useHeartRateMonitor.ts` — o gráfico consome `samples` como prop. O comportamento de auto-follow é responsabilidade do gráfico, não do hook.
- `CaptureScreen.tsx`, `HistoryScreen.tsx` — call sites do `TrainingChart`. A interface do componente (`samples`) permanece compatível; nenhum `prop` novo obrigatório é exposto ao pai nesta fatia (zoom/pan/pin são internos).
## Critérios de sucesso
1. **Auto-follow funcional**: ao entrar no `CaptureScreen` e a captura estar rodando, o gráfico se mantém mostrando os últimos N segundos; cada novo BPM real reposiciona o viewport para incluir a nova amostra à direita.
2. **Pin sobrevive a novas amostras**: tocar num ponto, deixar o sensor entregar 5 novas leituras → o pin continua visível na mesma posição, com o mesmo BPM e timestamp. O gráfico pode crescer à direita.
3. **Pan sai do live**: arrastar para a esquerda → `liveTail = false`; novas amostras param de mover o viewport. O indicador "off-screen latest" aparece à direita.
4. **Retomada do live**: arrastar de volta até a snap zone (últimos 5% da duração visível) → `liveTail = true` e o gráfico volta a rolar.
5. **Pinch**: 2 dedos abrindo → zoom in em torno do ponto médio; fechar → zoom out. Não dispara tap acidental.
6. **Botões `+` / `−`**: zoom in/out de 2x no centro.
7. **Granularidade adaptativa**: 10 min de janela visível → labels em `HH:mm`; 1 min → `HH:mm:ss` 30s; 30 s → 15 s; 10 s → 10 s; 2 s → 1 s; < 2 s → 0.5 s. Trocas suaves, sem flicker.
8. **Hit-test estável**: tap a 30% da largura do gráfico fixa o pin na amostra mais próxima no tempo, mesmo que o layout tenha width real diferente de `CHART_WIDTH`.
9. **Hit-test fora da área**: tap longe de qualquer amostra (gap > 10% da duração) não seta pin; o readout continua mostrando "TOQUE/ARRASTE PARA INSPECIONAR".
10. **Sem regressão visual**: tokens do Corner Instrument mantidos (cores, raio, borda, tipografia label, fundo `colors.surfaceHot`).
11. **Sem gráfico fake**: se `samples.length < 2`, mostra o estado vazio atual (sem render de linha). Nenhuma mudança nesse fluxo.
12. **Testes passam**: `npm test -- --runInBand` cobre `chartViewport.ts` (granularidade, zoom, pan, snap, pick). `App.test.tsx` continua passando.
13. **Build OK**: `npm run typecheck` e `npm run lint` exit 0. Build Android (`./gradlew :app:assembleDebug` em `FightRateRN/android`) só é rodado se JDK/JBR estiver disponível no ambiente.
## Riscos e mitigações
- **Risco**: gesture-handler requer setup de imports no Android (já é o caso em RN 0.86, mas o `MainActivity` pode precisar de `createReactActivityDelegate` atualizado).
**Mitigação**: seguir o passo-a-passo da documentação oficial; se o build falhar, voltar à abordagem `PanResponder + botões` e reusar o resto do spec.
- **Risco**: reanimated v3 + RN 0.86 — compatibilidade.
**Mitigação**: travar versões compatíveis (`react-native-reanimated@~3.16.0` recomendado para RN 0.86). Documentar a versão exata no `package.json` após smoke test.
- **Risco**: conflito entre gesto de pan do `ScrollView` pai e pan do gráfico.
**Mitigação**: usar `simultaneousWithExternalGesture` no `GestureDetector` do gráfico para desativar o pan do `ScrollView` apenas dentro da área do gráfico; testar com `CaptureScreen` (que está dentro de `ScrollView`).
- **Risco**: a busca binária de `pickSampleAtTimestamp` precisa de `samples` ordenados. Garantir invariante no hook e/ou ordenar defensivamente.
**Mitigação**: função pura de ordenação dedicada; teste cobre o caso de samples fora de ordem.
## Não-objetivos (fora do escopo)
- Crosshair, brush selection, comparação de duas janelas no mesmo gráfico.
- Exportar gráfico como imagem.
- Reorganizar layout da `CaptureScreen` ou da `HistoryScreen` (apenas a parte do `TrainingChart`).
- Trocar a paleta de cores.
- Adicionar leitura de RR no eixo Y (permanece em BPM).
- Sincronizar dois gráficos lado a lado.
- Persistir o último zoom/pan entre sessões (state efêmero).