TODO
Roadmap executável do Ioruba. Baseline revisada em 2026-08-12 a partir do
estado real do release v1.8.2.
Todos os Scrums até o 11 estão cumpridos, e os Scrums 13, 14, 15 e 18 estão
fechados. O Scrum 16 entregou o export de sessionStats, mas segue aberto
(persistência e comparação entre sessões). O histórico detalhado vive no git e
no CHANGELOG.md; este documento olha para frente.
Formato:
[x]concluído ·[ ]pendente- descrição
(tag/tag/tag)-fácil|médio|difícil
Estado atual (baseline v1.8.2)
- Firmware (
firmware/arduino/ioruba-controller):NUM_KNOBSparametrizável porIORUBA_NUM_KNOBS, com tabela de pinos analógicos por placa (Nano, Uno, Mega2560, Leonardo/Micro, ESP32, RP2040, ESP8266) estatic_assertcontra o limite de canais da placa.ADC_MAXderivado deIORUBA_ADC_BITS(10-bit AVR, 12-bit ESP32/RP2040). HandshakeHELLO board=...; fw=...; protocol=...; knobs=...; mcu=...; adcBits=..., framev0|v1|..., calibração + EEPROM (magic/schema). Botões e encoders opcionais (IORUBA_NUM_BUTTONS/IORUBA_NUM_ENCODERS) emitemEVapós opt-inEVENTS ON.PROTOCOL_VERSION=2. - Shared (
packages/shared): protocolo, perfil e runtime genéricos em contagem de knobs e em resolução de ADC — o lockSLIDER_MAX=1023saiu, a normalização usafirmwareInfo.adcBitse o parser de frame aceita até 16-bit. Perfil temcontrolscom bindingsmute/next/prev, emuteaceita umtarget(AudioTarget) opcional —next/prevrejeitamtargetna validação. - Desktop (
apps/desktop): Tauri 2 + React 19, store Zustand, serial viatauri-plugin-serialpluginv3 (stream e auto-reconnect nativos viawatch()). Backends de áudio:linux(pactl: master/app/source/sink),windows(WASAPI: master),macos(CoreAudio: master),unsupported. Shell com sidebar compacto, status pill de runtime, dialog central de configurações, changelog embutido e notificação opt-in de release nova. Telemetria de sessão exportável (JSON/CSV), watch log, wizard de calibração, painel de hardware, editor visual de botões e encoders (com escolha do alvo do mute a partir do inventário de áudio), i18n en/pt-BR/es. - Distribuição: release multiplataforma (
deb/rpm/AppImage/NSIS/MSI/app) + AUR/Homebrew/Scoop/winget + provenance; instalador one-line (scripts/install.sh/install.ps1) e auto-update in-app assinado vialatest.json.
Prioridade declarada: integração hardware↔SO, mais placas, eficiência, organização, ampliação, distribuição e UX completa.
Scrum 11 — Hardware: mais placas e mais knobs
Foco principal pedido. Hoje só Nano AVR com 3 pinos fixos.
- Parametrizar
ANALOG_PINSpor placa em vez de{A0,A1,A2}fixo — tabela de pinos por MCU/board selecionada em compile-time, dimensionada porNUM_KNOBS(firmware/hardware)-médio- Tabela
ANALOG_PINSpor placa (ARDUINO_AVR_*/ESP32/RP2040),static_assert(NUM_KNOBS <= ANALOG_PIN_COUNT). Usa os primeiros NUM_KNOBS canais.
- Tabela
- Matriz de compilação de firmware no CI por FQBN (Nano, Uno, Mega2560, Leonardo, Micro) via
arduino-cli, espelhando o gate atual(firmware/ci/hardware)-médio- Job
firmwarematrizado por FQBN + jobfirmware-host(parser default + wide 8k/12-bit). Scriptsfirmware:compile:matrix/firmware:test:wide.
- Job
- Suporte a Arduino Mega (A0..A15) habilitando >6 knobs no mesmo board — validar limites de ADC e frame
(firmware/hardware/expansão)-médio- Mega compila com 12 knobs (verificado local + host wide 8 knobs). Frame/EEPROM/struct já dimensionados por NUM_KNOBS.
- Suporte a placas de 12-bit (ESP32, RP2040/Pico): reportar
adcBitsno handshake e normalizar a resolução no shared (hojeSLIDER_MAX=1023fixo quebra 4095)(firmware/shared/protocol)-difícil- Firmware deriva
ADC_MAXdeIORUBA_ADC_BITS(auto 12 em ESP32/RP2040, 10 em AVR). Shared remove o lock 1023: funções de mixer/runtime recebemadcMaxopcional e o parser de frame aceita até 16-bit; normalização usafirmwareInfo.adcBits. Ainda pendente: toolchain real ESP32/RP2040 e teste em hardware.
- Firmware deriva
- Toolchain para ESP32/RP2040 (core
arduino-cliadicional ou avaliação de PlatformIO)(firmware/build/hardware)-difícil- Optou-se por cores
arduino-cli(não PlatformIO): job CIfirmware-archmatrizaesp32:esp32+rp2040:rp2040(earlephilhower) com caches próprios. Ambos compilam (verificado local). Corrigida colisãoBOARD_NAMEcom macro do core arduino-pico.
- Optou-se por cores
- Handshake estendido: reportar
board,mcueadcBits; bumpPROTOCOL_VERSIONse incompatível, com fallback para v2(firmware/protocol)-médiomcu/adcBitsadicionados como campos aditivos do handshake; protocolo mantido em v2 (campos opcionais, hosts antigos ignoram, novos assumem 10-bit quando ausentes) — sem quebra de compatibilidade.
- Detecção automática e exibição do board/MCU no desktop a partir do
board=do handshake(frontend/hardware/ux)-fácil- Tile “Hardware” no
OverviewSignalPanelmostra board · MCU +adcBits-bit · protocolo (com aviso de incompatibilidade).
- Tile “Hardware” no
- Suporte a botões/encoders além de potenciômetros (mute/next/prev) — novo tipo de input no protocolo e perfil
(firmware/shared/expansão)-difícil- Firmware aceita
IORUBA_NUM_BUTTONS/IORUBA_NUM_ENCODERS, usaINPUT_PULLUP, debounce e quadratura, e só emiteEV type=...após opt-inEVENTS ONpara preservar desktops antigos. Shared parseiaEVbutton/encoder; perfil ganhoucontrolscom bindingsmute/next/prev; desktop resolve eventos, executa ações via Tauri (pactl/playerctlno Linux, mute no Windows) e registra suporte/erros no watch log.
- Firmware aceita
- Documentar pinagem e matriz de placas suportadas em
docs/guides/hardware-setup.md(docs/hardware)-fácil- Seção “Supported boards” (tabela MCU/bits/canais/max-knobs/ordem de pinos) + mirror PT-BR.
Scrum 12 — Integração SO↔áudio mais profunda
Hoje Windows/macOS só controlam master. Linux tem cobertura completa.
- Per-app volume no Windows via
IAudioSessionManager2/ISimpleAudioVolume— targetsapplicationfora do Linux(backend/audio/windows)-difícil - Enumerar e controlar
sink/sourceno Windows (devices de saída/entrada)(backend/audio/windows)-difícil - Avaliar per-app volume no macOS (sem API pública trivial; investigar
AudioObjectpor processo ou rejeitar formalmente)(backend/audio/macos/research)-difícil - Ação de mute/toggle direcionada —
dispatch_control_action(action, target)aceitaAudioTargetopcional e resolve sink/source/application no Linux (set-sink-mute/set-source-mute/set-sink-input-mute); Windows aceitamasterou sem alvo, rejeita o resto(backend/shared/frontend)-médioControlConfigganhoutarget?. Os matchers de sink/source foram extraídos deapply_targetspararesolve_sink_matches/resolve_source_matches, compartilhados entre knob e mute.targetsó é aceito comaction: "mute"(next/prevfalam com o player MPRIS). O editor visual (Configurações › Editor de perfil › Botões e encoders) cria os bindings sem passar pelo JSON.
- Toggle de mute atribuível a um knob (hoje só botão/encoder disparam
ControlConfig) — pendência herdada do item acima(shared/frontend/ux)-médio - Mapear hotkeys globais (
tauri-plugin-global-shortcutjá presente) a ações de mixagem(frontend/backend/ux)-médio - Avaliar backend PipeWire nativo no Linux (sem fork/exec de
pactl)(backend/audio/linux/research)-difícil - Estudo: transporte MIDI como alternativa à serial para controladores genéricos
(backend/protocol/research)-difícil
Scrum 13 — Eficiência e otimização
- Estender o cache de inventário (TTL ~250ms, já existe no Linux) aos backends Windows/macOS — hoje re-inicializam COM/CoreAudio a cada chamada
(backend/audio/performance)-médioTtlCache<T>+INVENTORY_TTLemaudio/common.rs, com 7 testes host-independentes. Os três backends passam a concordar sobre quão velho aceitam ficar; o TTL também é o mecanismo que faz troca de device padrão ser notada sem assinarIMMNotificationClient. Linux mantémget/storepara não segurar o mutex durante o fork/exec dopactl.
- Reusar handle de device (COM apartment /
IMMDevice/AudioObjectID) entre chamadas respeitando thread-affinity(backend/audio/performance)-difícil- Windows: thread dedicada (
ioruba-wasapi) entra no apartment uma vez e é dona doIAudioEndpointVolume; chamadas viram jobs enviados por canal. Afinidade deixa de ser algo a raciocinar — antes cada escrita de volume faziaCoInitializeEx+CoCreateInstance+GetDefaultAudioEndpoint+Activateinteiros. Um lote de sliders é um único hop. macOS: oAudioObjectIDnão tem afinidade, mas o probeHasProperty/IsPropertySettable(até 8 canais × 2 chamadas) rodava por escrita; virou uma estratégiaVolumeElementsresolvida uma vez e cacheada com o id. Escrita que falha invalida na hora.
- Windows: thread dedicada (
- Coalescing/debounce de writes de volume sob movimento rápido de knob, por target
(backend/runtime/performance)-médioscheduleAudioFlushvirou throttle leading+trailing (AUDIO_APPLY_MIN_INTERVAL_MS40ms; comsmoothTransitionsusa otransitionDurationMsdo perfil): primeiro lote sai imediato, rajadas coalescem num flush trailing com o valor mais recente por slider. Corrige também o starvation do debounce puro anterior, que só aplicava áudio quando o knob parava. +2 testes com fake timers.
- Reduzir o bundle do chart trocando
rechartspor uma lib mais leve — o chunkchartsestá em 368.91 kB (gzip 106.66 kB)(frontend/bundle/performance)-médio- Trocado por um componente SVG próprio (~8 kB) com a mesma spline monotone (Fritsch–Carlson, o que o recharts pegava do
curveMonotoneXdo d3-shape), grid, eixos, cursor e tooltip. Olazynunca funcionou: omanualChunksmandavareact/index.jsereact-dom/index.jspara o chunkcharts, o que dava ao entry uma dependência estática dele e fazia o Vite emitirmodulepreloadnoindex.html— os 360 kB desciam em todo boot. Payload de boot medido: 878.45 kB → 535.15 kB raw, 252.14 kB → 154.49 kB gzip (-39%). Olazy/Suspensesaiu junto: não se paga para 8 kB.
- Trocado por um componente SVG próprio (~8 kB) com a mesma spline monotone (Fritsch–Carlson, o que o recharts pegava do
- Instrumentar e logar latência knob→áudio no watch log (já há timings de boot/connect/refresh)
(observability/performance)-fáciluse-serial-runtimecronometraapplySliderTargetsBatchcomperformance.now(); emitewarningno watch log quando passa deAUDIO_APPLY_SLOW_MS(80ms), com tempo + nº de alvos (sem flood).
- Perfilar consumo em sessão longa (telemetria + watch log) e validar ausência de leaks
(performance/observability)-médioioruba-store.soak.test.ts: 20k frames seriais e asserção de que toda coleção para de crescer, mais uma asserção genérica sobre o tamanho serializado da store inteira (para que um campo futuro sem teto falhe aqui sem ninguém lembrar de estender o teste). Achou um leak real:pushNotificationdeduplicava por id mas nunca aparava — era a cadência de release (6h) segurando a lista, não o código. Capado em 100.
Scrum 14 — Organização e qualidade de código
- Extrair lógica duplicada dos backends (
describe_target,summarize_slider_outcomerepetidos emwindows.rs/macos.rs/linux.rs) paraaudio/common.rs(backend/refactor/organização)-médioaudio/common.rs(sem cfg, compila em toda plataforma):describe_target,volume_percenteMasterOnlyBackend::apply_batch— o loop de apply master-only dos backends Windows/macOS virou uma chamada genérica sobre a closureset_master_volume; strings de outcome parametrizadas por plataforma, comportamento preservado.
- Testes host-independentes para
summarize_slider_outcome/describe_target(hoje sem testes emwindows.rs/macos.rs)(test/backend/coverage)-fácil- 7 testes em
common.rscom setter fake: happy path, dedupe de master, erro do setter, targets não suportados, mix updated+unavailable, lista vazia, clamp/round de percent. Rodam no gate Linux e nos smokes nativos.
- 7 testes em
- Cobertura de testes do store Zustand (
ioruba-store.ts), incl. reset desessionStatsvia wrapper doset(test/frontend/coverage)-médio- +3 testes: acumulação de
sessionStatsviaprocessSerialLine(min/max/last percent por knob), reset explícito viaresetSessionStats, e reset automático pelo wrapper dosetquando uma action zeratelemetry(viasetDemoMode(false)).
- +3 testes: acumulação de
- Documentar o contrato Rust↔TS dos backends e o dispatch por
cfgemaudio/mod.rs(docs/backend/organização)-fácildocs/guides/audio-backend-contract.md(+ mirror PT-BR): comandos Tauri, convenções serde (camelCase/lowercase/tag kind), matriz de capacidade por backend, dispatch cfg, modelo de outcome, papel docommon.rse dos smokes nativos.
- Gate de
shellcheckno CI parascripts/install.she demais scriptssh(ci/quality)-fácil- Job
scripts-lintno CI rodashellcheckem install.sh/run-appimage-compat.sh/validate-appimage.sh (passam limpo). Script localnpm run lint:scripts.
- Job
- Lint de PowerShell (
PSScriptAnalyzer) parascripts/install.ps1(ci/quality)-fácil- Mesmo job
scripts-lint: PSScriptAnalyzer via pwsh, reporta Warning+Error, falha só em Error.
- Mesmo job
Scrum 15 — Distribuição e updates
- Auto-updater in-app (
tauri-plugin-updater+latest.jsonno release)(dist/release/security)-difícil- Chave Tauri gerada e guardada como secrets
TAURI_SIGNING_PRIVATE_KEY/password; pública embutida notauri.conf.json; privada original guardada fora do git em~/.config/ioruba/updater/(mode 600; precisa de backup). Plugin Rust + permissões + prompt explícito no app verificam assinatura, baixam, instalam e relançam. Cada matriz sobe.sig; job serial cria umlatest.jsoncompleto no fim, evitando a corrida de delete/upload que ocorreria se os 4 runners o escrevessem em paralelo. Sem key/assinatura o release falha fechado.
- Chave Tauri gerada e guardada como secrets
Assinatura + notarização macOS— descartado por decisão de projeto(dist/macos/security)- O projeto não possui e não pretende adquirir Apple Developer ID nem certificado Authenticode. Toda a ramificação condicional de assinatura foi removida do
release.yml: um único passo de build, sem secretsAPPLE_*/WINDOWS_CERTIFICATE*. A integridade segue verificável porSHA256SUMS.txte por attestations do GitHub, e o updater in-app mantém assinatura própria (minisign), independente disso. Instalador e cask fazem strip de quarantine; o README documenta o aviso do SmartScreen. - Não reintroduzir passos condicionais: a ramificação anterior sobreviveu à remoção dos imports em
def4782e deixouWINDOWS_CERTIFICATE_THUMBPRINT/APPLE_SIGNING_IDENTITYindefinidas — o workflow parecia assinar e não assinava.
- O projeto não possui e não pretende adquirir Apple Developer ID nem certificado Authenticode. Toda a ramificação condicional de assinatura foi removida do
- Gerar
.dmgno macOS além do.app.tar.gz(dist/macos)-médio- Não habilitado cegamente: o script DMG do Tauri chama Finder via AppleScript, que já falhou por autorização intermitente em runner GitHub-hosted; adicioná-lo à matriz hoje faz uma release inteira falhar. O
.app.tar.gzé o artefato reproduzível. Como a assinatura macOS foi descartada, só vale reabrir se o caminho de DMG se provar estável num draft — semcodesign/spctlpara servir de gate.
- Não habilitado cegamente: o script DMG do Tauri chama Finder via AppleScript, que já falhou por autorização intermitente em runner GitHub-hosted; adicioná-lo à matriz hoje faz uma release inteira falhar. O
- Manifest Homebrew cask para macOS
(dist/macos/packaging)-médio- Job
package-manifestsgera o cask depois deSHA256SUMS.txte o publica embernardopg/homebrew-iorubavia deploy key isolada (host key GitHub pinada). Usuário:brew tap bernardopg/ioruba && brew install --cask ioruba.
- Job
- Manifest Scoop e submissão winget para Windows
(dist/windows/packaging)-médio- Mesmo job publica
ioruba.jsonembernardopg/scoop-iorubapor outra deploy key isolada; usuário:scoop bucket add ioruba https://github.com/bernardopg/scoop-ioruba && scoop install ioruba. Três YAMLs winget são anexados à release; a submissão inicial é microsoft/winget-pkgs#415149. PR automático futuro exigiria GitHub App/token fine-grained próprio, não um PAT pessoal amplo.
- Mesmo job publica
- Automatizar publicação do AUR (
ioruba-desktop/-bin) no fluxo de release(dist/linux/ci)-médio- Já existia: job
aur-publishnorelease.ymlgera PKGBUILD/.SRCINFO, clona os dois pacotes por SSH (host key pinada, semssh-keyscan) e faz push. O item estava marcado como pendente por engano. Endurecido na v1.7.1 com retry e backoff — o release da v1.7.1 bateu num outage do AUR (The AUR is down due to maintenance) e o job morreu na primeira tentativa.
- Já existia: job
- Endurecer o instalador one-line: testar arm64 Linux/macOS e cobrir ausência de assets
(dist/installer/quality)-fácil- Corrigido fallback perigoso: padrões como
_arm64.deb$|.deb$escolhiam o primeiro.deb(amd64) quando não havia build arm64 e o instalavam silenciosamente.require_asset_urlagora exige arquitetura exata e, na ausência, mostra os assets da release. 13 testes por fixture cobrem tokens x86_64/arm64/aarch64, ausência, ambiguidade e release vazia; ShellCheck + teste entram no CI.
- Corrigido fallback perigoso: padrões como
Scrum 16 — Telemetria e dados
- Export dos
sessionStatspara arquivo (JSON/CSV) via dialog, reusando o padrão do export de perfil/watch log(frontend/backend/telemetry)-fácil- Formatters puros
sessionStatsToJson/sessionStatsToCsvno shared (+testes), comando Tauriexport_session_stats(filtros JSON/CSV), botões JSON/CSV noSessionStatsPanel.
- Formatters puros
- Histórico de telemetria persistente em disco (opt-in) para análise pós-sessão
(backend/telemetry/persistence)-médio - Visualização comparativa entre sessões (picos, médias, duração)
(frontend/telemetry)-médio
Scrum 17 — Ampliação: automação e comunidade
- Regras condicionais de mixagem (“quando o app X tocar, reduzir Y”) — subsistema acima do mapeamento knob→target; exige design/spec
(shared/backend/frontend/expansão)-difícil - Galeria/repositório de presets compartilháveis pela comunidade (import/export por arquivo já existe)
(frontend/product/expansão)-difícil - Perfis por aplicação ativa (trocar mapeamento conforme o app em foco)
(backend/frontend/expansão)-difícil
Scrum 18 — Experiência completa
- Wizard de calibração de knobs na UI (ler/escrever
minRaw/maxRaw/deadzone via comandoCONFIG, que já existe no protocolo)(frontend/firmware/ux)-médioCalibrationWizardna seção Hardware: fluxo mín→máx→revisão por knob com leitura ao vivo e rastreio do extremo observado; valida faixa mínima (16 contagens) e gravacalibrationno slider do perfil ativo viaupdateActiveProfileConfig— o runtime serial já sincroniza o firmware viaCONFIGquando o perfil diverge. +3 testes de componente.
- Auditoria de acessibilidade (a11y) do dashboard, foco/teclado/aria
(frontend/a11y/ux)-médio- Navegação por setas/Home/End no tablist do sidebar (padrão WAI-ARIA, tabindex itinerante já existia mas os tabs inativos eram inalcançáveis por teclado); foco gerenciado no wizard de calibração (entra na sessão, volta ao botão de origem) +
aria-liveno passo erole="alert"na faixa curta;aria-pressednos filtros do watch log;scope="col"nas tabelas de hardware/estatísticas;role="img"nomeado no gráfico de telemetria;aria-labelno textarea de JSON avançado. Cobertura axe estendida a todos os painéis (HardwarePanel, CalibrationWizard, SessionStatsPanel, WatchLogPanel, OverviewSignalPanel, ProfileWorkbench nas 3 views).
- Navegação por setas/Home/End no tablist do sidebar (padrão WAI-ARIA, tabindex itinerante já existia mas os tabs inativos eram inalcançáveis por teclado); foco gerenciado no wizard de calibração (entra na sessão, volta ao botão de origem) +
- Ampliar i18n além de en/pt-BR (estrutura de
i18n.tsjá suporta)(frontend/i18n)-médio- Espanhol (
es) completo:TEXT_MAP_EScobre 100% das chaves, registro por idioma emLANGUAGE_TEXT_MAPS, uniãoUiLanguageno shared + validação (normalizePersistedStatee editor JSON caem para pt-BR em idioma desconhecido), opção no seletor de perfil. Guia de tradução atualizado (EN + espelho PT).
- Espanhol (
- Exibir board/MCU/
adcBits/protocolo detectados num painel de diagnóstico claro(frontend/hardware/ux)-fácilHardwarePanel(seção Hardware): placa, MCU, resolução do ADC, protocolo (compat.), knobs e calibração por knob, com estado vazio. Integrado à navegação agrupada nova.
- Indicador visual de latência e saúde da conexão sempre visível (alinhado ao
.impeccable.md)(frontend/ux/observability)-fácilConnectionHealthIndicatorno topo do sidebar (sempre visível): dot colorido por estado + label + frescura do sinal (tempo desde o último frame, tick 1s) como proxy de latência. Store ganhoulastFrameAt. +4 testes.
Entregue fora de Scrum (v1.5.2 → v1.8.2)
Trabalho que nasceu de bug report ou de decisão de produto no meio do caminho, sem ter passado por um item planejado. Registrado aqui para a baseline não mentir.
- Auto-heal de perfis salvos antes do bump de baud do firmware (9600 → 115200), que ficavam presos em loop de handshake
(desktop/persistence)-fácil - Firmware 0.6.1: desligar o rádio WiFi no
setup()em ESP8266/ESP32 — o rádio ligado por padrão injetava ruído mensurável no ADC(firmware/hardware)-fácil - Restart limpo quando o binário é trocado em disco durante a execução (upgrade de pacote), no lugar do segfault do WebKitWebProcess ao esconder para a bandeja
(desktop/runtime)-médio - Migração para
tauri-plugin-serialpluginv3:watch()nativo substitui o triolisten()/startListening()/cancelListen(), e o auto-reconnect sai douse-serial-runtimepara o plugin(desktop/serial)-médio - Refresh do shell: sidebar compacto (marca + saúde de conexão + navegação ícone-e-rótulo), status pill flutuante de runtime e ribbon reduzido a sessão e perfil ativo
(frontend/ux)-médio - Dialog central de configurações (idioma, tema, notificações, launch-on-login, versão em execução, changelog) sobre um wrapper reusável de
<dialog>nativo com foco preso, Escape/backdrop e restauração de foco(frontend/ux/a11y)-médio - Notificação opt-in de release nova: checagem a cada seis horas, comparação semver, estado de não-lido, deduplicação e preferências persistidas
(frontend/product)-médio - Links externos por
tauri-plugin-openercom escopo de capability restrito ahttps://github.com/bernardopg/ioruba*; CSP libera sóhttps://api.github.com(security/desktop)-fácil - Dialog de changelog no app deixou de listar a seção
Unreleased— ela descreve trabalho que não está no binário em execução(frontend/product)-fácil
Não-objetivos
- Reintroduzir tooling de build na raiz fora de
apps//packages//firmware/. - Cobertura completa de áudio em plataformas sem backend nativo — nesses casos, modo UI/demo ou suporte parcial com banners explícitos.