API v1

Painel de Controle Remoto / ConfigEngine

Backend: config_admin (Go + Fiber) Android: RemoteConfigHelper · RuntimeConfig iOS: RemoteConfigService.shared

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.

Papéis: o 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.
Regra fail-safe (kill-switch): chave ausente ou com valor 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

1

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.

2

Salvar (persistir)

Salvar alteraçõesPUT /api/config (Bearer ADMIN_TOKEN). O backend grava o config.json com escrita atômica (arquivo .tmp + rename) e carimba updated_at/updated_by.

3

Publicar no Firebase

Publicar no FirebasePOST /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.

4

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": ... }.

GET /api/config
Público. Lê o config atual (painel e, no futuro, os apps). Retorna { "data": FlagConfig }.
PUT /api/config
ProtegidoAuthorization: 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.
POST /api/publish
Protegido — Bearer. Empurra o config salvo para o Firebase Remote Config. Requer FIREBASE_SA_PATH + FIREBASE_PROJECT_ID. Retorna { "data": { "published": true, "version": "N" } }.
GET /healthz
Público. Healthcheck → { "data": { "status": "ok" } }.
GET /
Serve o painel web estático (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"`
}
CampoTipoFail-safe?Papel
screen_flags{chave:bool}Sim (ausente/true = ligado)Kill-switch de tela inteira (~12 chaves)
button_flags{chave:bool}SimKill-switch por botão (~90 chaves)
screen_messages{chave:string}Vazio = sem manutençãoMensagem de manutenção da tela (não esconde)
update_config{chave:string}Não (vazio publica vazio)Chaves de force-update lidas no login
Chaves com underscore: o Firebase Remote Config não aceita . 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:

ModoCampoChaveEfeito no app
Kill-switchscreen_flags / button_flagsscreen_map_enabled = falseEsconde a tela/botão (fica indisponível)
Manutençãoscreen_messagesscreen_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):

1

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.

2

GET do template

GET .../v1/projects/{id}/remoteConfig — captura o ETag e os parâmetros existentes (preserva o que não é gerido pelo painel).

3

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.

4

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ávelDefaultDescrição
ADMIN_TOKENdev-token (com warning)Token exigido em PUT /api/config e POST /api/publish (Authorization: Bearer).
FIREBASE_SA_PATHCaminho do JSON da service account (escopo firebase.remoteconfig). Necessário para publicar.
FIREBASE_PROJECT_IDproject_id da SA / app-monisatId do projeto Firebase de destino.
PORT8090Porta HTTP do painel.
CONFIG_PATHconfig.jsonArquivo 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étodoPapel
isScreenEnabled(key)Kill-switch de tela (delega a isFlagEnabled). Fail-safe: source STATICtrue.
isFlagEnabled(key)Genérico: qualquer flag booleana (telas e botões). STATICtrue.
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

assets/app_config.json uma vez e mapeia cada botão (chave screenId/buttonId) para seu feature_flag, denied_message e error_message.

MétodoPapel
isButtonEnabled(screenId, buttonId)Resolve a feature_flag do botão e consulta RemoteConfigHelper.isFlagEnabled. Sem flag → habilitado.
wire(view, …, action) / safeClickRegistra 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.
Só mexe no que escondeu: o 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étodoPapel
isScreenEnabled(_ screenName)Kill-switch de tela via mapa screenFlagKeys. Tela sem flag → true.
isFlagEnabled(_ key)Genérico por chave (telas e botões). Source .statictrue.
screenMaintenanceMessage(forScreenName:)Mensagem de manutenção (maintenanceKey(for:)). .static"".

Paridade Android / iOS

Capacidade equivalente, wiring diferente: ambas as plataformas leem as mesmas chaves do Firebase Remote Config e têm fail-safe idêntico (STATIC/.static → ligado). A diferença é o como:
AspectoAndroidiOS
Motor de botõesCatálogo app_config.json + RuntimeConfig (wiring genérico, safeClick/wire/reapply)Chamada flag a flag por ViewController (isFlagEnabled("…"))
Kill-switch de telaisScreenEnabled + NavigationManagerPermsisScreenEnabled via screenFlagKeys
Tempo realstartRealtimeUpdatesreapply()Realtime por dono (ObjectIdentifier), removido em viewWillDisappear
ManutençãoscreenMaintenanceMessage(flagKey)screenMaintenanceMessage(forScreenName:)

Relação com o Force Update

As chaves de atualização obrigatória/opcional (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).