Skip to content

Releases: adrianfiio/ixcsoft-mapa

v0.76.0

Choose a tag to compare

@adrianfiio adrianfiio released this 02 Aug 13:39

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_compile em todos os arquivos tocados/novos.
  • is_access_blocked e has_trust_release_this_month são funções
    puras, sem banco — executei de verdade
    via script Python direto:
    6 asserções pra is_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 campo customer da fatura, só então apaga o model
    Customer inteiro. Nomes de constraint/índice conferidos contra a
    migration 0001_initial.py que os criou, pra garantir que bate
    exatamente com o que existe no banco.
  • Chaves do static/css/app.css balanceadas (282/282).
  • {% if %}/{% endif %}/{% for %}/{% endfor %}/{% block %}/
    {% endblock %} balanceados em todos os templates tocados e novos.
  • git diff --check limpo.
  • 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

Choose a tag to compare

@adrianfiio adrianfiio released this 02 Aug 13:15

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 (OneToOneField pra
    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 no IXCCustomer mas
    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.html agora 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_compile em todos os arquivos tocados/novos.
  • Migration nova revisada campo a campo; dependência de
    ixc_integration ajustada pra 0008_sync_progress (a mais recente),
    não 0001_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.css balanceadas (280/280).
  • {% if %}/{% endif %}/{% for %}/{% endfor %}/{% block %}/
    {% endblock %} balanceados em base.html,
    templates/billing/customer_list.html e
    templates/platform_overview.html — a estrutura aninhada nova do
    menu do Superadmin em base.html foi conferida diff a diff.
  • Revisei que "Configurar →" não permite configurar o mesmo cliente do
    ERP duas vezes (usa hasattr no acessor reverso do
    OneToOneField, que o Django projeta especificamente pra funcionar
    com hasattr/getattr nesse 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

Choose a tag to compare

@adrianfiio adrianfiio released this 02 Aug 13:00

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_company já restringiam o
    financeiro a CompanyMembership.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 o CompanyMembership.Role genérico já combinado
    com Company.company_type entrega 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 pk de cliente):
    superusuário não precisa nem do parâmetro — o cliente é buscado pelo
    pk e 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ínculo CompanyMembership, 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
    de CompanyOnboardingForm.
  • 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 de CompanyTeamMemberForm) com nível
    de acesso
    (VIEW/EDIT) escolhido já no cadastro.
  • slug gerado 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
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_compile em todos os arquivos Python tocados/novos.
  • Migration escrita à mão (sem GDAL neste ambiente de preparação),
    revisada campo a campo contra models.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.css balanceadas (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

Choose a tag to compare

@adrianfiio adrianfiio released this 02 Aug 12:23

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:

  1. 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.
  2. Página de detalhe do cliente nova, em vez de um botão no popup do
    mapa.
  3. Mensalidade recorrente automática via Celery Beat, em vez de
    lançamento manual mês a mês.
  4. 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.py este ano): 5 asserções, todas
    passaram, incluindo o caso de fevereiro bissexto vs. não-bissexto.
  • python -m py_compile em 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 rodar makemigrations).
    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.css balanceadas (276/276, só 3 classes
    novas pequenas — reaproveitei .overview-table, .sync-pill,
    .metric-card, .panel, .platform-form e .info-list
    existentes em vez de criar CSS novo pra tudo).
  • {% if %}/{% endif %}/{% for %}/{% endfor %}/{% block %}/
    {% endblock %} balanceados nos 3 templates novos
    (templates/billing/) e no base.html tocado.
  • YAML do docker-compose.yml vá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

Choose a tag to compare

