Releases: adrianfiio/ixcsoft-mapa
Release list
v0.76.0
AFService Map v0.76.0 — Correção de arquitetura do financeiro
Backup/ponto de rollback: tag v0.75.1. Trabalho feito diretamente por
mim (Claude), a partir da correção explícita mandada pelo usuário:
"Os clientes (Provedores ISP e Projetistas) NÃO criam clientes e NÃO
gerenciam outros usuários em seus painéis. Eles são os meus clientes
finais. Quem cria e gerencia tudo sou eu, no painel Superadmin."
Com migration (remove o model Customer por completo).
O erro de modelo, corrigido
Desde a v0.74.0 eu tinha construído o financeiro assumindo que cada
Provedor ISP cobraria os próprios assinantes de internet — por
isso a v0.75.1 chegou a ligar o financeiro aos clientes já sincronizados
do IXCSoft (IXCCustomer). Estava errado: quem é cobrado nesta
plataforma é a empresa cliente da AFService (Provedor ISP ou
Projetista) — a mensalidade é pela licença/uso do sistema, não pela
internet que o provedor vende aos assinantes dele. Isso nunca teve
nenhum dado real (a funcionalidade tinha dias, e todos os prints
mostravam "nenhum cliente cadastrado"), então a correção pôde remover o
model antigo sem migração de dado nenhuma.
O que muda
Painel do cliente (Provedor ISP / Projetista) — só consulta
Removido por completo: botão "Novo cliente", cadastro, edição e
detalhe por cliente. O painel agora é só leitura: mensalidade, dia de
vencimento, o que está em aberto, atrasado e pago, histórico de faturas,
exportação em CSV. Continua exigindo CompanyMembership.role == EDIT —
perfil VIEW não acessa, mesma regra restrita de antes (essa parte já
estava certa, não mudou).
Painel Superadmin — único lugar que cadastra e gerencia
Já existia desde a v0.75.0 o cadastro de empresa (Provedor/Projetista +
primeiro usuário responsável) fora do Django Admin — isso continua
igual. O que entrou agora, por empresa
(/painel/plataforma/empresas/<id>/financeiro/):
- Editar assinatura — mensalidade, dia de vencimento, se gera
cobrança automática. - Lançar cobrança manual avulsa — fatura fora do ciclo mensal
automático, pra qualquer cobrança pontual. - Registrar pagamento por fatura em aberto.
- Controle de status: Reativar, Bloquear, Cancelar, Desativar
empresa, Excluir. "Excluir" não apaga nada do banco — desativa a
empresa e cancela a assinatura junto, mesma filosofia não-destrutiva
já usada no resto do sistema (nunca apaga equipamento, cabo, fusão
etc.). Reversível pela mesma tela. - Exportar CSV — por empresa e consolidado de todas as empresas.
"Liberação de confiança"
Botão no painel do cliente (e na tela de bloqueio): concede mais 2
dias de acesso mesmo com a assinatura bloqueada/cancelada. Limitado a
1 uso por mês corrido, validado no servidor
(apps.billing.services.request_trust_release — checa
TrustRelease.objects.filter(company=..., created_at__year=.., created_at__month=..)
antes de conceder, não só desabilita o botão na tela).
Bloqueio de acesso de verdade
Novo middleware apps.billing.middleware.SubscriptionAccessMiddleware:
quando a assinatura de uma empresa está bloqueada ou cancelada e não há
liberação de confiança em vigor, qualquer usuário dessa empresa (exceto
Superadmin, que nunca é afetado) é redirecionado pra uma tela de "conta
bloqueada" em qualquer página que tente acessar — login, logout,
arquivos estáticos, /admin/ e /api/ ficam de fora do bloqueio.
Decisão de segurança mais importante desta rodada: empresa sem
CompanySubscription cadastrada (hoje, a imensa maioria — o recurso é
nascendo agora) nunca é bloqueada. O bloqueio só passa a valer
quando o Superadmin bloqueia/cancela explicitamente pela tela de
financeiro daquela empresa. Testei essa regra como função pura antes de
sequer escrever o middleware (ver seção de verificação) — não queria
correr o risco de travar todo mundo fora do sistema por um bug de dia
1.
O que ficou de fora, de propósito
- Chamada real a gateway de pagamento — só a tela de credencial
(já existia desde a v0.75.0), sem integração de verdade. - Cobrança automática puxando o gateway quando a fatura vence — a
geração de fatura recorrente continua automática (Celery Beat), mas
"cobrar" ainda é manual (Superadmin registra o pagamento recebido).
Verificação feita
python -m py_compileem todos os arquivos tocados/novos.is_access_blockedehas_trust_release_this_monthsão funções
puras, sem banco — executei de verdade via script Python direto:
6 asserções prais_access_blocked(incluindo o caso crítico "sem
assinatura cadastrada nunca bloqueia", "bloqueada mas com carência no
futuro não bloqueia", "cancelada sem carência bloqueia") e 4 pra
has_trust_release_this_month(incluindo mês/ano diferentes) — todas
passaram.- Migration revisada campo a campo (sem GDAL neste ambiente de
preparação): ordem correta — remove a constraint única, depois o
índice, depois o campocustomerda fatura, só então apaga o model
Customerinteiro. Nomes de constraint/índice conferidos contra a
migration0001_initial.pyque os criou, pra garantir que bate
exatamente com o que existe no banco. - Chaves do
static/css/app.cssbalanceadas (282/282). {% if %}/{% endif %}/{% for %}/{% endfor %}/{% block %}/
{% endblock %}balanceados em todos os templates tocados e novos.git diff --checklimpo.- Grep no projeto inteiro confirmando que nenhuma referência solta a
Customer/billing-customers/billing-customer-*sobrou fora das
migrations antigas (onde é esperado — são registro histórico).
manage.py check, manage.py migrate, manage.py test e o teste
real dependem de banco, GDAL e navegador — não rodam neste ambiente de
preparação.
Atualização
Execute apply no servidor — esta versão tem migration (remove o
model Customer inteiro). Depois, um Ctrl+F5 no navegador.
Teste sugerido: como Superadmin, abrir o financeiro de uma empresa de
teste, lançar uma cobrança manual e registrar o pagamento; bloquear a
assinatura dessa empresa e confirmar (logado como usuário EDIT dela)
que qualquer página redireciona pra "conta bloqueada"; pedir liberação
de confiança e confirmar os +2 dias de acesso; tentar pedir de novo no
mesmo mês e confirmar que é bloqueado; confirmar que um usuário VIEW
continua sem acessar /painel/financeiro/ de forma nenhuma; confirmar
que uma empresa qualquer sem assinatura configurada continua
acessando o sistema normalmente (fail-safe).
v0.75.1
AFService Map v0.75.1 — Hotfix da ACL/painel Superadmin
Backup/ponto de rollback: tag v0.75.0. Trabalho feito diretamente por
mim (Claude), a partir de 3 prints reais mandados pelo usuário logo
depois da v0.75.0: provedor sem opção real de configurar financeiro dos
clientes que já existem, projetista sem o menu Financeiro, e Superadmin
sem menu nenhum na Visão da plataforma. Com migration
(apps.billing.0003_customer_ixc_customer).
1. Financeiro do projetista sumia por completo
Print mostrou o menu lateral do projetista (LC-PROJETOS) com só 4
ícones — Visão geral, Mapa, Equipamentos, Minha administração — sem
Alertas (certo, projetista não tem cliente pra gerar alerta) e sem
Financeiro (errado — a especificação pedia explicitamente que
Projetista Editor visse o financeiro da própria empresa).
Causa: no templates/base.html, coloquei o link de Financeiro dentro
do mesmo {% if not current_company.is_designer %} que já escondia
Alertas — copiei o padrão sem separar os dois. Corrigido: Alertas
continua condicional a not is_designer, Financeiro ficou fora desse
bloqueio.
2. Provedor com 2.965 clientes reais e Financeiro mostrando zero
Print da "Visão da plataforma" mostra "Nic Fibra Telecom" com 2.965
clientes (contagem de IXCCustomer, a tabela de clientes já
sincronizados do IXCSoft) — mas a tela de Financeiro daquela mesma
empresa mostrava "Nenhum cliente cadastrado ainda." e só oferecia
"Novo cliente →" pra digitar um por um.
Causa: apps.billing.Customer (criado na v0.74.0) era um cadastro
100% independente, sem nenhuma ligação com IXCCustomer — fazia
sentido pra empresa sem ERP nenhum, mas deixava uma empresa com ERP (e
milhares de clientes já cadastrados) sem nenhum caminho razoável pra
começar a usar o financeiro.
Corrigido:
Customer.ixc_customer— novo campo (OneToOneFieldpra
IXCCustomer, opcional).- A lista de clientes agora mostra, além dos já configurados, uma seção
"Clientes do ERP sem financeiro configurado" — paginada (25 por
página) e com busca — listando quem já existe noIXCCustomermas
ainda não tem mensalidade/financeiro cadastrado aqui. - Botão "Configurar →" em cada um leva pro formulário de cadastro já
com nome, documento, e-mail e telefone pré-preenchidos a partir do
cadastro do ERP — só falta definir mensalidade/vencimento. - Tentar configurar o mesmo cliente do ERP duas vezes redireciona pro
cadastro já existente, não deixa duplicar.
Empresa sem ERP continua exatamente como antes — cadastro manual
completo, sem seção nova nenhuma (a seção só aparece quando existe
algum IXCCustomer sem financeiro vinculado).
3. Superadmin sem menu nenhum
Print da "Visão da plataforma" mostra só a topbar — nenhuma barra
lateral, nenhum menu. O usuário: "eu não quero ficar acessando django
ali quero meu menu geral".
Causa: PlatformOverviewView (apps/core/views.py) setava
context["hide_sidebar"] = True de propósito, com um comentário meu
de uma rodada anterior dizendo que "superusuário já tem o Django Admin
pra qualquer outra tela" — decisão errada, o usuário não quer depender
do /admin/ pro uso do dia a dia.
Corrigido:
- Removido o
hide_sidebar = True. templates/base.htmlagora mostra um menu dedicado pro
Superadmin (quando logado sem vínculo de empresa nenhum): "Visão da
plataforma" e "Nova empresa" — sem os itens que só fazem sentido
dentro de uma empresa (Mapa, Equipamentos, Alertas, Minha
administração), que continuam aparecendo normalmente pra usuário de
empresa.- A tabela de empresas na própria Visão da plataforma ganhou colunas
"Atrasados"/"Pendentes" por empresa — dá a visão financeira
consolidada pedida ("dash de geral de financeiro") sem precisar de
uma página nova, reaproveitando a mesma tabela que já mostra rede,
alertas e sincronização.
Verificação feita
python -m py_compileem todos os arquivos tocados/novos.- Migration nova revisada campo a campo; dependência de
ixc_integrationajustada pra0008_sync_progress(a mais recente),
não0001_initial, evitando qualquer risco de ordem entre migrations
mesmo só adicionando uma FK pra uma tabela que já existe desde a
0001. - Chaves do
static/css/app.cssbalanceadas (280/280). {% if %}/{% endif %}/{% for %}/{% endfor %}/{% block %}/
{% endblock %}balanceados embase.html,
templates/billing/customer_list.htmle
templates/platform_overview.html— a estrutura aninhada nova do
menu do Superadmin embase.htmlfoi conferida diff a diff.- Revisei que "Configurar →" não permite configurar o mesmo cliente do
ERP duas vezes (usahasattrno acessor reverso do
OneToOneField, que o Django projeta especificamente pra funcionar
comhasattr/getattrnesse caso).
manage.py check, manage.py migrate, manage.py test e o teste
visual real (abrir Financeiro como projetista, configurar um cliente do
ERP como provedor, ver o menu do Superadmin) dependem de banco, GDAL e
navegador reais — precisam ser feitos no servidor.
Atualização
Execute apply no servidor — esta versão tem migration
(apps.billing.0003_customer_ixc_customer), roda automaticamente.
Depois, um Ctrl+F5 no navegador.
Teste sugerido: como usuário Projetista, confirmar que "Financeiro"
aparece no menu; como provedor com ERP, abrir Financeiro e confirmar
que a seção de clientes do ERP sem configuração aparece e que
"Configurar →" pré-preenche os dados; como Superadmin, confirmar que a
Visão da plataforma mostra o menu lateral e as colunas
Atrasados/Pendentes na tabela de empresas.
v0.75.0
AFService Map v0.75.0 — ACL do financeiro e painel Superadmin
Backup/ponto de rollback: tag v0.74.0. Trabalho feito diretamente por
mim (Claude), a partir de uma especificação de perfis de acesso e telas
de Superadmin que o usuário mandou. Com migration
(apps.billing.0002_gateway_configuration).
O que já estava pronto (verificado antes de escrever qualquer código)
Antes de implementar, conferi apps/billing/views.py (da v0.74.0) contra
a especificação recebida. A regra de acesso pros perfis de empresa já
estava completa:
_require_billing_company/_editable_companyjá restringiam o
financeiro aCompanyMembership.role == EDIT— perfil VIEW já era
bloqueado por completo, redirecionado com mensagem, sem exceção.- Essa regra já não dependia do tipo da empresa (Provedor/Projetista) —
um EDIT de qualquer tipo vê o próprio financeiro, um VIEW de qualquer
tipo não vê nenhum. Não criei papéis novos ("ISP Editor", "Projetista
View" etc.) porque oCompanyMembership.Rolegenérico já combinado
comCompany.company_typeentrega exatamente essa regra sem duplicar
enum.
O que realmente faltava, e é o conteúdo desta versão:
1. Superadmin acessa o financeiro de qualquer empresa
_require_billing_company (renomeada pra _require_list_company/
_customer_for_request) antes bloqueava superusuário por completo,
mandando "acessar como usuário da empresa" — o que não fazia sentido
pra Superadmin, que não tem (nem deveria ter) vínculo de equipe em
nenhuma empresa.
Agora:
- Lista e criação de cliente: superusuário resolve a empresa via
?empresa=<id>na query string — vindo do link "Financeiro →" que
cada linha da tabela de empresas ganhou na Visão da plataforma. - Detalhe, edição e pagamento (rotas que já têm um
pkde cliente):
superusuário não precisa nem do parâmetro — o cliente é buscado pelo
pke a empresa dele é lida diretamente do próprio registro. - Usuário comum nunca tem
?empresa=respeitado — a resolução dele
continua 100% baseada no próprio vínculoCompanyMembership, a query
string é ignorada em todo o resto do módulo. Não abre brecha de
autorização entre empresas. - Cliente de outra empresa continua dando 404 pra usuário comum (nunca
200 com "acesso negado") — não revela que o registro existe em outro
lugar, mesmo padrão de segurança já usado no resto do sistema.
2. Cadastro de empresa pelo Superadmin, fora do Django Admin
Busquei em todo apps/core por Company.objects.create antes de
escrever qualquer coisa: não existia nenhum fluxo em-app pra criar
uma empresa do zero. company_onboarding (o único fluxo parecido) exige
que a CompanyMembership já exista — é o próprio usuário da empresa
preenchendo os dados dela depois de já ter sido criada (hoje, só via
/admin/).
Tela nova em painel/plataforma/empresas/novo/
(SuperadminCompanyForm, apps/core/forms.py): cria a empresa e o
primeiro usuário responsável numa única transação —
- Dados da empresa (nome, documento, contato, endereço) — mesmos campos
deCompanyOnboardingForm. - Tipo (Provedor/Projetista) e modo de operação (ERP/manual)
definidos na hora, não depois. - Usuário responsável (nome, usuário de acesso, senha — mesma validação
de força de senha e unicidade deCompanyTeamMemberForm) com nível
de acesso (VIEW/EDIT) escolhido já no cadastro. sluggerado automaticamente a partir do nome (antes só dava pra
preencher à mão no admin).
3. Configuração de gateway de pagamento centralizada
Novo model CompanyPaymentGatewayConfiguration
(apps/billing/models.py): uma credencial por empresa, provedor
(nenhum/Efí/Inter/Mercado Pago — reaproveita o enum GatewayProvider já
existente desde a v0.74.0), modo sandbox, ativo/inativo.
A credencial em si (client_id/client_secret/access_token — três
campos genéricos, cobrem a maioria dos gateways) é serializada em JSON e
criptografada com SecretCipher antes de ir pro banco — mesmo padrão já
usado pra senha de SMTP em CompanyEmailConfiguration.password_encrypted.
Editar a credencial sem preencher um campo mantém o valor já salvo (não
sobrescreve com vazio), mesmo comportamento do formulário de e-mail.
Tela nova em painel/plataforma/empresas/<id>/gateway/, só Superadmin,
com link "Gateway →" ao lado de "Financeiro →" na tabela de empresas.
Nenhuma chamada real a gateway acontece a partir daqui — isso é só
armazenamento e gestão de credencial. A integração de verdade (gerar
boleto/PIX cobrável, confirmar pagamento) continua fora de escopo, mesma
decisão já tomada na v0.74.0 pros campos gateway_provider/
gateway_charge_id da fatura — evita ter que redesenhar o schema de
novo quando isso entrar.
Verificação feita
python -m py_compileem todos os arquivos Python tocados/novos.- Migration escrita à mão (sem GDAL neste ambiente de preparação),
revisada campo a campo contramodels.py. - Geração de slug (
slugify+ sufixo numérico em colisão) — validei a
lógica do laço de sufixo com um script Python direto: nome único,
colisão simples, colisão dupla, nome vazio — 4 asserções, todas
passaram. - Chaves do
static/css/app.cssbalanceadas (278/278). {% if %}/{% endif %}/{% for %}/{% endfor %}/{% block %}/
{% endblock %}balanceados em todos os templates tocados e novos.- Revisão manual de cada view de financeiro confirmando que a resolução
de empresa do usuário comum nunca lê a query string (request.GET) —
só o superusuário tem esse caminho, evitando qualquer brecha de
autorização cruzada entre empresas.
manage.py check, manage.py migrate, manage.py test e o teste
real dependem de banco, GDAL e navegador — não rodam neste ambiente de
preparação.
Atualização
Execute apply no servidor — esta versão tem migration
(apps.billing.0002_gateway_configuration), roda automaticamente.
Depois, um Ctrl+F5 no navegador.
Teste sugerido: como Superadmin, cadastrar uma empresa nova pela Visão
da plataforma definindo tipo e nível de acesso do responsável; logar
como esse usuário e confirmar que o financeiro da própria empresa
aparece; criar um segundo usuário VIEW na mesma empresa e confirmar que
/painel/financeiro/ bloqueia ele; como Superadmin, acessar o
financeiro de qualquer empresa pela Visão da plataforma e salvar uma
credencial de gateway de teste.
v0.74.0
AFService Map v0.74.0 — Módulo financeiro
Backup/ponto de rollback desta rodada: tag v0.73.1. Trabalho feito
diretamente por mim (Claude), a pedido do usuário ("vamos criar 1 base
de financeiro, para controle de pagamento etc dos meus clientes").
Com migration (apps.billing.0001_initial, roda automaticamente no
apply).
Contexto
O sistema não tinha nenhum controle de pagamento — os únicos dados
vindos do IXCSoft ficam soltos em raw_data (JSON), sem estrutura de
fatura, mensalidade ou pagamento em lugar nenhum. Também não existia uma
página de detalhe por cliente — o único lugar onde um cliente aparecia
era como popup no mapa. Como a plataforma atende empresa com ou sem
ERP, o cadastro financeiro do cliente não podia depender do
AccessPoint (que só existe quando há sincronização) — é um cadastro
próprio, com vínculo opcional.
Decisões confirmadas com o usuário antes de implementar
Perguntei diretamente antes de desenhar o banco, pra não ter que
redesenhar depois:
- Controle manual agora, com o schema pronto pra gateway depois
(Efí, Inter, Mercado Pago — escolhido por empresa no futuro). Os
campos existem, mas nenhuma integração real entrou nesta rodada. - Página de detalhe do cliente nova, em vez de um botão no popup do
mapa. - Mensalidade recorrente automática via Celery Beat, em vez de
lançamento manual mês a mês. - Mesmo acesso EDIT de hoje — sem controle de permissão novo.
O que entrou
Cadastro de cliente (apps.billing.Customer)
Nome, documento, e-mail, telefone, endereço, situação
(ativo/inativo/suspenso), mensalidade e dia de vencimento (opcionais —
sem eles, o cliente só recebe lançamento manual), e um vínculo opcional
com um AccessPoint já existente (pra quem já usa mapa/ERP).
Mensalidade recorrente automática
apps.billing.tasks.generate_monthly_invoices, agendada de hora em hora
no CELERY_BEAT_SCHEDULE (config/settings.py): para cada cliente com
mensalidade ativa, cria a fatura (Invoice) do mês corrente se ainda
não existir. Idempotente de verdade — protegida por
UniqueConstraint(customer, reference_month), então rodar de novo (ou
com mais frequência) nunca duplica fatura. O dia de vencimento é
ajustado automaticamente pra sempre existir no mês (dia 31 cadastrado
vira o último dia de fevereiro, por exemplo).
apps.billing.tasks.mark_overdue_invoices, mesmo agendamento: fatura
pendente com vencimento no passado vira "Atrasada" sozinha.
Registro de pagamento
Cada fatura aceita um ou mais pagamentos (Payment) — dinheiro, PIX,
transferência, cartão ou outro, com observação e quem registrou. A
fatura só fecha como "Paga" quando a soma dos pagamentos atinge o valor
devido; pagamento parcial fica registrado sem fechar a fatura.
Página de cliente nova
- Lista (
/painel/financeiro/): clientes da empresa, busca por
nome, resumo (faturas atrasadas, pendentes, pagas no mês). - Detalhe (
/painel/financeiro/<id>/): dados do cliente, histórico
completo de faturas com status, e um formulário de pagamento pra cada
fatura ainda em aberto. - Menu lateral ganhou o item "Financeiro" (mesma regra de acesso de
"Alertas": some para empresa em modo projetista).
Reservado para o futuro, não integrado
Invoice.gateway_provider (nenhum/Efí/Inter/Mercado Pago) e
Invoice.gateway_charge_id existem no schema desde já, mas nenhuma
chamada de API, webhook ou geração de boleto/PIX cobrável de verdade
entrou nesta versão — só evita ter que alterar o banco quando isso for
implementado.
O que ficou de fora, de propósito
- Integração real de gateway de pagamento.
- Botão de financeiro no popup do mapa (o usuário escolheu a página de
detalhe nova). - Visão financeira consolidada entre empresas na "Visão da plataforma"
— fica igual às outras seções, só por empresa.
Verificação feita
clamp_due_day/due_date_for(o cálculo que garante um dia de
vencimento sempre válido no mês, incluindo fevereiro e meses de 30
dias) são funções puras, sem banco — executei de verdade via
script Python direto nesta sessão (mesmo método já usado com
apps/snmp_monitoring/link_status.pyeste ano): 5 asserções, todas
passaram, incluindo o caso de fevereiro bissexto vs. não-bissexto.python -m py_compileem todos os arquivos Python novos
(apps/billing/*) e nos tocados (config/settings.py,
config/urls.py).- Migration escrita à mão, campo a campo, contra
models.py(sem GDAL
neste ambiente de preparação — não dá pra rodarmakemigrations).
Dependências (core.0013_dashboard_widget_layout,
access.0002_add_ftth_fields) conferidas como as últimas migrations
de cada app antes de referenciá-las. - Chaves do
static/css/app.cssbalanceadas (276/276, só 3 classes
novas pequenas — reaproveitei.overview-table,.sync-pill,
.metric-card,.panel,.platform-forme.info-listjá
existentes em vez de criar CSS novo pra tudo). {% if %}/{% endif %}/{% for %}/{% endfor %}/{% block %}/
{% endblock %}balanceados nos 3 templates novos
(templates/billing/) e nobase.htmltocado.- YAML do
docker-compose.ymlválido.
manage.py check, manage.py migrate, manage.py test e o teste
visual real dependem de banco, GDAL e navegador — não rodam neste
ambiente de preparação. Precisam ser feitos no servidor.
Atualização
Execute apply no servidor — esta versão tem migration
(apps.billing.0001_initial), roda automaticamente. Depois, um
Ctrl+F5 no navegador.
Teste sugerido: abrir "Financeiro" no menu, cadastrar um cliente com
mensalidade e dia de vencimento, confirmar que a fatura do mês aparece
(pode rodar generate_monthly_invoices manualmente via
manage.py shell na primeira vez, já que o Celery Beat só passa a rodar
a cada hora a partir do próximo ciclo), registrar um pagamento parcial e
depois o restante, e confirmar que a fatura vira "Paga" só quando o
total bate.
v0.73.1
AFService Map v0.73.1 — Hotfix estrutural do runtime do mapa
Backup/ponto de rollback desta rodada: branch backup/mapa-pre-v0722-20260802
(pointer no commit ef12a9a, v0.73.0). Pacote preparado pelo ChatGPT como
"v0.72.2" sobre o commit 16a125a (v0.72.1) — antes do reskin dos
dashboards (v0.73.0) ter entrado no main. Aplicado via
apply_map_ui_v0722.py (validado antes com --dry-run, checksums
SHA-256 conferidos um a um). Sem migration.
Sobre o número de versão
O pacote chamava a si mesmo de "v0.72.2" porque foi preparado sobre o
commit da v0.72.1, sem saber ainda que a v0.73.0 (reskin dos dashboards)
já tinha entrado no main. Confirmei que os dois pacotes não têm
nenhuma sobreposição de arquivo (este só toca em templates/map.html,
.gitignore e cria os arquivos novos do mapa; o reskin só tocou em
templates/base.html, static/css/app.css e os 3 templates de
dashboard) — mas apliquei renomeando a versão final pra 0.73.1 em vez
de 0.72.2, pra manter o changelog em ordem cronológica real (este pacote
chegou e foi aplicado depois da v0.73.0, mesmo tendo sido preparado
antes).
Causa raiz confirmada lendo o código, não só a explicação do pacote
Antes de aceitar a explicação do ChatGPT, comparei o map-ui-v0721.js
antigo com o map-ui-v0722.js novo pra confirmar que os bugs descritos
realmente existiam:
1. Laço de reconstrução do popup. Em v0.72.1, schedulePopup()
registrava um MutationObserver no conteúdo do popup, além de 3 timers
(80/260/650ms). O observador tinha uma trava de debounce (scheduled)
contra reagir à própria mutação — isso eu já tinha conferido na release
da v0.72.1. O que não tinha percebido: map-ui-v072.js (a base, carregada
antes) também tem seu próprio sistema de decoração de popup
(decoratePopup, registrado em state.map.on("popupopen", ...)) que
mexe no mesmo .leaflet-popup-content. Dois sistemas diferentes
mutando a mesma área, um com observador ativo — o terreno certo pra
reconstrução em cascata, mesmo com debounce individual em cada um.
popupIdentity() também recalculava nome/tipo/código toda vez lendo o
DOM ao vivo, sem cache — se uma leitura acontecesse no meio de uma
mutação (o content.replaceChildren() do próprio código, ou a decoração
concorrente do map-ui-v072.js), o texto capturado podia vir de um
estado intermediário e ser usado como base pra próxima reconstrução.
O hotfix v0.72.2 remove o MutationObserver dos popups por completo.
Restam só 2 checagens finitas depois de abrir o popup (100ms e 420ms —
suficiente pra Master Suite/monitoramento injetarem as ações delas antes
da normalização final) e a identidade é capturada uma única vez e
cacheada em content._uiV0722Identity, nunca mais recalculada por cima
de um DOM que o próprio código já reconstruiu.
2. Botões de ação perdendo a função. Em v0.72.1, actionKey() só
reconhecia um botão pela chave de um atributo de uma lista fixa:
showElementCables, manageContainer, editElement, deleteElement,
masterAssetSheet, showUnifilar, openMonitorLinks. Qualquer botão
com outro atributo (data-insert-cable, data-reserve-cable,
data-cable-route, etc. — usados em Reserva, Inserir caixa, Rota do
cabo) caía no fallback por texto (button.textContent...), e a
deduplicação (if (keys.has(key)) return;) descartava silenciosamente
qualquer botão cujo texto colidisse com outro já visto. Achei essa lista
fixa lendo o código da v0.72.1 — é uma explicação plausível e verificável
pros botões que "paravam de responder" sem gerar nenhum erro no console
(o botão simplesmente não ia parar no popup final).
O hotfix v0.72.2 troca isso por uma chave feita de todos os
atributos data-* do próprio botão (ordenados), então nenhum botão real
se confunde com outro.
O que mais entrou
- Cancelamento de ferramenta de verdade: a barra agora rastreia qual
ferramenta está ativa (state.activeTool), mostra um botão
"Cancelar [ferramenta]" enquanto ela está ativa, cancela comEsc, e
cancela sozinha ao fechar os diálogos de elemento/cabo no meio da
operação (#element-dialog/#cable-dialogclose) — resolve o
"próximo clique preso no mapa" relatado. - Ícones não interceptam mais o clique:
pointer-events: nonenos
ícones/SVGs internos dos botões do popup e da barra. popupcloselimpa os timers pendentes — antes, se o popup fosse
fechado antes dos timers de normalização dispararem, eles rodavam
mesmo assim contra um popup já fechado.- Menu lateral em trilho de 72px, régua que vira cabo, medição de área,
busca comCtrl+K, whitelabel, tema claro/escuro, SNMP/InfluxDB/
Telegraf, backbone, PTP wireless, alertas, fusões, Canvas 2D, matriz de
manobra, KMZ/KML — tudo preservado sem mudança de comportamento.
Verificação feita
- Checksums SHA-256 do pacote conferidos um a um contra
SHA256SUMS. BASE_COMMIT(16a125a, v0.72.1) confirmado batendo com o estado do
repositório antes da v0.73.0 ter entrado.- Confirmado por leitura do
git diffque o pacote não toca em
templates/base.htmlnemstatic/css/app.css(arquivos do reskin
v0.73.0) — as únicas mudanças emtemplates/map.htmlforam as duas
tagsv0721→v0722, exatamente como o script prometia. - Confirmado que
map-optical-editor-v3.js,map-link-monitoring.js,
map-master-suite.jsemap-ui-v072.js/.csscontinuam intactos. - Li o
map-ui-v0722.jsinteiro (não só o resumo do pacote) pra
confirmar a ausência real doMutationObservere o cache de
identidade, e omap-ui-v0722.cssinteiro (pointer-events, seletores
de busca/popup). --dry-rundo aplicador, sem erro.python -m py_compileno Python tocado.- Sintaxe do
map-ui-v0722.jsvalidada com esprima. - Chaves do
map-ui-v0722.cssbalanceadas (74/74). {% if %}/{% endif %}detemplates/map.htmlbalanceados (10/10).docker-compose.ymlvalidado como YAML.- Rodei manualmente as 6 asserções do teste estático incluído
(test_map_ui_v0722_static.py, sem dependência de banco): ausência do
MutationObserver, cache de identidade, rótulos limpos dos botões,
menu Caixas/Cancelar, reset de posição da busca, gaveta de rotas real
— todas passaram.
A suíte Django completa (manage.py check, manage.py test) e o teste
visual real dependem de banco, GDAL e navegador — não rodam neste
ambiente de preparação.
Atualização
Execute apply no servidor. Sem migration nesta versão. Depois, um
Ctrl+F5 no navegador garante que o JS/CSS novos sejam carregados
(o map-ui-v0721.js/.css não é mais referenciado pelo template, só
fica como arquivo morto no disco).
Teste sugerido: abrir um popup de CTO/CEO/CDO/Torre/Rack/CPD-POP/Poste/
cabo e confirmar que o nome/código não crescem mais e que Editar, Fusões,
Equipamentos, Ficha/QR, Excluir, Rota do cabo, Monitorar enlace,
Adicionar reserva e Inserir caixa respondem ao clique; ativar uma
ferramenta (CTO, Cabo, etc.) e confirmar que aparece "Cancelar" e que
Esc cancela; testar a busca (Ctrl+K) sem deslocamento lateral.
v0.73.0
AFService Map v0.73.0 — Reskin visual dos dashboards
Backup/ponto de rollback desta rodada: tag v0.72.1. Trabalho feito
diretamente por mim (Claude), a partir de 3 HTMLs de referência que o
Gemini gerou (Tailwind + FontAwesome via CDN) pro SuperAdmin, Provedor/ISP
e Projetista — usados só como inspiração visual, não copiados como
código. Sem migration, sem mudança em Python.
O que pediu, o que entreguei
O usuário confirmou que queria a linguagem visual dos 3 mockups (fundo
bem escuro, cards com glow, badge de ícone colorido, sidebar retrátil em
trilho de ícones, cabeçalho tipo hero) aplicada nos dashboards reais do
sistema — não os HTMLs em si. O sistema de widgets por trás dessas 3
páginas (grid arrastável/redimensionável via Gridstack só no modo de
edição, dados reais vindo das views) já estava completo e não precisou
mudar; este pacote é puramente visual.
O que decidi não trazer, e por quê
- Tailwind CSS e FontAwesome via CDN: o próprio Tailwind desaconselha
o build via CDN em produção (sem purge, carrega o framework inteiro, e
cria uma dependência de rede externa toda vez que alguém abre o
dashboard). O projeto inteiro é CSS/JS vanilla sem bundler, ícone
inline em SVG — mantive esse padrão, só amplieistatic/css/app.css. - Widgets de faturamento, inadimplência e bloqueio automático que
apareciam nos mockups: não existe módulo financeiro no sistema hoje.
Mostrar isso seria inventar número falso na tela — não fiz. - Busca funcional na topbar: uma busca decorativa sem back-end real
seria enganosa. A busca de verdade já existe em "Minha administração" e
no mapa; não dupliquei.
O que mudou
Menu lateral (templates/base.html, static/css/app.css)
Ao recolher, a sidebar não some mais por completo — vira uma faixa de
72px só com os ícones da navegação (mesmo padrão já usado no mapa desde a
v0.72.1: .app-sidebar.desktop-collapsed agora tem width: 72px em vez
de transform: translateX(-100%)). O texto de cada link foi movido pra
um <span class="nav-label"> novo (antes era texto solto ao lado do
ícone), pra poder esconder só o rótulo no modo recolhido.
Cabeçalho "hero" dos dashboards
Classe nova .page-heading.dashboard-hero, usada só nas 3 páginas de
dashboard (dashboard.html, dashboard_designer.html,
platform_overview.html) — fundo em gradiente com dois halos desfocados
(::before/::after com blur), borda sutil, padding maior. O
.page-heading genérico usado nas outras páginas do sistema (lista de
equipamentos, conta, etc.) não foi tocado.
Acento roxo só na Visão da plataforma
body.platform-role (aplicado só quando a rota é platform-overview,
exclusiva de superusuário) sobrescreve --primary pra roxo fixo — o
dashboard de provedor e de projetista continuam seguindo a cor de
whitelabel de cada empresa (current_company.brand_color), como sempre.
A regra vence o :root{--primary:...} do whitelabel por especificidade
CSS (body.platform-role tem mais peso que :root sozinho), então não
depende de ordem de carregamento.
Selo de percentual no card "Acessos online"
dashboard.html recalcula o mesmo % online que já existia dentro do
painel "Estado dos acessos" ({% widthratio %}) e mostra como pílula
colorida (.metric-badge) ao lado do número — reaproveita dado que já
era calculado, não adiciona cálculo novo no back-end.
Feed de alertas com borda lateral por severidade
.alert-item (painel "Alertas recentes") e as linhas de "Precisa de
atenção" (Visão da plataforma) ganham border-left colorido — vermelho
pra crítico/alto, amarelo pra médio/atenção, azul pra informativo — em
cima do markup que já existia, sem mudar dado nenhum.
Avatar + breadcrumb na topbar
.topbar-user ganha um círculo com a inicial do nome (via filtro Django
padrão |slice:":1"|upper, sem template tag nova) ao lado do nome
completo. Antes do título da página, um pequeno "AFService ›" no mesmo
espírito do breadcrumb dos mockups.
Verificação feita
- Nenhuma mudança em Python além do bump de versão em
config/settings.py
(py_compileOK) — não há migration nem lógica de view nova. - Chaves do
static/css/app.cssbalanceadas (271/271) antes e depois da
edição. {% if %}/{% endif %},{% block %}/{% endblock %},{% for %}/
{% endfor %}balanceados embase.html,dashboard.html,
dashboard_designer.htmleplatform_overview.html.- Revisão do
git diffde cada arquivo tocado, confirmando que os únicos
HTMLs alterados foram exatamente as classes/spans novos descritos
acima — nenhum widget, view ou endpoint mudou.
Não dá pra testar visualmente neste ambiente (sem browser/servidor
com banco real). Teste real: abrir os 3 dashboards (conta de provedor,
conta de projetista, e "Visão da plataforma" como superusuário), recolher
e expandir o menu lateral (confirmar que vira um trilho de ícones sem
sobrepor o conteúdo), conferir o cabeçalho hero, o selo de percentual no
card de acessos online, o acento roxo só na visão da plataforma, e o
avatar/breadcrumb na topbar.
Atualização
Execute apply no servidor. Sem migration nesta versão. Depois, um
Ctrl+F5 no navegador garante que o CSS novo seja carregado.
v0.72.1
AFService Map v0.72.1 — Hotfix de consistência de UI
Backup/ponto de rollback desta rodada: branch backup/mapa-pre-v0721-20260802
(pointer no commit c6cf0ec, v0.72.0). Pacote preparado pelo ChatGPT sobre
esse mesmo commit. Aplicado via apply_map_ui_v0721.py (validado antes com
--dry-run, sem erro, e com os checksums SHA-256 do pacote conferidos um a
um contra SHA256SUMS). Sem migration.
Isso desfaz algum hotfix anterior?
Não. Antes de aplicar, li o apply_map_ui_v0721.py inteiro: ele só
acrescenta duas linhas novas no templates/map.html (<link> de
map-ui-v0721.css e <script> de map-ui-v0721.js, ambas logo depois das
equivalentes da v0.72.0), faz o bump de versão em config/settings.py e
docker-compose.yml, e cria três arquivos novos (JS, CSS, teste estático).
Ele nunca abre map-optical-editor-v3.js, map-link-monitoring.js ou
map-master-suite.js, e não reescreve map-ui-v072.js/.css — só
convive com eles. Os hotfixes anteriores (v0.71.1) continuam exatamente
como ficaram.
A causa raiz do menu lateral, verificada antes de aceitar
O relato era: ao recolher o menu, ele não ia pra uma faixa fina de ícones —
ficava uma faixa larga. Fui conferir a explicação do pacote direto no CSS
em vez de confiar de olho:
map-master-suite.csstembody.map-master-suite #map-sidebar.map-sidebar-master { width: 272px !important; }.map-ui-v072.css(v0.72.0) tembody.map-ui-v072 #map-sidebar.v072-collapsed { width: 72px !important; }.
As duas regras usam !important e têm a mesma especificidade CSS (1 id
- 2 classes cada). Quando isso empata, quem carrega por último no HTML
vence — e dependendo da ordem de<link>, a regra de 272px podia ganhar
mesmo com a classev072-collapsedjá aplicada. Não era uma falha de
lógica JS, era um empate de especificidade puro.
O hotfix resolve isso com um seletor mais específico:
body.map-ui-v0721 #map-sidebar.map-sidebar-master.map-sidebar-v072.v072-collapsed,
body.map-ui-v0721 #map-sidebar.map-sidebar-v072.v072-collapsed {
width: 72px !important;
min-width: 72px !important;
max-width: 72px !important;
flex: 0 0 72px !important;
}Três/quatro classes em vez de duas — especificidade estritamente maior que
a regra de 272px, então passa a vencer sempre, independente da ordem de
carregamento das folhas de estilo. Também reajusta o Leaflet
(invalidateSize) ao abrir/fechar o menu e mantém o último estado
(recolhido ou não) salvo no navegador.
O que entrou
Menu lateral
- Recolhe de verdade para 72px (causa raiz explicada acima).
- Reajusta o tamanho do mapa Leaflet ao abrir/fechar.
- Mantém o último estado salvo no navegador (
localStorage). - Logo e cor continuam seguindo o whitelabel da empresa.
Barra superior
- "Selecionar" virou dois modos explícitos: Visualizar e Editar.
No modo Editar aparecem as ações administrativas permitidas. - Menu reorganizado:
Caixa ▾ (CTO/CEO/CDO), além de Poste, Cabo, Régua,
Área, Mais ▾ (CPD/POP, Rack, Torre), Rotas e Enlaces — todos reaproveitando
os mesmos botões/handlers legados (data-tool/data-quick-tool) por
baixo, sem duplicar lógica.
Rotas
- O botão abre a gaveta de rotas diretamente, sem depender de outro botão
oculto. - Só aparece quando existe rota óptica com ligação real (cabo conectado),
não só um nome cadastrado sem topologia. - Projeto sem rota nenhuma mostra mensagem objetiva em vez de ficar
escondido sem explicação.
Popups
Padrão único de HUD para CTO, CEO, CDO, Rack, Torre, CPD/POP, Poste e
cabos: ícone do tipo, nome, status (quando monitorado), ações organizadas
(Cabos e ligações, Equipamentos, Ficha/QR, Editar/Excluir conforme
permissão), botão Excluir realmente vermelho. A normalização do popup
agora é idempotente — compara uma assinatura do conteúdo (nome + tipo +
código + ações + status) antes de remontar, e só remonta se algo mudou de
verdade. Isso resolve a alternância entre popup moderno e popup antigo que
acontecia quando outro módulo injetava uma ação depois do popup já ter
sido montado.
Busca
Continua usando os handlers antigos por baixo (endereço, busca no
projeto), mas agora abre dentro de um painel com o visual do mapa novo,
cor de whitelabel, atalho Ctrl+K e fechamento por botão ou Esc.
Monitoramento
Sem mudança de comportamento: o indicador nos popups continua batendo
com o status real do SNMP (verde=normal, vermelho=offline,
amarelo=degradado/atenção, cinza=sem dados), some quando não há perfil
configurado. Este pacote não mexe em backbone, PTP, InfluxDB, Telegraf
ou no motor de alertas.
Verificação feita nesta rodada
- Checksums SHA-256 do pacote conferidos um a um contra
SHA256SUMS
antes de tocar em qualquer arquivo. BASE_COMMITdo pacote (c6cf0ec) confirmado igual aoHEADdomain
antes de aplicar.- Leitura completa do
apply_map_ui_v0721.pyconfirmando que ele não
toca nos arquivos dos hotfixes anteriores (detalhado acima). - Revisão do JS novo (
map-ui-v0721.js) atrás do mesmo padrão de bug já
visto duas vezes este ano (observador de mutação reagindo à própria
mudança): o observador que reconstrói sidebar/busca/toolbar ao detectar
mudanças no<body>tem guarda-antes-de-mutar em todos os três pontos
que ele chama (fixSidebar,fixSearchTriggers,rebuildToolbar—
cada um marca a flagdataset.v0721 = "1"antes de mexer no DOM, não
depois), e o observador de popup usa assinatura idempotente antes de
remontar. Não encontrei o padrão de bug desta vez. --dry-rundo aplicador, sem erro.python -m py_compilenos arquivos Python tocados.- Sintaxe do
map-ui-v0721.jsvalidada com esprima (nenhuma sintaxe nova
precisou de neutralização desta vez). - Chaves do
map-ui-v0721.cssbalanceadas (72/72). {% if %}/{% endif %}detemplates/map.htmlbalanceados (10/10).docker-compose.ymlvalidado como YAML.- Rodei manualmente as 5 asserções do teste estático incluído
(test_map_ui_v0721_static.py, que não depende de banco/GDAL): ordem de
carregamento do JS/CSS novo, toolbar com os modos e submenu de Caixa,
seletor de especificidade do menu lateral presente no CSS, contextualidade
de rotas/busca, normalização de popup idempotente — todas passaram.
A suíte Django completa (manage.py check, manage.py test) e o teste
visual real dependem de banco, GDAL e navegador — não rodam neste ambiente
de preparação.
Atualização
Execute apply no servidor. Sem migration nesta versão. Depois, um
Ctrl+F5 no navegador garante que o CSS/JS novos sejam carregados.
Teste sugerido: recolher e expandir o menu lateral e confirmar que ele
realmente vira uma faixa de 72px (não uma faixa larga só com ícones);
alternar entre Visualizar e Editar na barra superior; abrir Rotas num
projeto com e sem rota conectada; abrir um popup de CTO/Rack/Torre e
confirmar que o visual não pisca entre moderno e antigo ao clicar em ações
diferentes; testar a busca com Ctrl+K.
v0.72.0
AFService Map v0.72.0 — Redesign do mapa
Backup/ponto de rollback desta rodada: tag v0.71.1 (também disponível
como branch backup/mapa-pre-v072-20260802). Pacote preparado pelo
ChatGPT sobre o commit 065a3bb (v0.71.0), aplicado aqui já em cima do
hotfix v0.71.1. Aplicado via apply_map_ui_v072.py (validado antes com
--dry-run, sem erro). Sem migration.
A pergunta que importava: isso desfaz o hotfix v0.71.1?
Não. Conferi isso antes de aplicar qualquer coisa, como combinado. O
pacote só toca em templates/map.html, apps/core/context_processors.py,
config/settings.py/docker-compose.yml (versão) e três arquivos novos
(map-ui-v072.js, map-ui-v072.css, um teste estático). Ele referencia
map-link-monitoring.js só como texto-âncora (pra inserir a tag <script>
nova logo depois) e nunca abre map-optical-editor-v3.js — os dois
arquivos que corrigi no hotfix v0.71.1 continuam exatamente como
ficaram lá.
Só precisei ajustar uma coisa antes de aplicar: o script esperava
encontrar a string literal "0.71.0" pra fazer o bump de versão, mas o
main já estava em "0.71.1" por causa do hotfix. Corrigi isso
manualmente (defini a versão pra 0.72.0 direto) antes de rodar o
aplicador — sem isso ele teria abortado com erro, o que já é o
comportamento esperado (falha fechada) quando a base não bate.
A causa raiz do laço de requisições, finalmente resolvida
O print que você mandou (filtrado por Fetch/XHR) mostrou container-layout-v3
e equipment de map-master-suite.js repetindo dezenas de vezes por
segundo. O ChatGPT identificou a causa real: o template carregava dois
editores de estrutura ao mesmo tempo — o antigo (container-structure-v09.js,
que monta suas próprias abas Resumo/Equipamentos/Diagrama/Fibras/YAML) e
a Master Suite (que monta as abas novas Equipamentos/Canvas 2D/Matriz/
Fibras/YAML) — os dois brigando pelo mesmo <dialog id="container-dialog">,
cada um reagindo às mutações do outro. Meu hotfix v0.71.1 (trava de
"já em andamento" + debounce) reduzia o estrago, mas a causa de fundo —
dois sistemas de abas duplicados e competindo — continuava lá.
Este pacote remove o carregamento do editor antigo por completo. Só a
Master Suite continua ativa.
Verificação extra que fiz antes de liberar
Ao ler o map-ui-v072.js novo, encontrei outro observador de mutação
sem proteção contra reagir à própria mudança — exatamente o padrão de
bug do hotfix anterior, só que num lugar novo:
// como estava:
const observer = new MutationObserver(() => requestAnimationFrame(cleanContainerUi));
observer.observe(container, { childList: true, subtree: true, attributes: true, attributeFilter: ["open", "class"] });cleanContainerUi() adiciona classes CSS (classList.add(...)) em
elementos dentro do diálogo — e como o próprio observador escuta mudança
de class, cada chamada dele podia reagendar a si mesmo indefinidamente
(sem gerar requisição de rede dessa vez, mas consumindo CPU/frame à toa).
O observador vizinho no mesmo arquivo já tinha uma trava contra isso;
apliquei a mesma trava aqui antes de liberar:
let containerScheduled = false;
const observer = new MutationObserver(() => {
if (containerScheduled) return;
containerScheduled = true;
requestAnimationFrame(() => {
containerScheduled = false;
cleanContainerUi();
});
});O que entrou
Sidebar operacional nova
Logo da empresa, seletor de projeto, Início, busca no mapa, Equipamentos,
importar KMZ/KML, Alertas, Configurações, alternar tema, botão Sair
vermelho, dados da empresa no rodapé, recolhimento pra uma barra de 72px,
último estado salvo no navegador. As seções antigas (Camadas, Adicionar
ao mapa, Resumo) saem da lateral — Camadas continua no controle inferior
do mapa.
Whitelabel no mapa
O dashboard já usava current_company.logo; o mapa mostrava sempre a
logo estática da AFService. Agora usa automaticamente a logo, o nome
comercial e a cor principal configurados em "Minha administração →
Marca" — sem cadastro separado pro mapa, com fallback pra AFService
quando a empresa não tem logo própria.
Barra de ferramentas única
Selecionar | CTO | Caixa ▾ (CEO/CDO) | Poste | Cabo | Régua | Área | Mais ▾ (CPD/POP, Rack, Torre) | Rotas | Enlaces
"Rotas" só aparece quando a Master Suite confirma uma rota com ligação
real (não só cadastrada pelo nome, sem cabo conectado). "Enlaces" só
aparece depois de um projeto selecionado.
Régua que vira cabo
Marca vários pontos com distância acumulada, permite desfazer ponto,
continuar editando ou descartar, e converte o traçado num cabo real
(nome, código, tipo, modelo/quantidade de fibras, origem, destino,
descrição) — usando a API já existente /api/map/cables/create/, que já
cuida do modelo e da geração das fibras.
Ferramenta Área
Marca por vértices, calcula m² e converte automaticamente pra hectare ou
km², com desfazer vértice, continuar editando e exportação em GeoJSON
(incluindo project_id e area_m2).
Popups HUD modernos
CTO, CEO, CDO, Rack, Torre, CPD/POP, Poste, cabo e outros ganham um
popup novo: ícone do tipo, nome, tipo, código, status real, ação
principal em destaque, ações secundárias organizadas, botão Excluir
realmente vermelho, tema escuro/claro. Os handlers originais (Cabos e
ligações, Equipamentos, Ficha/QR, Fusões, Editar, Excluir, Monitoramento)
são reaproveitados, não recriados — os botões só são movidos pro popup
novo.
Status SNMP sem informação falsa
O popup só mostra indicador de status quando o elemento tem
monitoramento configurado de verdade (lendo as classes que o sistema de
monitoramento da v0.71.0 já aplica): verde pulsante (normal), vermelho
pulsante (offline), amarelo (degradado/atenção), cinza (sem dados), ou
nenhum indicador (sem monitoramento configurado) — nunca mostra "online"
só por o elemento existir no mapa.
Alertas com ou sem ERP
O sino de alertas superior passa a incluir Q(company_id=company.id)
diretamente e não depende mais de a empresa ter ERP configurado — mostra
queda de equipamento, porta, backbone e enlace PTP mesmo pra empresa só
com cadastro manual ou com outro ERP. Empresa em modo projetista
continua sem alerta operacional (ela não tem cliente pra monitorar).
Tema claro e escuro
Botão na sidebar pra alternar entre tema escuro (NOC) e claro, com
preferência salva no navegador.
Verificação feita nesta rodada
--dry-rundo aplicador rodado antes da aplicação real, sem erro (só
precisei corrigir o descompasso de versão antes, explicado acima).- Confirmado por leitura do próprio script que ele não abre nem
reescrevemap-optical-editor-v3.jsnemmap-link-monitoring.js— só
referencia o segundo como texto-âncora. python -m py_compilenos arquivos Python tocados/adicionados.- Sintaxe de
map-ui-v072.js(novo, 815 linhas) verificada com esprima —
precisou neutralizar também separador numérico (1_000_000, recurso
ES2021 que navegadores reais suportam, mas o parser de checagem não). - Chaves de
map-ui-v072.css(novo, 834 linhas) balanceadas;{% if %}/
{% endif %}detemplates/map.htmlbalanceados;docker-compose.yml
validado como YAML. - Rodei manualmente as 5 asserções do teste estático incluído no
pacote (test_map_ui_v072_static.py, que não depende de banco): CSS/JS
novos carregados,container-structure-v09.jsrealmente removido,
ordem de carregamento correta, whitelabel presente no template, API de
criação de cabo/GeoJSON presentes no JS, seletores legados presentes no
CSS de compatibilidade, filtro de empresa presente no context
processor — todas passaram. - Encontrado e corrigido um segundo observador de mutação sem proteção
contra auto-disparo emmap-ui-v072.js(detalhado acima) antes de
liberar.
A suíte Django completa (manage.py check, manage.py test) e os
testes visuais do docs/TEST_PLAN_V072.md dependem de banco, GDAL e
navegador reais — não rodam neste ambiente de preparação.
Atualização
Execute apply no servidor. Sem migration nesta versão. Depois, um
Ctrl+F5 no navegador garante que o JS/CSS novos sejam carregados
(evita cache dos arquivos antigos — incluindo o container-structure-v09.js
que não deve mais nem ser baixado).
Teste sugerido: abrir o mapa e conferir a sidebar nova, a logo/cor da
empresa aparecendo automaticamente, a barra de ferramentas única; abrir
uma Torre/Rack → Equipamentos e conferir que só um conjunto de abas
aparece agora (sem a duplicação Resumo/Equipamentos por baixo); testar a
Régua convertendo um traçado em cabo real; testar a ferramenta Área;
abrir um popup de CTO/CEO/cabo e conferir os botões antigos funcionando
dentro do visual novo; conferir que o sino de alertas aparece mesmo numa
empresa sem ERP; testar a troca de tema claro/escuro. E, principalmente:
repetir o cenário que travou o navegador (abrir Torre → Equipamentos) e
confirmar no DevTools (Fetch/XHR) que não há mais enxurrada de
requisições.
v0.71.1
AFService Map v0.71.1 — Hotfix: laço de requisições
Backup/ponto de rollback: tag v0.71.0.
Hotfix urgente a partir de um report ao vivo em produção: ao abrir o
diálogo "Monitoramento SNMP" de um equipamento dentro de uma Torre, o
navegador entrou num laço de requisições — o DevTools mostrou cerca de
8900 chamadas em ~60 segundos pra /api/map/elements/<id>/container-layout-v3/,
todas falhando com net::ERR_INSUFFICIENT_RESOURCES (o navegador
esgotou o limite de conexões pendentes).
Causa
loadContainerLayout(), em static/js/map-optical-editor-v3.js, usa uma
variável de módulo (containerLayoutId) pra evitar buscar o layout do
mesmo elemento duas vezes. O problema: essa variável só é atualizada
depois que a requisição termina. Se a função for chamada de novo
antes da primeira resposta voltar, a segunda chamada ainda vê o valor
antigo, passa pela checagem, e dispara sua própria requisição — e assim
por diante enquanto o gatilho continuar disparando mais rápido que o
tempo de resposta da rede.
O gatilho, neste caso: static/js/map-link-monitoring.js (v0.71.0) reage
a mudanças no diálogo de Rack/Torre pra desenhar o botão "Monitoramento"
e colorir portas/equipamentos por status — e essas próprias mudanças no
DOM eram vistas pelo observador de mutação de OUTRO script
(map-optical-editor-v3.js), que então tentava recarregar o diagrama,
que por sua vez também mexia no DOM, retriggando o observador do
monitoramento — um laço de mutação alimentando os dois lados, rápido o
bastante pra estourar a checagem "já carregado" do jeito que ela estava
escrita.
Corrigido
loadContainerLayout()ganhou uma trava de "já em andamento",
marcada antes doawait(não depois), mais um intervalo mínimo de 1
segundo entre buscas reais. Isso fecha a janela de corrida por completo
— não importa quantas vezes a função for chamada nem o que estiver
disparando isso, só uma requisição por vez consegue passar.- Título da porta parou de crescer indefinidamente: cada redecoração
de status SNMP grudava mais um· SNMP UPem cima do título anterior
em vez de substituir. Corrigido pra guardar o título original uma vez
(dataset.monitorBaseTitle) e sempre remontar a partir dele. - Observador de mutação do monitoramento agora se desconecta antes de
mexer no DOM (inserir botão, colorir portas/equipamentos) e só volta
a observar depois — evita que ele reaja às próprias mudanças, cortando
metade do laço na origem.
Atualização
Execute apply no servidor. Sem migration nesta versão — só dois
arquivos JavaScript. Depois do apply, um Ctrl+F5 no navegador
garante que a versão nova do JS seja carregada (evita cache do arquivo
antigo, que é justamente o que estava travando).
Teste sugerido: abrir uma Torre/Rack, entrar em Equipamentos, clicar
"Monitoramento" num equipamento, e conferir no DevTools (aba Network)
que não há mais uma enxurrada de requisições repetidas — só uma
chamada a container-layout-v3 ao abrir a estrutura.
v0.71.0
AFService Map v0.71.0 — Monitoramento de portas e enlaces
Backup/ponto de rollback desta rodada: tag v0.70.0 (também disponível
como branch backup/mapa-pre-monitoramento-v071). Pacote preparado pelo
ChatGPT, construído em cima do apps.snmp_monitoring já existente (da
v0.68.0), aplicado via apply_monitoring_links_v071.py (validado antes
com --dry-run, sem erro).
Tem migrations — snmp_monitoring.0002_link_monitoring e
alerts.0002_network_monitoring_alerts — vieram prontas no pacote desta
vez (diferente da Master Suite v0.70.0, não precisei escrever nenhuma à
mão). Revisei as duas por completo: só criação de tabela, AddField
opcional (null=True) e AlterField de choices — nada destrutivo.
Um bug real encontrado e corrigido antes de liberar
O aplicador do pacote tem uma função (patch_core_views) que insere
Q(company_id__in=company_ids) em dois filtros de alerta dentro de
apps/core/views.py. Ela usa um laço que chama .replace(old, new, 1)
duas vezes — mas como o texto de substituição (new) contém o próprio
texto original (old) como sufixo, a segunda chamada encontrou de novo
o texto recém-substituído (que ainda continha old embutido) em vez do
segundo ponto do arquivo. Resultado: o filtro novo saiu duplicado no
primeiro ponto (DashboardView._provider_context, inofensivo — só uma
condição OR repetida) e completamente ausente no segundo, que é a
view de verdade da página "Central de alertas" (company_alerts).
Na prática, sem esse ajuste, um alerta de porta ou enlace monitorado
(que tem company preenchido diretamente, mas não cto/olt/route)
nunca apareceria na Central de alertas de ninguém — o próprio recurso
principal desta versão ficaria invisível na tela que deveria mostrá-lo.
Corrigi manualmente: removi a duplicata, adicionei o filtro certo em
company_alerts, e ampliei o select_related da view pra incluir
monitored_link/container_equipment/equipment_port (o template já
passou a exibir esses campos; sem o select_related cada linha da
lista faria consultas extras).
O que entrou
Monitoramento nos equipamentos internos
Switch, roteador, firewall, servidor, access point, rádio PTP, ONU/ONT e
outros equipamentos ativos de Rack/Torre ganham a opção "Monitoramento":
IP de gerência, porta SNMP, community (criptografada, nunca retorna pela
API) e intervalo de coleta. OLT continua bloqueada neste fluxo
universal — ela usa a integração própria já existente.
Descoberta e mapeamento de portas
Depois da primeira coleta, o sistema mostra ifName/ifIndex/ifAlias/
status/última coleta, e cada interface pode ser associada a uma porta
física já cadastrada no equipamento, com uma função: Backbone, Uplink,
Acesso, Wireless/PTP, Gerência ou Outro. Só as portas vinculadas
controlam o status de enlaces — uma interface de cliente ou desligada
sem vínculo não derruba o equipamento inteiro.
Backbone entre cidades (com dois caminhos)
Um enlace liga duas portas monitoradas e opcionalmente um FiberCable
do mapa. Dá pra cadastrar dois caminhos independentes (ex.: principal e
secundário), cada um com portas, cabo, cor e alerta próprios — uma queda
no principal deixa só aquele cabo vermelho, o backup continua normal.
Estados: UP+UP = normal, UP+DOWN ou DOWN+DOWN = offline, UP+sem dado =
degradado, sem dados nas duas pontas = sem dados.
Quando confirma a queda (após o debounce): porta, equipamento, estrutura
e linha do enlace ficam vermelhos, o FiberCable.status vira offline (o
cabo aparece vermelho no mapa) e um alerta é criado. Na recuperação,
tudo volta ao normal e o alerta fecha sozinho.
Enlaces PTP wireless
Mesmo conceito, mas sem exigir cabo óptico: linha reta tracejada entre
as torres, cor roxa por padrão (configurável), mostra o status das duas
pontas no popup.
Debounce
Configurável por porta/enlace — padrão 30s pra confirmar queda e 30s
pra confirmar recuperação, evitando abrir e fechar alerta por oscilação
SNMP passageira.
Central de alertas ampliada
Novos escopos: equipamento, porta monitorada, enlace monitorado.
AlertEvent passa a ter company diretamente (além de elemento,
equipamento, porta, enlace, cabo, rota, projeto) — funciona pra empresa
com IXCSoft, com outro ERP, ou só com cadastro manual (antes só existiam
vínculos com OLT/PON/ONU/CTO/rota).
Desempenho
Consulta ao InfluxDB deixou de ser uma por equipamento — agora os
perfis são consultados em lote, portas atualizadas numa única passada, e
os enlaces avaliados depois. Atualização visual do mapa a cada 15s,
coleta Celery a cada 30s (SNMP_STATUS_POLL_SECONDS, configurável).
Além disso, corrigido um problema já existente desde a v0.68.0: o sinal
que recarrega o Telegraf disparava a cada save() do perfil — inclusive
os saves de telemetria feitos pela própria task de coleta (a cada ~30s).
Agora só dispara reload quando um campo de configuração de verdade muda
(IP, community, alvo etc.), não a cada ciclo de telemetria.
Segurança
- Token do InfluxDB só por variável de ambiente — nunca no código. Se o
token que foi colado numa conversa anterior ainda não foi rotacionado
no InfluxDB, isso continua pendente (é uma ação fora do repositório,
só o painel do InfluxDB resolve). - Community SNMP criptografada, nunca em texto puro.
- Socket do Docker continua montado só no
worker— verificado
manualmente queweb:não o recebe. - Recarga do Telegraf por
SIGHUP, semos.systemnemdocker restart.
Verificação feita nesta rodada
--dry-rundo aplicador rodado antes da aplicação real, sem erro.python -m py_compileem todos os arquivos Python tocados/adicionados.- Sintaxe de
map-link-monitoring.js(novo) verificada com esprima;
chaves demap-link-monitoring.cssbalanceadas;{% if %}/{% endif %}
detemplates/map.htmlbalanceados;docker-compose.ymlvalidado como
YAML. - Leitura completa dos dois modelos novos, das duas migrations, da task
de coleta, do módulo de status/debounce, da API (6 endpoints) e do
influx_client.py: todo endpoint usascope_company_queryset/
can_edit_company; toda query ao InfluxDB valida o formato do
influx_id(regex^[a-f0-9]{32}$) antes de compor a query Flux,
mesmo esse valor sendo gerado pelo servidor (nunca vindo direto do
usuário) — proteção extra contra injeção na query. - Rodei de verdade os testes puros incluídos no pacote
(test_link_status.py, que não depende de banco): as 5 asserções de
UP/UP, UP/DOWN, ponta ausente e debounce de queda/recuperação passaram
quando executadas diretamente neste ambiente. - Verifiquei manualmente as 4 asserções do teste estático
(test_monitoring_package_static.py): CSS/JS carregados no template,
rotaapi/monitoring/registrada,websem/var/run/docker.sock,
nenhum token hardcoded emsettings.py— todas passam. - Encontrado e corrigido o bug de duplicação/omissão do filtro de
empresa emapps/core/views.py(detalhado acima).
A suíte Django completa (manage.py check, manage.py test), as
migrations reais e os testes com equipamento físico (docs/TEST_PLAN_MONITORAMENTO_ENLACES.md)
dependem de banco, PostGIS, InfluxDB, Telegraf e SNMP reais — não rodam
neste ambiente de preparação.
Atualização
Execute apply no servidor — roda as duas migrations automaticamente.
Depois, rode manualmente (o apply não faz isso sozinho):
docker compose exec web python manage.py rebuild_snmp_monitoring --pollComo configurar depois do deploy
- Abrir uma Torre ou Rack → Equipamentos.
- Criar as portas físicas do switch/roteador/AP/rádio.
- Clicar em "Monitoramento", informar IP e community, salvar.
- Clicar em "Consultar agora", aguardar alguns segundos, clicar em
"Atualizar". - Associar cada interface SNMP à porta física correspondente.
- Repetir na outra ponta.
- Abrir o botão de enlaces na barra do mapa → escolher Backbone óptico/
Fibra/Cobre/PTP wireless → selecionar as duas portas (e o cabo, se
for backbone/fibra). - Também dá pra usar "Monitorar enlace" direto no popup do cabo.
Teste sugerido: derrubar administrativamente uma porta de teste e
confirmar que, após ~30s, a porta/marcador/linha ficam vermelhos, o cabo
(se houver) vira offline, e um alerta abre na Central de alertas da
empresa certa; reativar a porta e confirmar que tudo volta ao normal e o
alerta fecha sozinho após outros ~30s.