Painel de Controle Remoto / ConfigEngine
Painel web + backend Go (Fiber) que liga/desliga telas e botões dos apps Monisat em tempo real — sem publicar release — e coloca telas em manutenção (aviso ao usuário). O painel persiste um config.json local e publica no Firebase Remote Config; os apps (Android + iOS) leem as mesmas chaves e reagem em segundos via Realtime Remote Config.
config_admin é o painel de controle (fonte de edição); o Firebase Remote Config é o canal de distribuição; os apps são os consumidores. O painel nunca fala direto com os apps — sempre via Firebase.
true = LIGADO. Só entra no config com false o que for explicitamente desligado. Erro de rede, chave nova ainda não publicada ou valor STATIC → o app trata como habilitado. Nunca esconde tela/botão por engano.
Visão Geral do Fluxo
Editar no painel
O painel web (web/index.html, servido em /) lista todas as telas e botões a partir do catálogo docs/app_config.android.json. O admin liga/desliga toggles e escreve mensagens de manutenção.
Salvar (persistir)
Salvar alterações → PUT /api/config (Bearer ADMIN_TOKEN). O backend grava o config.json com escrita atômica (arquivo .tmp + rename) e carimba updated_at/updated_by.
Publicar no Firebase
Publicar no Firebase → POST /api/publish. O backend autentica com service account (OAuth2 JWT-bearer), faz merge das chaves no template do Remote Config e publica uma nova versão.
Apps reagem em tempo real
Android (RemoteConfigHelper + RuntimeConfig) e iOS (RemoteConfigService.shared) recebem o diff via Realtime Remote Config e reaplicam telas/botões sem restart — em segundos.
Endpoints do Backend
Segue as convenções do monorepo: struct como fonte de verdade e respostas embrulhadas em { "data": ... }.
/api/config
{ "data": FlagConfig }./api/config
Authorization: Bearer $ADMIN_TOKEN. Substitui os flags e persiste no config.json. Força version=1, normaliza mapas nulos para {} e carimba updated_at; usa X-Admin-User para updated_by./api/publish
FIREBASE_SA_PATH + FIREBASE_PROJECT_ID. Retorna { "data": { "published": true, "version": "N" } }./healthz
{ "data": { "status": "ok" } }./
web/index.html).Contrato — FlagConfig
Fonte de verdade em config_admin/main.go. Persistido em config.json e espelhado nos parâmetros do Firebase Remote Config.
type FlagConfig struct {
Version int `json:"version"`
ScreenFlags map[string]bool `json:"screen_flags"` // ~12 telas
ButtonFlags map[string]bool `json:"button_flags"` // ~90 botões
ScreenMessages map[string]string `json:"screen_messages"` // screen_<x>_maintenance → aviso
UpdateConfig map[string]string `json:"update_config"` // force-update (versões + msg)
UpdatedAt string `json:"updated_at"`
UpdatedBy string `json:"updated_by,omitempty"`
}
| Campo | Tipo | Fail-safe? | Papel |
|---|---|---|---|
screen_flags | {chave:bool} | Sim (ausente/true = ligado) | Kill-switch de tela inteira (~12 chaves) |
button_flags | {chave:bool} | Sim | Kill-switch por botão (~90 chaves) |
screen_messages | {chave:string} | Vazio = sem manutenção | Mensagem de manutenção da tela (não esconde) |
update_config | {chave:string} | Não (vazio publica vazio) | Chaves de force-update lidas no login |
. em nomes de chave (só letras/números/_). Por isso o formato screen_map_enabled / grid_details_finalizeTrip_enabled. As mesmas chaves aparecem no catálogo docs/app_config.android.json e são lidas por Android e iOS.
Kill-switch vs. Manutenção
Dois modos distintos, propositalmente separados:
| Modo | Campo | Chave | Efeito no app |
|---|---|---|---|
| Kill-switch | screen_flags / button_flags | screen_map_enabled = false | Esconde a tela/botão (fica indisponível) |
| Manutenção | screen_messages | screen_map_maintenance = "Mapa em manutenção até 18h" | Tela segue acessível, mas exibe o aviso ao usuário |
A chave de manutenção é derivada da flag da tela trocando o sufixo _enabled por _maintenance (maintenanceKeyFor() no Android, maintenanceKey(for:) no iOS). Mensagem vazia (após trim) = sem manutenção.
Exemplo de config.json
{
"version": 1,
"screen_flags": {
"screen_map_enabled": true,
"screen_checklist_enabled": true,
"screen_grid_trip_enabled": true
// … ~12 telas, todas true por padrão
},
"button_flags": {
"grid_details_finalizeTrip_enabled": true,
"driver_trip_toggleVoice_enabled": true,
"login_entrar_enabled": true
// … ~90 botões
},
"screen_messages": {
"screen_map_maintenance": "Mapa em manutenção até 18h"
},
"update_config": {
"min_required_version_android": "1.0.39",
"latest_version_android": "1.0.40",
"min_required_version_ios": "1.0.31",
"latest_version_ios": "1.0.32",
"update_message": ""
},
"updated_at": "2026-07-04T12:00:00Z"
}
Publicação no Firebase Remote Config
Implementado em config_admin/firebase.go usando somente a stdlib do Go (sem novas dependências):
OAuth2 JWT-bearer
Carrega a service account (FIREBASE_SA_PATH), assina um JWT RS256 com a chave privada (PKCS#8, fallback PKCS#1) no escopo firebase.remoteconfig e troca por um access_token em oauth2.googleapis.com/token.
GET do template
GET .../v1/projects/{id}/remoteConfig — captura o ETag e os parâmetros existentes (preserva o que não é gerido pelo painel).
Merge das chaves
Sobrescreve screen_flags/button_flags (como "true"/"false"), screen_messages e update_config no mapa de parameters, preservando o resto do template.
PUT com If-Match
PUT do template com header If-Match: {ETag} — publica uma nova versão e retorna o versionNumber.
Variáveis de Ambiente
| Variável | Default | Descrição |
|---|---|---|
ADMIN_TOKEN | dev-token (com warning) | Token exigido em PUT /api/config e POST /api/publish (Authorization: Bearer). |
FIREBASE_SA_PATH | — | Caminho do JSON da service account (escopo firebase.remoteconfig). Necessário para publicar. |
FIREBASE_PROJECT_ID | project_id da SA / app-monisat | Id do projeto Firebase de destino. |
PORT | 8090 | Porta HTTP do painel. |
CONFIG_PATH | config.json | Arquivo que guarda os flags. |
Consumo no Android
Dois helpers cooperam: RemoteConfigHelper.kt (telas + versões) e RuntimeConfig.kt (motor de botões).
RemoteConfigHelper.kt
| Método | Papel |
|---|---|
isScreenEnabled(key) | Kill-switch de tela (delega a isFlagEnabled). Fail-safe: source STATIC → true. |
isFlagEnabled(key) | Genérico: qualquer flag booleana (telas e botões). STATIC → true. |
screenMaintenanceMessage(flagKey) | Mensagem de manutenção (troca _enabled→_maintenance). STATIC → "". |
startRealtimeUpdates(onUpdate) | Abre o canal Realtime Remote Config; ao mudar uma flag, faz activate() e chama onUpdate na main thread. Idempotente. |
versionStatus() | Reaproveitado da feature de force-update (MANDATORY/OPTIONAL/UP_TO_DATE) — mesmas chaves update_config. |
RuntimeConfig.kt — motor de botões
Lê assets/app_config.json uma vez e mapeia cada botão (chave screenId/buttonId) para seu feature_flag, denied_message e error_message.
| Método | Papel |
|---|---|
isButtonEnabled(screenId, buttonId) | Resolve a feature_flag do botão e consulta RemoteConfigHelper.isFlagEnabled. Sem flag → habilitado. |
wire(view, …, action) / safeClick | Registra o botão num WeakHashMap (não vaza) e aplica o estado atual; envolve o clique em try/catch com error_message. |
reapply() | Reaplica todos os botões cabeados quando o Remote Config muda ao vivo — o botão some/reaparece na hora, sem re-navegar. |
RuntimeConfig guarda hiddenByUs e só reexibe botões que ele mesmo escondeu — nunca revela um botão que a permissão de negócio já tinha ocultado.
Consumo no iOS
Um único singleton RemoteConfigService.shared (definido em login/LoginViewController.swift), chamado em ~35 arquivos Swift com as mesmas chaves do catálogo Android.
| Método | Papel |
|---|---|
isScreenEnabled(_ screenName) | Kill-switch de tela via mapa screenFlagKeys. Tela sem flag → true. |
isFlagEnabled(_ key) | Genérico por chave (telas e botões). Source .static → true. |
screenMaintenanceMessage(forScreenName:) | Mensagem de manutenção (maintenanceKey(for:)). .static → "". |
Paridade Android / iOS
STATIC/.static → ligado). A diferença é o como:
| Aspecto | Android | iOS |
|---|---|---|
| Motor de botões | Catálogo app_config.json + RuntimeConfig (wiring genérico, safeClick/wire/reapply) | Chamada flag a flag por ViewController (isFlagEnabled("…")) |
| Kill-switch de tela | isScreenEnabled + NavigationManagerPerms | isScreenEnabled via screenFlagKeys |
| Tempo real | startRealtimeUpdates → reapply() | Realtime por dono (ObjectIdentifier), removido em viewWillDisappear |
| Manutenção | screenMaintenanceMessage(flagKey) | screenMaintenanceMessage(forScreenName:) |
Relação com o Force Update
min_required_version_android, latest_version_android, min_required_version_ios, latest_version_ios, update_message) também são geridas por este painel, no campo update_config, e publicadas via POST /api/publish. Diferente dos kill-switches, elas não são fail-safe: são publicadas exatamente como estão (vazio publica vazio), então os valores no config.json devem espelhar a produção. Ver Atualização do App (Android) e Atualização do App (iOS).