Decisões técnicas relevantes para o projeto Dash for CF (Flutter). Consulte este documento ao iniciar novas sessões de desenvolvimento.
Status: Aceito
Data: 2025-12-29
Contexto: Ao usar versões mais recentes do retrofit (>4.6.0) com retrofit_generator 9.7.0, ocorre erro Parser.DartMappable não suportado durante code generation.
Decisão: Pinar retrofit em 4.6.0 no pubspec.yaml.
Consequência: Funciona corretamente com build_runner. Não atualizar retrofit sem testar compatibilidade com retrofit_generator.
retrofit: 4.6.0 # Pinned - incompatibility with newer versionsStatus: Aceito
Data: 2025-12-29
Contexto: Cloudflare API não permite CORS de origens arbitrárias. Browsers bloqueiam requests.
Decisão:
- Web usa proxy:
https://cors.verseles.com/api.cloudflare.com/client/v4 - Android/iOS/Desktop usam API direta:
https://api.cloudflare.com/client/v4 - Detecção via
kIsWebdo Flutter
Consequência: Manter proxy funcional para web. Mobile/desktop funcionam sem dependência externa.
Status: Aceito
Data: 2025-12-29
Contexto: Alternativas open-source (fl_chart, charts_flutter) têm menos features. Syncfusion oferece Community License gratuita para <$1M receita.
Decisão: Usar syncfusion_flutter_charts e syncfusion_flutter_maps.
Consequência:
- Não precisa de license key no código (funciona sem)
- Registrar em syncfusion.com/products/communitylicense para compliance legal
- Features avançadas disponíveis (SfMaps, tooltips, theming)
Status: Aceito
Data: 2025-12-29
Contexto: Usuário pode trocar de zona rapidamente. Resposta antiga pode sobrescrever resposta nova.
Decisão: Usar contador incremental _currentFetchId em todos os providers assíncronos.
int _currentFetchId = 0;
Future<void> fetch() async {
final fetchId = ++_currentFetchId;
// ... await API call ...
if (_currentFetchId == fetchId) {
state = AsyncData(result);
}
}Consequência: Respostas obsoletas são descartadas automaticamente.
Status: Atualizado
Data: 2026-01-20
Contexto: Dados de referência (data centers Cloudflare, países) precisam estar disponíveis offline e carregar instantaneamente.
Decisão: Implementar padrão Local-First:
- Carregar dados do asset local imediatamente (fallback)
- Buscar versão atualizada do CDN em background
- Atualizar state quando CDN responder
Implementações:
| Provider | Asset Local | CDN |
|---|---|---|
DataCentersNotifier |
assets/data/cloudflare-iata-full.json |
GitHub raw |
CountryNotifier |
assets/data/countries.json |
flagcdn.com/en/codes.json |
Padrão de código:
@override
FutureOr<Map<String, T>> build() async {
final localData = await _loadFromAsset();
if (!_hasFetched) {
unawaited(_fetchFromCdn());
}
return localData;
}Consequência:
- App funciona offline
- UI carrega instantaneamente com dados locais
- Dados atualizados quando online (background refresh)
Status: Aceito
Data: 2025-12-29
Contexto: Toggle de proxy (orange cloud) deve parecer instantâneo.
Decisão:
- Atualizar UI imediatamente
- Fazer API call em background
- Rollback se API falhar
Consequência: UX mais responsiva. Necessário tratamento de erro com rollback.
Status: Aceito
Data: 2025-12-29
Contexto: Cloudflare API pode demorar para refletir mudanças de DNSSEC.
Decisão: Após qualquer mudança, fazer polling duplo:
await updateDnssec(...);
await Future.delayed(Duration(seconds: 3));
await fetchSettings();
await Future.delayed(Duration(seconds: 2));
await fetchSettings();Consequência: UI reflete estado real após ~5 segundos.
Status: Aceito
Data: 2025-12-29
Contexto: API tokens Cloudflare têm pelo menos 40 caracteres.
Decisão: Validar token localmente antes de permitir acesso a rotas /dns/*.
Consequência: Feedback imediato para usuário. Evita calls desnecessárias à API.
Status: Aceito
Data: 2025-12-29
Contexto: MapBubbleSettings requer binding direto com shape data. Data centers não são países.
Decisão: Usar markerBuilder com MapMarker customizado para bubbles.
MapShapeLayer(
source: MapShapeSource.asset('assets/data/world.json'),
initialMarkersCount: dataPoints.length,
markerBuilder: (context, index) => MapMarker(
latitude: point.lat,
longitude: point.lng,
child: Container(...), // bubble circle
),
)Consequência: Controle total sobre posição e tamanho dos bubbles.
Status: Atualizado Data: 2025-12-31
Contexto: CI pode falhar por código não gerado ou análise não executada. Além disso, logs de comandos bem-sucedidos consomem tokens desnecessariamente.
Decisão: Usar Makefile com dois níveis de verificação:
-
make check- Validação rápida (~20s):- flutter pub get
- dart run build_runner build
- flutter analyze
- flutter test
-
make precommit- Verificação completa (~30s):- Executa check
- flutter build linux
- flutter build apk
Todos os comandos suprimem logs de sucesso: cmd > /tmp/log 2>&1 || cat /tmp/log
Consequência:
- Feedback rápido durante desenvolvimento (
make check) - Garantia completa antes de commit (
make precommit) - Economia de tokens com supressão de logs
Status: Aceito
Data: 2025-12-29
Contexto: Usuário pode ter muitas zonas. UX deve ser fluida.
Decisão:
| Situação | Comportamento |
|---|---|
| Zona salva não existe mais | Selecionar primeira |
| Filtro retorna 1 resultado | Auto-selecionar |
| Nenhuma zona salva | Selecionar primeira |
| Troca de zona | Reset filtros |
Consequência: UX intuitiva. Menos cliques.
Status: Aceito
Data: 2025-12-29
Contexto: Animação de delete (pulse vermelho) deve ser visível antes de item sumir.
Decisão: Delay de ~1200ms entre início de delete e execução da API call.
Consequência: Usuário vê feedback visual. Pode ser percebido como "lento" mas é intencional.
Status: Aceito
Data: 2025-12-29
Contexto: REST API não oferece analytics agregados. GraphQL permite queries flexíveis.
Decisão: Usar endpoint GraphQL (/client/v4/graphql) para analytics com query dnsAnalyticsAdaptiveGroups.
Consequência: Dados ricos. Query única para 7 dimensões.
Status: Aceito Data: 2025-12-29
Contexto: Debug de problemas no app requer visibilidade de logs tanto em tempo real quanto histórico. No Android, acessar logcat é inconveniente. Compartilhar logs para análise precisa ser fácil.
Decisão: Implementar sistema de logging híbrido com:
- LogService Singleton: Centraliza todos os logs com níveis (debug, info, api, warning, error)
- Aba Debug Logs: Visualização em tempo real no Drawer
- Filtros por tempo: 1m, 5m, 15m, 30m, All
- Filtros por categoria: All, API, Errors, State, Debug
- Botão Copy: Copia logs filtrados para clipboard
- Arquivo opcional: Toggle nas Settings para persistir em arquivo
Estrutura:
lib/core/logging/
├── log_entry.dart # Modelo de entrada
├── log_level.dart # Níveis e categorias
├── log_service.dart # Serviço singleton
├── log_provider.dart # Providers Riverpod
└── presentation/
└── debug_logs_page.dart
Integração:
- Interceptors Dio logam requests/responses automaticamente
- Providers logam state changes e erros
- main.dart captura FlutterError e async errors
- Global
loghelper para uso fácil:log.error('msg', error: e)
Formato de Export:
=== Debug Logs (last 5 min) ===
Session: 2025-12-29T19:00:00
Platform: android
Exported: 2025-12-29T19:05:00
Total entries: 42
[19:00:01.123] [API] GET /zones
→ 200 OK (145ms)
Consequência:
- Debug mais fácil em todas as plataformas
- Logs podem ser copiados e compartilhados para análise
- Arquivo opcional não impacta performance quando desativado
Status: Atualizado Data: 2026-01-23
Contexto: O tempo de carregamento da API da Cloudflare pode variar. Exibir skeleton loadings em cada navegação prejudica a fluidez.
Decisão: Implementar o padrão Stale-While-Revalidate (SWR) em todos os módulos principais (DNS, Workers, Pages):
- Armazenamento: SharedPreferences (JSON serializado)
- Expiração: 3 dias
- Refresh: Background (automático ao acessar a tela)
- Abrangência:
- DNS: Zones, Records e Settings.
- Workers: Scripts list, Settings, Schedules, Domains e Routes.
- Pages: Projects list, Deployments e Custom Domains.
Fluxo:
1. Ao acessar:
- Tentar carregar cache local
- Se cache existe: mostrar imediatamente (isFromCache: true)
- Iniciar refresh em background (isRefreshing: true)
2. Durante refresh:
- UI mostra indicador sutil (LinearProgress ou skipLoadingOnRefresh)
- Se falhar: manter dados do cache
- Se sucesso: atualizar estado e persistir novo cache
Consequência:
- UX instantânea: dados aparecem sem spinners centrais
- App funciona offline com dados em cache
- Consistência arquitetural entre funcionalidades
Status: Aceito Data: 2025-12-31
Contexto: Ao acessar aba Analytics pela primeira vez, exibia "No analytics data" em vez de loading. Além disso, fetch imediato em zona change bloqueava a UI da aba atual.
Decisão:
- Iniciar com
isLoading: truequando zona já selecionada - Delay de 500ms antes de fetch para não bloquear aba atual
- Limpar dados anteriores ao trocar de zona (evitar dados stale)
// Ao trocar de zona
ref.listen(selectedZoneIdProvider, (prev, next) {
state = const AnalyticsState(isLoading: true); // Reset
_scheduleFetch(); // Delay 500ms
});
// Se já tem zona no build
if (currentZone != null) {
_scheduleFetch();
return const AnalyticsState(isLoading: true);
}Consequência:
- Spinner exibido em vez de "No data"
- Aba Records/Settings carrega sem delay
- Analytics pronto quando usuário navega para aba
# Validação rápida durante desenvolvimento (~20s)
make check
# Verificação completa antes de commit (~30s)
make precommit
# Builds específicas
make android # APK arm64 + upload via tdl
make android-x64 # APK x64 para emulador
make linux # Linux release
make web # Web release
# Desenvolvimento
make deps # Instalar dependências
make gen # Gerar código (Freezed, Retrofit)
make test # Rodar testes
make analyze # Análise estática
make clean # Limpar artefatos
# Gerar ícones e splash
dart run flutter_launcher_icons
dart run flutter_native_splash:create| Método | Endpoint | Descrição |
|---|---|---|
| GET | /zones | Listar zonas |
| GET | /zones/{zoneId}/dns_records | Listar registros DNS |
| POST | /zones/{zoneId}/dns_records | Criar registro DNS |
| PUT | /zones/{zoneId}/dns_records/{id} | Atualizar registro DNS |
| DELETE | /zones/{zoneId}/dns_records/{id} | Deletar registro DNS |
| GET | /zones/{zoneId}/dnssec | Obter status DNSSEC |
| PATCH | /zones/{zoneId}/dnssec | Atualizar DNSSEC |
| GET | /zones/{zoneId}/settings | Listar settings |
| PATCH | /zones/{zoneId}/settings/{id} | Atualizar setting |
| GET | /zones/{zoneId}/dns_settings | Obter DNS settings |
| PATCH | /zones/{zoneId}/dns_settings | Atualizar DNS settings |
| GET | /accounts/{accountId}/workers/scripts/{scriptName}/tail | Iniciar sessão Tail (WS) |
| POST | /accounts/{accountId}/workers/scripts/{scriptName}/tail | Iniciar sessão Tail (WS) |
| GET | /accounts/{accountId}/workers/tail | Conectar ao Tail via WebSocket |
| Query | Descrição |
|---|---|
| dnsAnalyticsAdaptiveGroups | Analytics agregados por dimensão |
Dimensões usadas: datetimeFifteenMinutes, queryName, queryType, responseCode, coloName, ipVersion, protocol
-
flutter_secure_storage_web usa dart:html: Não compatível com WASM. Web build usa JavaScript renderer.
-
Syncfusion Maps precisa de GeoJSON específico: world.json deve ter propriedade
namepara cada país. -
CI precisa de build_runner: Sempre adicionar step de code generation antes de analyze/build.
Status: Atualizado Data: 2026-01-23
Contexto: Ao abrir uma zona (DNS) ou um Worker, o usuário inicia na aba principal. Navegar para outras abas causava skeleton loading mesmo com cache SWR, devido ao delay inicial da API.
Decisão: Implementar preload inteligente das abas secundárias em segundo plano:
- Gatilho: Mudança de zona selecionada ou abertura de detalhes de um Worker.
- Prioridade: Aba ativa carrega imediatamente.
- Background: Abas secundárias carregam após delay de 300-800ms.
- Abrangência:
- DNS: Records, Analytics e Settings.
- Workers: Overview, Triggers e Settings.
Implementação:
TabPreloaderNotifier(DNS)WorkerTabPreloader(Workers)
Consequência:
- Navegação entre abas torna-se instantânea
- Redução drástica na percepção de latência da rede
- Melhor aproveitamento do ciclo de vida dos providers Riverpod
Status: Aceito Data: 2025-12-31
Contexto: Lista de data centers Cloudflare (IATA codes) precisa estar atualizada. Atualmente carrega de asset e atualiza do CDN em runtime.
Decisão: Adicionar sync na build para garantir dados mais recentes:
- Makefile target:
sync-datacenters - Fonte: GitHub raw (insign/Cloudflare-Data-Center-IATA-Code-list)
- Dependência:
depsdepende desync-datacenters
sync-datacenters:
@curl -fsSL $(IATA_URL) -o assets/data/cloudflare-iata-full.json
deps: sync-datacenters
@flutter pub getFluxo:
make deps → sync-datacenters → pub get → código gerado
Formato JSON (já compatível):
{
"AMS": {"place": "Amsterdam, Netherlands", "lat": 52.3, "lng": 4.8, "cca2": "NL"},
...
}Consequência:
- Dados sempre atualizados em cada build
- Nenhuma conversão necessária (formato compatível)
- CDN runtime continua como fallback
Status: Aceito Data: 2026-01-20
Contexto: Cloudflare Pages não suporta Flutter nativamente. O build baixava o Flutter SDK (~1.4GB) a cada deploy, levando ~3 minutos. Tentativas de usar cache do CF Pages (via wrapper Eleventy) falharam - o cache era salvo mas não restaurado.
Decisão: Migrar build web para GitHub Actions com deploy via wrangler pages deploy:
- Build: GitHub Actions com
subosito/flutter-action(cache nativo funciona) - Deploy:
cloudflare/wrangler-actionfaz upload direto para CF Pages - CF Pages: Hook de build pausado (apenas recebe uploads)
Workflow (.github/workflows/build.yml):
build-web:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: 'stable'
cache: true # Cache do Flutter SDK
- run: flutter pub get
- run: flutter build web --release
- uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy build/web --project-name=dash-for-cfSecrets necessários:
CLOUDFLARE_ACCOUNT_ID: ID da conta CloudflareCLOUDFLARE_API_TOKEN: Token com permissão "Cloudflare Pages: Edit"
Configuração CF Pages:
- Settings → Builds & deployments → Pause deployments
Resultado:
| Métrica | CF Pages Build | GitHub Actions |
|---|---|---|
| Tempo (1º run) | ~3 min | ~2 min |
| Tempo (cached) | ~3 min (sem cache) | ~1m48s |
| Cache Flutter | ❌ Não funciona | ✅ Funciona |
Consequência:
- Builds mais rápidos com cache funcionando
- Controle total do pipeline via YAML
- Mesma URL de deploy: https://cf.dash.ad
- Requer manter secrets do Cloudflare no GitHub
Status: Aceito Data: 2026-01-22
Contexto: Deployments de Pages passam por estados transitórios (queued, building). Usuário precisa ver atualização sem refresh manual.
Decisão: Implementar polling condicional que:
- Inicia automaticamente quando há items em estado ativo (building/queued)
- Para automaticamente quando todos items completam
- Usa Timer com intervalo de 5 segundos (balance entre responsividade e rate limit)
Implementação:
class _PageState extends ConsumerState<Page> {
Timer? _pollingTimer;
static const _pollingInterval = Duration(seconds: 5);
void _updatePolling(List<Item> items) {
final hasActive = items.any((i) => i.isBuilding);
if (hasActive && _pollingTimer == null) {
_pollingTimer = Timer.periodic(_pollingInterval, (_) {
ref.read(provider.notifier).refresh();
});
} else if (!hasActive && _pollingTimer != null) {
_pollingTimer?.cancel();
_pollingTimer = null;
}
}
@override
void dispose() {
_pollingTimer?.cancel();
super.dispose();
}
}Aplicações:
PagesListPage: Polling quando projeto tem deploy ativoPagesProjectPage: Polling quando deployment está building/queuedDeploymentLogsNotifier: Polling de logs a cada 3s durante build
Consequência:
- UX responsiva sem refresh manual
- Timer cleanup no dispose evita memory leaks
- Polling para automaticamente (não desperdiça recursos)
- Rate limit respeitado (5s é conservador)
Status: Aceito Data: 2026-01-23
Contexto: O conjunto de ícones padrão Material Icons é limitado em termos de personalização visual (weight, fill, grade). Para um visual mais moderno e alinhado com o Material 3 avançado, era necessária uma biblioteca mais flexível.
Decisão: Substituir todos os ícones pela biblioteca material_symbols_icons.
Padrões estabelecidos:
- Usar a classe
Symbolsem vez deIcons. - Para Bottom Navigation e abas, usar
fill: 0para o estado inativo efill: 1para o estado selecionado. - Ícones principais definidos:
- DNS:
graph_3 - Analytics:
finance_mode - Pages:
electric_bolt - Settings:
settings
- DNS:
Consequência: Consistência visual aprimorada e maior controle sobre a expressividade dos ícones sem aumentar o tamanho do bundle significativamente.
Status: Aceito Data: 2026-01-23
Contexto: Usuários que alternam entre DNS, Analytics e Pages frequentemente perdiam o contexto ao fechar o app, sempre retornando para a tela inicial (DNS).
Decisão: Implementar persistência automática da última rota visitada.
Implementação:
- Adicionado campo
lastVisitedRouteao modeloAppSettings. AppRouterutiliza um listener noGoRouterpara disparar atualizações de configuração sempre que a rota muda.- Na inicialização, o
initialLocationdo router é extraído das configurações salvas.
Consequência: Melhoria significativa na UX e continuidade do fluxo de trabalho do usuário.
Status: Aceito Data: 2026-01-25
Contexto: Com o crescimento da base de código, testes manuais tornaram-se inviáveis e propensos a erro. Era necessário definir uma estratégia clara para garantir a qualidade do código em diferentes níveis.
Decisão: Adotar a estratégia da Pirâmide de Testes, dividindo os testes em três camadas com responsabilidades distintas:
-
Unit Tests (
test/unit/):- Foco: Lógica de negócios pura, parsers, models (Freezed), e transformações de dados.
- Ferramentas:
flutter_test,mockito. - Volume: Maior quantidade, execução rápida (<10ms).
- Dependências: Mockadas completamente.
-
Widget Tests (
test/widget/):- Foco: Componentes de UI isolados, interações simples (taps, inputs), e renderização condicional.
- Ferramentas:
widget_test,pumpWidget. - Volume: Quantidade média.
- Dependências: Providers Riverpod sobrescritos com mocks ou dados estáticos.
-
Integration Tests (
test/integration/):- Foco: Fluxos completos do usuário (ex: adicionar registro DNS, rollback de deployment).
- Ferramentas:
integration_test(simulado viawidget_testcomRobot Pattern). - Volume: Menor quantidade, execução mais lenta.
- Dependências: Simula o app inteiro, mas com mocks na camada de rede (Dio/Retrofit) para determinismo.
Consequência:
- Maior confiança em refatorações.
- Documentação viva do comportamento do sistema.
- Execução rápida no CI (
make check).
Status: Aceito Data: 2026-01-25
Contexto: Aplicações modernas tendem a remover botões de "Salvar" explícitos para reduzir fricção. No entanto, salvar a cada keystroke pode causar excesso de requisições e validações prematuras.
Decisão: Implementar um padrão híbrido de auto-save dependendo do tipo de input:
-
Inputs de Texto (TextField/TextFormField):
- Gatilho:
onBlur(perda de foco) ouonEditingComplete(Enter). - Justificativa: Evita requisições incompletas enquanto o usuário digita.
- Gatilho:
-
Toggles e Seletores (Switch/Dropdown):
- Gatilho:
onChange(Imediato). - Justificativa: A intenção do usuário é clara e a mudança é binária/atômica.
- Gatilho:
-
Feedback Visual:
- Exibir indicadores sutis de carregamento ou notificações (Toasts) em caso de erro.
- Manter o estado local otimista quando possível (ver ADR-009).
Consequência:
- UX mais fluida e moderna.
- Redução de cliques desnecessários.
- Necessidade de tratamento de erros robusto para reverter estados otimistas em caso de falha.
Status: Aceito
Data: 2026-01-26
Contexto: A Cloudflare introduziu Smart Placement para otimizar a latência de Workers/Pages e Observability para logging avançado. O app precisava suportar essas configurações de forma consistente.
Decisão:
-
Criar modelos reutilizáveis
PlacementeObservabilitypara ambos os módulos. -
Implementar interface de configuração granular (Cards/Toggles) nos Settings.
-
Workers suportam
head_sampling_rateespecífico para Observability.
Consequência: UX consistente entre Workers e Pages. Preparação para dashboards de telemetria futuros.
Status: Aceito
Data: 2026-01-26
Contexto: Alguns campos na documentação da Cloudflare ou nomes de recursos internos (ex: "build cache") divergem dos nomes exatos exigidos pela API REST v4 (ex: "build_caching").
Decisão: Priorizar nomes de campos da API REST em detrimento de terminologias de marketing ou dashboard oficial nas anotações @JsonKey.
Implementação:
-
BuildConfig: MapearbuildCacheparabuild_caching. -
WorkerBinding: Usarproject_namepara bindings do tipoaiconforme resposta do payload da API. -
PagesSourceConfig: Usarproduction_deployments_enabled(plural) para controle de deployments em produção.
Consequência: Prevenção de falhas silenciosas na persistência de dados. Facilidade de debug cruzando logs do app com a documentação oficial da API.
Status: Aceito Data: 2026-01-30
Contexto: Usuários com dispositivos OLED/AMOLED solicitaram opção de dark mode com black puro (#000000) em vez do dark gray padrão do Material 3 (#121212) para maximizar economia de bateria.
Pesquisa Realizada:
- Material Design 3 recomenda #121212 para evitar "vibração visual" de cores saturadas sobre black puro
- Diferença de bateria entre #000000 vs #121212: apenas 0.3% (1.2mW), mas perceptível em uso prolongado
- 81.9% dos usuários preferem dark mode para conforto visual
- Em OLED/AMOLED, pixels pretos (#000000) desligam completamente = máxima economia
Decisão: Implementar toggle opcional "AMOLED Black" na página Settings que:
- Adiciona campo
amoledDarkMode: boolaoAppSettings(default: false) - Cria tema
AppTheme.amoledcom pure black backgrounds (#000000) - Aplica automaticamente quando dark mode está ativo e toggle habilitado
- Mitiga "vibração visual" usando:
- Containers/Cards: #1A1A1A (10% white) para hierarquia visual
- Bordas sutis: #404040 e #2A2A2A para separação
- Texto: #FFFFFF (contraste máximo WCAG AAA)
Implementação:
// AppSettings model
@Default(false) bool amoledDarkMode,
// AppTheme
static ThemeData get amoled {
final amoledColorScheme = ColorScheme.fromSeed(
seedColor: _cloudflareOrange,
brightness: Brightness.dark,
).copyWith(
surface: const Color(0xFF000000), // Pure black
surfaceContainerHighest: const Color(0xFF1A1A1A), // 10% white
outline: const Color(0xFF404040), // Subtle borders
);
// ... rest of theme config
}
// main.dart
darkTheme: useAmoled ? AppTheme.amoled : AppTheme.dark,UI: SwitchListTile no Theme Card da Settings Page com ícone Symbols.contrast.
Consequência:
- Usuários têm controle total: podem desativar se preferirem #121212 padrão
- Economia marginal de bateria mas perceptível em uso prolongado
- Cards com #1A1A1A mantêm hierarquia visual sem "desaparecer" no black puro
- Bordas sutis evitam saturação excessiva sobre black
Fontes:
- Material Design 3 Dark Theme
- AMOLED Black vs Gray Battery Test (XDA)
- Implementing Dark Mode in Flutter
Última atualização: 2026-02-15 (adicionados ADR-036 CI/CD Quality Gates, ADR-037 Token Sanitization)
Status: Aceito
Data: 2026-01-29
Contexto: Logs em tempo real do Workers exigem streaming contínuo. Polling não é adequado (latência e custo de API). A Cloudflare fornece API de Tail Logs via WebSocket.
Decisão:
- Usar WebSocket para streaming de logs (tail) com sessão dedicada por script.
- Adotar
web_socket_channelcomo dependência para gerenciamento de conexão. - Modelos
TailSessioneTailLog(Freezed) para serialização consistente. - Notifier dedicado (
WorkerTailNotifier) para:- iniciar/parar sessão
- reconectar quando necessário
- filtrar por nível (all/log/warn/error)
- limpar buffer de logs sem encerrar a sessão
Consequência:
- Logs em tempo real com baixa latência
- Melhor UX com status de conexão e auto‑scroll
- Maior complexidade de estado (sessão ativa + buffer + filtros)
Status: Aceito Data: 2026-02-15
Contexto: O pipeline de CI/CD anterior tinha testes e builds em workflows separados sem gates de qualidade objetivos. Cobertura de testes e issues do analyzer não eram verificados automaticamente, permitindo regressões silenciosas.
Decisão: Reestruturar CI/CD em três workflows com quality gates quantitativos:
-
CI (
ci.yml): Roda em todas as branches (push/PR):- Verificação de código gerado commitado (
.g.dart,.freezed.dart) - Analyze budget gate (max 50 issues, via
tool/ci/check_analyze_budget.sh) - Coverage threshold gate (min 25%, via
tool/ci/check_coverage.sh) - Upload para Codecov com
codecov.ymlde configuração - Artefatos de coverage e analyze para debugging
- Verificação de código gerado commitado (
-
Build (
build.yml): Roda em main (push/PR):- Setup compartilhado com cache de código gerado
- Builds paralelos: Web (+ deploy CF Pages), Android (com signing)
- Linux e macOS disponíveis (desabilitados por ora)
-
Release (
release.yml): Disparado por tagsv*ou manual:- Builds completos (Web, Android)
- Criação automática de GitHub Release com notes geradas
- APK assinado como artefato do release
Quality Gates locais:
make analyze: Usacheck_analyze_budget.sh(budget: 50)make coverage: Usacheck_coverage.sh(threshold: 25%)make release V=patch|minor|major: Automação completa de versionamento
Consequência:
- Regressões de qualidade detectadas automaticamente
- Budget controlado permite redução gradual de issues
- Releases automatizados eliminam erros manuais de versionamento
- Codecov fornece visibilidade de cobertura por PR
Status: Aceito Data: 2026-02-15
Contexto: O LogService armazena logs que podem ser exportados (clipboard, arquivo). API tokens da Cloudflare (40+ caracteres) podem vazar nos logs via headers de request, mensagens de erro ou state changes.
Decisão: Adicionar sanitização automática de tokens no LogService._log():
- Padrão Regex: Detecta strings alfanuméricas de 40+ caracteres (padrão de API tokens Cloudflare).
- Redação: Substitui por
<4 primeiros chars>...[REDACTED]. - Aplicação: Sanitiza tanto
messagequantodetailsantes de armazenar/exportar. - LoggingInterceptor: Removido log do header Authorization que expunha parcialmente o token.
static final _tokenPattern = RegExp(r'(?:Bearer\s+|Authorization:\s*)?([A-Za-z0-9_-]{40,})', caseSensitive: false);
String _sanitize(String text) {
return text.replaceAllMapped(_tokenPattern, (match) {
final token = match.group(1)!;
if (token.length >= 40) {
return '${token.substring(0, 4)}...[REDACTED]';
}
return match.group(0)!;
});
}Consequência:
- Tokens nunca aparecem em logs exportados
- Logs continuam úteis para debug (4 chars identificam qual token)
- Segurança por default (sanitização automática, não opt-in)