Todo o curso convergiu para este ponto. Você aprendeu reatividade, visualizações sincronizadas, mapas, redes, tabelas, validação, segurança, identidade visual, módulos, empacotamento, conteinerização e deploy — cada peça isolada. Um profissional, porém, não é medido por saber as peças, mas por integrá-las num sistema coeso. Esta aula final constrói um dashboard completo do zero à produção, mostrando como as decisões de cada módulo se encaixam num único projeto real.

Projetar e implementar um dashboard de produção de ponta a ponta — o "Painel de Operações Regionais" — integrando visualizações sincronizadas, mapa e rede, tabelas avançadas, validação/segurança, identidade visual e arquitetura modular; e empacotá-lo com golem, conteinerizá-lo com Docker e prepará-lo para ShinyProxy.

O projeto: Painel de Operações Regionais

Vamos construir um dashboard para uma empresa com operações em várias regiões. Requisitos, mapeados a cada módulo do curso:

A arquitetura: um mapa de módulos

Antes de uma linha de código, o desenho. O app se decompõe em módulos com responsabilidade única, e o estado global flui entre eles pelo contrato da programação em Shiny (entrada por argumento, saída por retorno):

app_server
│
├── mod_filtros_global  ──▶ retorna reactive: estado (região, período)
│                                    │
│         ┌──────────────────────────┼──────────────────────────┐
│         ▼                          ▼                          ▼
├── mod_kpis(estado)      ├── mod_serie_ranking(estado)   ├── mod_geo_rede(estado)
│                                                                │
└── mod_tabela_mestra(estado) ──▶ retorna reactive: linha_selecionada
                                              │
                                              ▼
                                     mod_detalhe(linha_selecionada)

O princípio organizador é o mesmo desde a visualizações sincronizadas: as interações escrevem num estado central; os painéis leem desse estado. Cada módulo é uma caixa isolada, testável, que só conhece seus argumentos de entrada e o que retorna.

Estrutura golem do projeto

painelOperacoes/
├── DESCRIPTION            # deps: shiny, bslib, dplyr, leaflet, sf,
│                          #       visNetwork, DT, plotly, shinyvalidate...
├── R/
│   ├── app_ui.R           # compõe os módulos com bslib
│   ├── app_server.R       # amarra os módulos (o "maestro")
│   ├── run_app.R
│   ├── mod_filtros_global.R
│   ├── mod_kpis.R
│   ├── mod_serie_ranking.R
│   ├── mod_geo_rede.R
│   ├── mod_tabela_mestra.R
│   ├── mod_detalhe.R
│   ├── fct_dados.R        # funções de carga/consulta (parametrizadas!)
│   └── utils_ui.R         # helpers de UI (badges, formatação)
├── inst/
│   ├── app/www/
│   │   ├── _brand.yml     # identidade visual
│   │   └── custom.scss
│   └── golem-config.yml   # config por ambiente
├── tests/testthat/        # testes de módulos e funções
├── renv.lock              # versões travadas
├── Dockerfile             # conteinerização
└── application.yml        # exemplo de config ShinyProxy

O maestro: app_ui e app_server

O app_ui monta a casca visual com bslib e a marca; o app_server conecta os módulos. Repare como ambos são curtos — toda a lógica está nos módulos.

# R/app_ui.R
app_ui <- function(request) {
  tagList(
    golem_add_external_resources(),
    bslib::page_navbar(
      title = "Painel de Operações Regionais",
      theme = bslib::bs_theme(brand = app_sys("app/www/_brand.yml")),

      bslib::nav_panel("Visão Geral",
        mod_filtros_global_ui("filtros"),       # filtros globais
        mod_kpis_ui("kpis"),                     # linha de value_boxes
        bslib::layout_columns(
          col_widths = c(8, 4),
          mod_serie_ranking_ui("serie_rank"),    # gráficos sincronizados
          mod_geo_rede_ui("geo_rede")            # mapa + rede
        )
      ),
      bslib::nav_panel("Detalhamento",
        mod_tabela_mestra_ui("tabela"),          # tabela-mestra
        mod_detalhe_ui("detalhe")                # detalhe da linha selecionada
      ),
      bslib::nav_spacer(),
      bslib::nav_item(bslib::input_dark_mode())  # claro/escuro
    )
  )
}