@adrianfiio adrianfiio released this 02 Aug 02:25

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()
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 com Esc, e
    cancela sozinha ao fechar os diálogos de elemento/cabo no meio da
    operação (#element-dialog/#cable-dialog close) — resolve o
    "próximo clique preso no mapa" relatado.
  • Ícones não interceptam mais o clique: pointer-events: none nos
    ícones/SVGs internos dos botões do popup e da barra.
  • popupclose limpa 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 com Ctrl+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 diff que o pacote não toca em
    templates/base.html nem static/css/app.css
    (arquivos do reskin
    v0.73.0) — as únicas mudanças em templates/map.html foram as duas
    tags v0721v0722, exatamente como o script prometia.
  • Confirmado que map-optical-editor-v3.js, map-link-monitoring.js,
    map-master-suite.js e map-ui-v072.js/.css continuam intactos.
  • Li o map-ui-v0722.js inteiro (não só o resumo do pacote) pra
    confirmar a ausência real do MutationObserver e o cache de
    identidade, e o map-ui-v0722.css inteiro (pointer-events, seletores
    de busca/popup).
  • --dry-run do aplicador, sem erro.
  • python -m py_compile no Python tocado.
  • Sintaxe do map-ui-v0722.js validada com esprima.
  • Chaves do map-ui-v0722.css balanceadas (74/74).
  • {% if %}/{% endif %} de templates/map.html balanceados (10/10).
  • docker-compose.yml validado 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

Choose a tag to compare

@adrianfiio adrianfiio released this 02 Aug 02:05

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ó ampliei static/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_compile OK) — não há migration nem lógica de view nova.
  • Chaves do static/css/app.css balanceadas (271/271) antes e depois da
    edição.
  • {% if %}/{% endif %}, {% block %}/{% endblock %}, {% for %}/
    {% endfor %} balanceados em base.html, dashboard.html,
    dashboard_designer.html e platform_overview.html.
  • Revisão do git diff de 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

Choose a tag to compare

@adrianfiio adrianfiio released this 02 Aug 01:45

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.css tem body.map-master-suite #map-sidebar.map-sidebar-master { width: 272px !important; }.
  • map-ui-v072.css (v0.72.0) tem body.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 classe v072-collapsed já 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_COMMIT do pacote (c6cf0ec) confirmado igual ao HEAD do main
    antes de aplicar.
  • Leitura completa do apply_map_ui_v0721.py confirmando 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 flag dataset.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-run do aplicador, sem erro.
  • python -m py_compile nos arquivos Python tocados.
  • Sintaxe do map-ui-v0721.js validada com esprima (nenhuma sintaxe nova
    precisou de neutralização desta vez).
  • Chaves do map-ui-v0721.css balanceadas (72/72).
  • {% if %}/{% endif %} de templates/map.html balanceados (10/10).
  • docker-compose.yml validado 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

Choose a tag to compare

@adrianfiio adrianfiio released this 02 Aug 01:03

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-run do 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
    reescreve map-optical-editor-v3.js nem map-link-monitoring.js — só
    referencia o segundo como texto-âncora.
  • python -m py_compile nos 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 %} de templates/map.html balanceados; 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.js realmente 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
    em map-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

Choose a tag to compare

@adrianfiio adrianfiio released this 02 Aug 00:47

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 do await (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 UP em 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

Choose a tag to compare

@adrianfiio adrianfiio released this 02 Aug 00:35

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 migrationssnmp_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 que web: não o recebe.
  • Recarga do Telegraf por SIGHUP, sem os.system nem docker restart.

Verificação feita nesta rodada

  • --dry-run do aplicador rodado antes da aplicação real, sem erro.
  • python -m py_compile em todos os arquivos Python tocados/adicionados.
  • Sintaxe de map-link-monitoring.js (novo) verificada com esprima;
    chaves de map-link-monitoring.css balanceadas; {% if %}/{% endif %}
    de templates/map.html balanceados; docker-compose.yml validado 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 usa scope_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,
    rota api/monitoring/ registrada, web sem /var/run/docker.sock,
    nenhum token hardcoded em settings.py — todas passam.
  • Encontrado e corrigido o bug de duplicação/omissão do filtro de
    empresa em apps/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 --poll

Como configurar depois do deploy

  1. Abrir uma Torre ou Rack → Equipamentos.
  2. Criar as portas físicas do switch/roteador/AP/rádio.
  3. Clicar em "Monitoramento", informar IP e community, salvar.
  4. Clicar em "Consultar agora", aguardar alguns segundos, clicar em
    "Atualizar".
  5. Associar cada interface SNMP à porta física correspondente.
  6. Repetir na outra ponta.
  7. 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).
  8. 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.