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:
- Visão geral com KPIs e filtros globais (região, período) — Módulos 1 e 5.
- Série temporal + ranking sincronizados ao filtro — visualizações sincronizadas.
- Mapa das unidades e rede de relacionamento entre elas — geoespacial e redes.
- Tabela-mestra que dirige o detalhamento — tabelas no Shiny.
- Login e ações sensíveis validadas/confirmadas — notificações, validação e segurança.
- Identidade visual da marca — identidade visual.
- Arquitetura modular — programação em Shiny, empacotada com golem — desenvolvimento com Golem.
- Docker + ShinyProxy para deploy — Módulos 9 e 10.
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ó emobserve/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:
DTcomserver = TRUEpara 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
bslibebreakpoints; recursos emwww/. (arquitetura responsiva) - Marca:
brand.yml+thematicpara coerência visual. (identidade visual) - Arquitetura: módulos com namespace; comunicação por argumento/retorno;
app_servercurto. (Módulos 7, 8) - Reprodutibilidade:
renv.locktravado; 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:
profvispara perfilar,bindCache()/bindEvent()para caching declarativo,poolpara 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
-
No
app_serverdo projeto, por quemod_filtros_global_serverretorna um reactive enquantomod_kpis_serverrecebe 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
reactivecom 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 oinputde um módulo a partir de outro. Esse desacoplamento é o que mantém os módulos isolados e testáveis, e faz oapp_serverlegível como um mapa do fluxo de dados. -
Por que o
Dockerfiledo projeto parte derocker/geospatialem vez derocker/r-ver?✓ Resposta:Porque o app usa
sfeleaflet, que dependem de bibliotecas de sistema geoespaciais — GDAL, GEOS e PROJ. A imagemrocker/geospatialjá traz essas bibliotecas compiladas e configuradas, evitando ter de instalá-las manualmente viaapt-get(o que é demorado e propenso a erros de versão). Partir derocker/r-verexigiria adicionar todas essas dependências de sistema noDockerfile. 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. -
O
app_serverdo projeto tem só algumas linhas. Isso é bom ou ruim? Explique.✓ Resposta:É bom — é sinal de arquitetura modular saudável. Num app bem projetado, o
app_serverfunciona 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 cadamod_*. Umapp_servercurto 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 (umapp_servergigante com toda a lógica) é justamente o monólito que os módulos vieram eliminar. -
Um revisor nota que uma consulta ao banco no
fct_dados.Rusapaste0("... 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
iddiretamente 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 (?) comparams. -
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õeinput$mapa_marker_click$id. (2) UmobserveEvent(input$mapa_marker_click, ...)grava esse id num estado localreactiveVal(unidade_sel) — o padrão "interação escreve no estado" (Módulos 1–2). (3) OrenderVisNetworkda rede depende deunidade_sel()e, protegido porreq(), recalcula a rede para a unidade clicada — "painel lê do estado". Tudo isso acontece dentro do módulomod_geo_rede(programação em Shiny), que recebeu oestadoglobal (região/período) como argumento e devolve o comportamento encapsulado. Se em vez de recriar quiséssemos ajustar a rede existente, usaríamosvisNetworkProxy(geoespacial e redes). O clique, portanto, atravessa: evento de widget →observeEvent→reactiveVal→reactive/renderdependente — exatamente o grafo reativo da arquitetura reativa.