# R/app_server.R
app_server <- function(input, output, session) {
  # 1) Filtros globais RETORNAM o estado (região, período)
  estado <- mod_filtros_global_server("filtros")

  # 2) Painéis RECEBEM o estado como entrada
  mod_kpis_server("kpis", estado = estado)
  mod_serie_ranking_server("serie_rank", estado = estado)
  mod_geo_rede_server("geo_rede", estado = estado)

  # 3) A tabela-mestra RETORNA a linha selecionada...
  linha_sel <- mod_tabela_mestra_server("tabela", estado = estado)
  # 4) ...que alimenta o módulo de detalhe
  mod_detalhe_server("detalhe", linha = linha_sel)
}

Um app_server legível como este — praticamente um índice do app — é o sinal de uma boa arquitetura modular. Quem lê entende o fluxo de dados sem abrir um único módulo.

Módulo de filtros: a fonte do estado

# R/mod_filtros_global.R
mod_filtros_global_ui <- function(id) {
  ns <- NS(id)
  bslib::card(
    bslib::card_body(
      class = "d-flex flex-row gap-3",
      selectInput(ns("regiao"), "Região", c("Todas", "Norte", "Sul", "Leste")),
      sliderInput(ns("periodo"), "Período", 2020, 2024, c(2022, 2024), sep = "")
    )
  )
}

mod_filtros_global_server <- function(id) {
  moduleServer(id, function(input, output, session) {
    # Retorna um reactive com o estado — a ENTRADA de todos os outros módulos
    reactive({
      list(regiao = input$regiao, periodo = input$periodo)
    })
  })
}

Módulo geo+rede: proxies e o padrão do geoespacial e redes

# R/mod_geo_rede.R (trecho do server)
mod_geo_rede_server <- function(id, estado) {
  moduleServer(id, function(input, output, session) {

    # Dados derivados do estado (compartilhado, cacheado)
    dados <- reactive({
      req(estado())
      unidades |> filtrar_por(estado()$regiao, estado()$periodo)
    })

    # Mapa base renderizado UMA vez
    output$mapa <- leaflet::renderLeaflet({
      leaflet::leaflet() |>
        leaflet::addProviderTiles(leaflet::providers$CartoDB.Positron) |>
        leaflet::setView(-51, -14, zoom = 4)
    })

    # Atualização via PROXY — sem recriar o mapa
    observe({
      leaflet::leafletProxy("mapa", data = dados()) |>
        leaflet::clearMarkers() |>
        leaflet::addCircleMarkers(layerId = ~id, radius = ~sqrt(volume),
                                  label = ~nome)
    })

    # Clique no mapa vira estado local que a rede lê (padrão )
    unidade_sel <- reactiveVal(NULL)
    observeEvent(input$mapa_marker_click, {
      unidade_sel(input$mapa_marker_click$id)
    })

    output$rede <- visNetwork::renderVisNetwork({
      req(unidade_sel())
      construir_rede(unidade_sel())
    })
  })
}

Segurança e ações sensíveis (notificações, validação e segurança)

A autenticação fica na plataforma (ShinyProxy, implantação com ShinyProxy) — o app nem precisa de tela de login. Dentro do app, ações sensíveis seguem o esqueleto validar→confirmar→executar→feedback, e toda consulta é parametrizada:

# R/fct_dados.R — consulta SEMPRE parametrizada
buscar_unidade <- function(con, id) {
  DBI::dbGetQuery(con, "SELECT * FROM unidades WHERE id = ?", params = list(id))
}

# Numa ação de exportação sensível dentro de um módulo:
observeEvent(input$exportar, {
  showModal(modalDialog(
    "Exportar dados sensíveis desta região?",
    footer = tagList(modalButton("Cancelar"),
                     actionButton(session$ns("ok_exp"), "Exportar",
                                  class = "btn-danger"))
  ))
})
observeEvent(input$ok_exp, {
  removeModal()
  id <- showNotification("Exportando...", duration = NULL)
  on.exit(removeNotification(id), add = TRUE)
  exportar(dados())
  showNotification("Exportação concluída.", id = id, type = "message")
})

