A Divisão de GIS da nossa cidade precisava de uma forma para os moradores reportarem buracos, vazamentos de água, grafites, descarte ilegal, problemas de código e preocupações similares, e para a equipe trabalhar nesses chamados sem um produto 311 de terceiros. Construímos isso no ArcGIS Enterprise que já utilizamos, e agora publicamos todo o sistema no GitHub, generalizado para que qualquer organização possa implementá-lo.
https://community.esri.com/home/leaving?allowTrusted=1&target=https%3A%2F%2Fgithub.com%2Fbrianmcleer%2Freport-a-concern
Esta postagem é um guia técnico de como ele foi montado e por que algumas escolhas foram feitas. O repositório contém o runbook completo de implantação.
O que vem na caixa
Dois widgets Experience Builder 1.21 (assistente público de envio e gerenciador de tickets da equipe), um esquema de geodatabase empresarial com regras de atributo, um pequeno proxy Flask que fica na frente do serviço de feição público, sete scripts Python que rodam no Agendador de Tarefas do Windows, templates HTML acessíveis para email, definições de trabalhos do Agendador de Tarefas e documentação. Nada no código está vinculado à nossa organização. Nomes de host, endereços de email, nomes de departamentos e IDs de itens vêm de dois arquivos de configuração ignorados pelo git e dos painéis de configurações dos widgets.
O modelo de dados
Tudo está em um único geodatabase empresarial SQL Server. O sistema principal é uma classe de feição pontual, Tickets, com um ID GUID do ticket, um número legível do ticket, categoria como subtipo (13 códigos), um campo texto subcategoria cujo domínio de valores codificados muda conforme o subtipo, status, departamento atribuído, o limite onde o ponto caiu, campos de contato do remetente e duas flags que os scripts verificam: notification_sent e survey_sent. Anexos são habilitados em Tickets para fotos.
Ao redor ficam tabelas relacionadas unidas pelo ID do ticket com classes de relacionamento compostas para que a exclusão do ticket seja em cascata: Ticket_Comments (com uma flag is_public e uma flag email_sent), Ticket_Photos_Meta e Survey_Responses. Três tabelas de consulta dirigem o roteamento: Service_Boundaries (polígonos com uma flag is_active), Category_Boundary_Lookup (quais categorias são válidas dentro de qual limite, além de uma mensagem redirecionadora para as que não são) e Ticket_Routing (categoria, subcategoria opcional, ID do limite, departamento padrão, email do departamento). Notification_Log é uma tabela auditável somente para acréscimos que todos os scripts e o proxy escrevem.
Tickets, comentários e respostas da pesquisa são versionados e arquivados. As tabelas lookup e Notification_Log não são, propositalmente, o que importa abaixo.
Fluxo da submissão, ponta a ponta
- O morador abre o app público Experience Builder e coloca um marcador. O widget submit consulta Service_Boundaries no cliente para encontrar quais polígonos ativos contêm o ponto, depois consulta Category_Boundary_Lookup para que a lista de categorias mostre apenas o que é válido ali. Um chamado sobre água não pode ser registrado dentro do distrito vizinho de água, e o morador vê as informações desse distrito em vez de um beco sem saída.
- O widget envia um applyEdits para uma URL da mesma origem sob o app, não diretamente para o serviço de feição. O IIS URL Rewrite (nível do site, assim uma republicação do Experience Builder não apaga) encaminha esse caminho para um proxy Flask no localhost via Application Request Routing. Uma segunda regra no nível do site retorna 403 para qualquer POST direto ao FeatureServer vindo de fora.
- O proxy limita a taxa por IP do cliente, rejeita mais de uma feição por requisição, verifica a geometria contra uma caixa delimitadora, valida categoria, comprimento da descrição, formato do email e telefone e só então encaminha ao FeatureServer no localhost. Para fotos ele lê os primeiros bytes e aceita apenas conteúdo real JPEG, PNG, WebP e HEIC independentemente do tipo declarado, com limite de tamanho. Cada requisição aceita ou rejeitada é escrita em Notification_Log com o IP do cliente, assim temos uma trilha auditável sem nenhum registro em arquivo.
- Cinco regras Arcade rodam na inserção no geodatabase: uma restrição geofence (bloqueia envios fora dos limites ou categoria inválida com mensagem da lookup), um cálculo de roteamento que encontra o limite, tenta combinar categoria mais subcategoria mais limite em Ticket_Routing e recua para a linha catch all da categoria para definir o departamento atribuído; um cálculo do número do ticket puxando uma sequência SQL começando em 10000; e duas restrições validadoras para campos obrigatórios e regras comerciais. Roteamento é dado, não código: adicionar um departamento ou mudar quem recebe tickets d'água em um distrito é editar uma linha.
- A cada cinco minutos o script notificador encontra tickets com notification_sent = 0, envia ao morador um email confirmando com link para status e envia ao departamento roteado um aviso com link direto ao app gerenciador.
Lado da equipe
O widget gerenciador roda num app interno Experience Builder atrás do login Portal. Ele lê a camada Tickets e as tabelas relacionadas do mapa web, então nenhum token extra ou URLs dos serviços são configurados. A equipe filtra por status, categoria e crachás do departamento; abre um ticket; muda status (mudança requer comentário; retroceder data resolvida escreve nota interna); adiciona comentários públicos ou internos; vê fotos numa lightbox; vê resposta da pesquisa se houver. Comentários públicos disparam email ao morador via script mailer dos comentários. Há exportação Excel com resumo e registros relacionados. Um guia ajuda está embutido no widget seguindo nosso padrão em todos os widgets.
Os scripts e duas coisas que erramos na primeira vez
Sete scripts compartilham um módulo comum para configuração, logging, alertas de falha, email, renderização de template e gravação auditável; assim cada script tem só sua lógica: notificador novo ticket; notificador reassignment (detecta mudança assigned_to e envia email ao novo departamento); mailer comentários públicos; mailer convite pesquisa (dispara quando ticket chega a Resolved ou Closed); pull resposta Survey123 (grava em Survey_Responses e envia email ao resolvedor se morador pediu retorno); relatório diretoria dias úteis dos tickets atrasados por categoria; relatório mensal todos departamentos. Cada falha numa execução é coletada e enviada num único email alerta ao sair; processo sai com código 1 para Task Scheduler mostrar erro.
Duas lições estão no código. Primeiro: emails duplicados. O notificador original enviava emails dentro da sessão batch wide edit e só confirmava a flag no fim. Se essa confirmação falhasse (um funcionário tinha o ticket aberto no gerenciador causando conflito na versão), todos os emails já tinham sido enviados e as flags revertidas; então a próxima execução enviava tudo novamente. Agora a flag é confirmada por ticket numa operação curta antes do envio do email. Se falhar confirmação não envia email; tenta novamente na próxima execução seguro. Se confirmar mas enviar falhar é logado visível mas nunca reenviado.
Segundo: tabela auditável. Notification_Log começou versionada; scripts escrevendo via cursores eram lentos e dependiam do Compress. Agora está desregistrada da versionagem; cada escritor insere via SQL direto com OBJECTID igual MAX+1 com pequena tentativa repetida; assim nenhuma escrita depende do contador ID da linha no geodatabase nem dois scripts colidem. Relacionado: mailer pesquisa lia tabela base sem ver resolução até próximo Compress; pesquisas saíam atrasadas um ciclo completo. Agora todo script lê pela feature class versionada.
Loop da pesquisa
Quando ticket é resolvido morador recebe link Survey123 com ID do ticket na URL. Script pull lê novas respostas do ArcGIS Online; grava em Survey_Responses no geodatabase; se morador pediu retorno liga staff que resolveu (resolvido via editor tracking com fallback departamento). Widget gerenciador mostra avaliação e comentários no ticket.
Segurança e privacidade
Usuários anônimos têm leitura na visão pública e podem criar apenas via proxy. Cabeçalhos IIS nível site definem HSTS, nosniff e frame options. Serviço feature restringe tipos/tamanho upload arquivos. Proxy nunca confia no tipo declarado do conteúdo. Retenção é feita por tabelas resumo que mantêm contagens por mês, categoria e departamento sem IDs ou texto livre; assim tickets antigos com dados pessoais podem ser apagados programadamente enquanto estatísticas permanecem.
Implementando
O runbook em docs/deployment.md é lista ordenada: construir esquema com script arcpy (teste seco primeiro); publicar três serviços (escrita pública, leitura pública, equipe); adicionar regras atributo; colocar dois widgets em your-extensions e construir apps; colocar regras IIS nível site; subir proxy num ambiente Python clonado ArcGIS Pro; preencher config.py e rac_secrets.py; importar trabalhos Agendador Tarefas Windows. Cada script vem modo teste ligado; nada envia email real até desligar isso. Doc troubleshooting é tabela sintoma-causa-solução feita durante migração entre servidores: erro IIS HTML 500 significa proxy não escuta; 404 significa ARR não instalado; falha CORS significa URL widget não mesma origem da página; erro Task Scheduler Windows Server 2025 significa job aponta Python base ArcGIS Pro em vez clone.
Zips dos widgets estão anexados a cada release GitHub. Issues e pull requests são bem-vindos no repositório.