Dockerfile e ShinyProxy do projeto

O Dockerfile é o do implantação com Docker, apontando para este pacote; note a base geoespacial por causa do sf/leaflet:

FROM rocker/geospatial:4.4.1          # já traz GDAL/GEOS/PROJ para sf/leaflet
WORKDIR /app
COPY renv.lock renv.lock
RUN R -e "install.packages('renv'); renv::restore()"
COPY . .
RUN R -e "remotes::install_local('.', upgrade = 'never')"
EXPOSE 3838
CMD ["R", "-e", "options(shiny.host='0.0.0.0', shiny.port=3838); painelOperacoes::run_app()"]

E o specs do ShinyProxy (implantação com ShinyProxy) que serve este app com autenticação e autorização por grupo:

  specs:
    - id: painel-operacoes
      display-name: Painel de Operações Regionais
      container-image: painel-operacoes:1.0
      container-env:
        GOLEM_CONFIG_ACTIVE: production
      access-groups: [operacoes, diretoria]

Checklist de produção

Antes de considerar um dashboard "pronto para produção", verifique — este checklist resume o curso inteiro:

  • Reatividade: trabalho caro em reactive() compartilhado; efeitos só em observe/observeEvent; req() nas pré-condições. (arquitetura reativa)
  • Performance visual: todos os widgets atualizados por proxy (leafletProxy, plotlyProxy, dataTableProxy), nunca recriados. (Módulos 1–3)
  • Tabelas: DT com server = TRUE para grandes volumes. (tabelas no Shiny)
  • Robustez: entradas validadas (shinyvalidate), ações sensíveis confirmadas por modal. (notificações, validação e segurança)
  • Segurança: segredos em variáveis de ambiente; queries parametrizadas; conteúdo de usuário escapado; auth na plataforma. (Módulos 4, 9, 10)
  • Responsividade: layout com bslib e breakpoints; recursos em www/. (arquitetura responsiva)
  • Marca: brand.yml + thematic para coerência visual. (identidade visual)
  • Arquitetura: módulos com namespace; comunicação por argumento/retorno; app_server curto. (Módulos 7, 8)
  • Reprodutibilidade: renv.lock travado; imagem Docker com base adequada. (implantação com Docker)
  • Escala: ShinyProxy (contêiner por sessão) com autenticação e access-groups. (implantação com ShinyProxy)
  • Qualidade: testes de módulos (testServer) e funções (testthat); devtools::check() limpo. (desenvolvimento com Golem)

Para onde ir a partir daqui

Você terminou a trilha, mas o campo é vasto. Direções naturais de aprofundamento:

  • Testes de UI ponta a ponta com shinytest2 (grava e replaya interações reais no navegador).
  • Observabilidade em produção: logging estruturado, métricas de uso, shiny.telemetry.
  • Performance profunda: profvis para perfilar, bindCache()/bindEvent() para caching declarativo, pool para conexões de banco.
  • CI/CD: automatizar check + build da imagem + deploy com GitHub Actions.
  • Kubernetes: operar o ShinyProxy em cluster para alta disponibilidade real.
  • Shiny para Python em profundidade, se seu pipeline for Python (Shiny para Python).

Um dashboard de produção integra as peças do curso num sistema coeso, em vez de tratá-las isoladamente. A arquitetura nasce de um mapa de módulos com responsabilidade única, e o estado central flui entre eles por entrada como argumento e saída como retorno. O app_ui e o app_server fazem o papel de maestro: ficam curtos e legíveis, apenas compondo e conectando módulos — e essa brevidade é o próprio sinal de que a arquitetura está certa. Cada módulo carrega o padrão que lhe cabe: filtros retornam estado, geo e rede trabalham por proxies, a tabela-mestra dirige o detalhamento, e as ações sensíveis seguem validar, confirmar, executar e dar feedback. O deploy reaproveita exatamente a mesma imagem golem com Docker — base rocker/geospatial por causa do sf — servida por ShinyProxy com autenticação e autorização. O checklist de produção condensa o curso inteiro: reatividade correta, proxies, server = TRUE, validação e segurança, responsividade, marca, modularidade, renv, ShinyProxy e testes. E o que vem depois, se você quiser seguir: shinytest2, observabilidade, caching com pool, CI/CD e Kubernetes.

Exercícios

  1. No app_server do projeto, por que mod_filtros_global_server retorna um reactive enquanto mod_kpis_server recebe um? Que princípio isso ilustra?

    ✓ Resposta:

    Porque os filtros são a fonte do estado global (região, período) e precisam disponibilizá-lo aos demais módulos — daí retornam um reactive com esse estado. O módulo de KPIs é consumidor desse estado — daí o recebe como argumento (estado = estado). Isso ilustra o contrato de comunicação entre módulos da programação em Shiny: entrada por argumento, saída por retorno, nunca acessando o input de um módulo a partir de outro. Esse desacoplamento é o que mantém os módulos isolados e testáveis, e faz o app_server legível como um mapa do fluxo de dados.

  2. Por que o Dockerfile do projeto parte de rocker/geospatial em vez de rocker/r-ver?

    ✓ Resposta:

    Porque o app usa sf e leaflet, que dependem de bibliotecas de sistema geoespaciais — GDAL, GEOS e PROJ. A imagem rocker/geospatial já traz essas bibliotecas compiladas e configuradas, evitando ter de instalá-las manualmente via apt-get (o que é demorado e propenso a erros de versão). Partir de rocker/r-ver exigiria adicionar todas essas dependências de sistema no Dockerfile. Escolher a imagem base pela pilha de dependências do app — como visto no implantação com Docker — economiza tempo de build e reduz risco.

  3. O app_server do projeto tem só algumas linhas. Isso é bom ou ruim? Explique.

    ✓ Resposta:

    É bom — é sinal de arquitetura modular saudável. Num app bem projetado, o app_server funciona como um maestro: sua única responsabilidade é compor e conectar os módulos (passar o estado de um como entrada de outro), enquanto toda a lógica real vive encapsulada dentro de cada mod_*. Um app_server curto e legível — quase um índice do fluxo de dados do app — permite entender o sistema inteiro de relance e facilita manutenção. O oposto (um app_server gigante com toda a lógica) é justamente o monólito que os módulos vieram eliminar.

  4. Um revisor nota que uma consulta ao banco no fct_dados.R usa paste0("... WHERE id = ", id). Qual o risco e como o projeto o evita?

    ✓ Resposta:

    O risco é SQL injection (notificações, validação e segurança): concatenar o id diretamente na string permite que um valor malicioso altere ou destrua a consulta. O projeto evita isso parametrizando todas as consultas — DBI::dbGetQuery(con, "SELECT * FROM unidades WHERE id = ?", params = list(id)) —, fazendo o driver tratar o valor como dado, não como parte do comando SQL. É uma regra absoluta do curso: nenhuma query com entrada externa é montada por concatenação; sempre placeholders (?) com params.

  5. Percorra mentalmente o caminho de um clique do usuário no mapa até a atualização do painel de rede, nomeando os mecanismos de cada módulo envolvidos.

    ✓ Resposta:

    (1) O usuário clica num marcador do mapa; como os marcadores têm layerId = ~id (geoespacial e redes), o Shiny expõe input$mapa_marker_click$id. (2) Um observeEvent(input$mapa_marker_click, ...) grava esse id num estado local reactiveVal (unidade_sel) — o padrão "interação escreve no estado" (Módulos 1–2). (3) O renderVisNetwork da rede depende de unidade_sel() e, protegido por req(), recalcula a rede para a unidade clicada — "painel lê do estado". Tudo isso acontece dentro do módulo mod_geo_rede (programação em Shiny), que recebeu o estado global (região/período) como argumento e devolve o comportamento encapsulado. Se em vez de recriar quiséssemos ajustar a rede existente, usaríamos visNetworkProxy (geoespacial e redes). O clique, portanto, atravessa: evento de widget → observeEvent → reactiveVal → reactive/render dependente — exatamente o grafo reativo da arquitetura reativa.

Fontes e leituras recomendadas