Skip to content

Vigiante — Documentação de Customização

Registro das alterações realizadas no projeto Vicon SAGA original para transformá-lo no Vigiante/PICAPS (Fiocruz Brasília), incluindo correções de bugs herdados, identidade visual, autenticação, compatibilidade com MySQL moderno, infraestrutura Docker e deploy em VPS Oracle Cloud com Nginx Proxy Manager.

Projeto base: Vicon SAGA (PHP 7.4 + Apache + MySQL 8, encoding ISO-8859-1) Rebranding: Vicon SAGA → Vigiante Organização: Fiocruz Brasília + UnB + SES-DF Data: Abril 2026


Sumário

  1. Identidade visual
  2. Conteúdo e textos
  3. Roteamento e URLs
  4. Encoding e caracteres inválidos
  5. Autenticação e criação de projeto
  6. Compatibilidade com MySQL moderno
  7. Logos, paletas e SVG
  8. Importação de dados (CSV/XLSX)
  9. Upload de arquivos e permissões
  10. Google Maps API
  11. Infraestrutura Docker
  12. Deploy em VPS Oracle Cloud
  13. Procedimentos de atualização
  14. Questões pendentes e notas de manutenção
  15. Ícones customizados no mapa — bugs corrigidos

Identidade visual

Cores

  • Primária: #00427e (azul Vigiante)
  • Gradiente navbar site público: #002648 → #003a6e
  • Gradiente navbar admin (install.php): #256aa0 → #3788c4 (azul mais claro para distinguir área administrativa)
  • Secundária / sucesso: #008B44 (verde)
  • Destaque logo: #FF5B04 (laranja)

Logos

  • img/logo.svg — Logo completa (símbolo + texto "Vigiante" laranja)
  • img/logo-symbol.svg — Apenas o símbolo (olho laranja sem texto), usado como default de projetos sem logo customizada
  • img/logofav.png — Favicon 512×512 regenerado a partir do símbolo, usado em todas as páginas (site, help, install, etc.)

Backgrounds

  • img/backgrounds/1.jpg, 1-150x150.jpg, 1-small.jpg, 1b.jpg, 1b-150x150.jpg — substituídos por gradientes claros neutros (o original tinha padrão decorativo de olhos que conflitava com o símbolo do Vigiante)
  • img/background-pattern-gray.png — mesma substituição
  • img/backgrounds/vigiante-bg.jpg — novo background default usado como fallback em thumbnails de projeto

Alteração em engine/project.php:

public $strProjectImageNoLogo       = 'img/logo-symbol.svg';
public $strProjectImageNoBackground = 'img/backgrounds/vigiante-bg.jpg';

Fontes

  • Removida a Bauhaus93 (fonte Windows decorativa, dependente de OS e com estilização difícil de ler em títulos longos)
  • .project-title e .project-info .title agora usam Arial, Helvetica, sans-serif
  • Tirado text-shadow e letter-spacing exagerado do título

Página install.php (administração)

CSS adicionado para distinguir visualmente como área admin:

.navbar-inner {
    background-color: #2d7bb6 !important;
    background-image: linear-gradient(to bottom, #256aa0, #3788c4) !important;
}
#imgLogo { height: 42px !important; width: auto !important; }
.navbar-inner .admin-badge { color: #fff; font-size: 14px; font-weight: 600; }
.btn-primary, .btn-info { background: linear-gradient(to bottom, #256aa0, #3788c4) !important; }
.btn-success { background: linear-gradient(to bottom, #006e36, #008B44) !important; }

O texto "Vicon SAGA —" foi removido (redundante com a logo) e substituído por "— Administração" discreto.


Conteúdo e textos

Página Sobre (site/sobre.php)

Criada do zero. Contém: - Histórico — 4 parágrafos institucionais sobre a origem do Vicon/Vigiante - Contato — e-mail contato@vigiante.com - Equipe Vicon SAGA — Jorge Xavier, Tiago Marino, Gabriel Lousada (com links Lattes) - Equipe Vigiante — Fernanda Machiner, Jeferson Martins de Castro, Marcelo Souza de Jesus, Rui Ogawa

Fotos em img/site/team/{fernanda,jeferson,marcelo}.png + rui.jpeg. CSS: legendas em azul (#00427e), cards 160×180px com object-fit: cover.

Home (site/home.php)

Estrutura em 3 seções: 1. Logo PICAPS centralizada 2. Nossa Atuação (texto PICAPS 2020) 3. Territórios Saudáveis, Sustentáveis e Solidários

Ambas as seções usam fundo branco uniforme (removida a classe vigiante-section-alt que dava fundo azul claro alternado).

Página Features (site/features.php)

CSS inline adicionado para garantir que links fiquem em azul Vigiante:

.main-menu a, .block a { color: #00427e !important; }
.main-menu a:hover, .block a:hover { color: #003466 !important; }
.block a.btn { color: #ffffff !important; }

Página Ajuda (help/index.php)

  • Removido: "Vicon SAGA Mobile" (menu + submenu "Vicon Mobile - Guia de Referência")
  • Removido: "Criação Vicon Mobile + Consulta QGIS"
  • Renomeado: "Criação e Consulta Vicon Web" → "Criação e Consulta"
  • Separadores (replacement chars UTF-8) trocados por • (•)
  • Item "Mobile" removido (site já é responsivo)
  • Botões de fechar modais (×) corrigidos para ×

Páginas removidas

  • contact.php e download.php (conteúdo obsoleto)

Roteamento e URLs

Problema original

ErrorDocument 404 /site/home no .htaccess da raiz mascarava 404s como home silenciosamente. Além disso, a regra genérica ^([a-zA-Z0-9]+)$ → index.php?strAlias=$1 interferia com URLs legítimas sob /site/.

Correção no .htaccess raiz

# Desativado
#ErrorDocument 404 /site/home

# Adicionado: URLs sob /site/ bypass o .htaccess raiz
RewriteRule ^site/ - [L]

Correção no site/.htaccess

Removidas rotas obsoletas (^contact$, ^download$), adicionada rota ^sobre$, e adicionado catch-all para URLs novas:

# Rotas explícitas primeiro (home, projects, features, sobre, join, ...)
RewriteRule ^sobre$  index.php?s=sobre [L,NC]

# Catch-all: qualquer /site/X vira ?s=X
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^([a-zA-Z0-9_-]+)$ index.php?s=$1 [L,NC,QSA]

Router em site/index.php

Adicionado fallback defensivo: se o Apache não passou $_GET['s'], extrai da URL direto:

if (empty($_GET['s'])) {
    $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
    $basename = basename(rtrim($uri, '/'));
    if ($basename && $basename !== 'index.php' && $basename !== 'site') {
        $_GET['s'] = $basename;
    }
}

Também foi trocado file_exists($_GET['s'].'.php') por file_exists(__DIR__.'/'.$_GET['s'].'.php') para não depender do CWD do Apache.


Encoding e caracteres inválidos

Replacement chars residuais

O projeto original usa ISO-8859-1 em múltiplos lugares mas tinha bytes inválidos (EF BF BD — UTF-8 replacement character) em vários arquivos.

  • index.php — 12 ocorrências substituídas:
  • 7 como separadores de footer → •
  • 3 como botão fechar modal → ×
  • 2 auxiliares
  • install.php — 2 botões fechar modal → ×

connection.inc — line endings

O arquivo original tinha apenas CR (formato Mac Classic pré-2001), fazendo o PHP ler tudo como uma linha única e falhar ao parsear as variáveis. Convertido para LF (Unix).

Mensagens de erro JavaScript

Em site/join.php, as mensagens de erro usam entidades HTML em vez de caracteres acentuados diretos, para compatibilidade com qualquer encoding:

showInfoAlert('A senha deve conter pelo menos uma letra e um número');
showInfoAlert('As senhas não coincidem');

A função showInfoAlert injeta via .innerHTML, então entidades HTML renderizam corretamente.

Conversão UTF-8 → ISO-8859-1 em ajax.php

Problema: browsers sempre enviam parâmetros em UTF-8 (independentemente do <meta charset>). O sistema Vicon SAGA é ISO-8859-1 internamente: banco, strings.csv, arquivos PHP. Quando um usuário digitava um título com acentos (ex: "Saúde de Emergência"), os bytes UTF-8 (C3 BA para ú) eram gravados num campo ISO-8859-1, e ao exibir, cada byte era lido como um caractere latino separado, resultando em "Saúde de Emergência".

Solução: conversor global no topo de ajax.php que processa todos os parâmetros $_GET e $_POST de uma vez, antes de qualquer handler:

function _utf8ToLatin1Recursive(&$value) {
    if (is_array($value)) {
        foreach ($value as &$v) _utf8ToLatin1Recursive($v);
    } elseif (is_string($value) && mb_check_encoding($value, 'UTF-8')) {
        $converted = @mb_convert_encoding($value, 'ISO-8859-1', 'UTF-8');
        if ($converted !== false) $value = $converted;
    }
}
_utf8ToLatin1Recursive($_GET);
_utf8ToLatin1Recursive($_POST);

Cobre todos os handlers do AJAX (SJI criar projeto, PU renomear, REC criar registro, etc.) — correção cirúrgica que resolve sem migrar o sistema para UTF-8.

Visualização de acentos no terminal MySQL

O banco grava em ISO-8859-1 (1 byte por caractere acentuado). Ao consultar direto via terminal, o byte C1 (Á) ou E1 (á) sozinho é inválido em UTF-8, então o terminal mostra . Os dados estão corretos — é só o terminal interpretando errado.

Para ver corretamente, adicione --default-character-set=latin1:

sudo docker compose exec db mysql --default-character-set=latin1 \
    -u viconsaga -psecret viconsaga \
    -e "SELECT idForm, strName FROM form;"

Mostra Territórios Atuação Picaps em vez de Territ�rios Atua��o Picaps.

Dados antigos com double-encoding

O fix do ajax.php só afeta inserções novas. Registros gravados antes do fix (durante desenvolvimento, enquanto o bug existia) têm bytes UTF-8 onde deveriam ter ISO-8859-1. Para corrigi-los:

UPDATE project
SET strName = CONVERT(CAST(CONVERT(strName USING BINARY) AS CHAR CHARACTER SET utf8) USING latin1)
WHERE idProject = X;

Ou simplesmente deletar e recriar.


Autenticação e criação de projeto

Problema original

No fluxo padrão, ao criar um projeto: 1. Sistema gerava senha aleatória de 4 caracteres 2. Enviava por e-mail (SMTP) em texto puro 3. Usuário precisava consultar o e-mail para entrar

Com SMTP não configurado, o usuário ficava sem acesso ao próprio projeto.

Nova UX

O usuário agora define a senha no momento da criação através de dois campos adicionais no formulário (site/join.php): - Senha (mínimo 8 caracteres, pelo menos uma letra e um número) - Confirmar senha

Uma caixa de instruções .alert-info acima dos campos lista os requisitos explicitamente.

Validação no cliente (antes do AJAX)

if (strPassword.length < 8) { ... }
if (!/[A-Za-z]/.test(strPassword) || !/[0-9]/.test(strPassword)) { ... }
if (strPassword !== strPasswordConfirm) { ... }

Mudanças no código

  • site/join.php: campos novos + validação + parâmetro strPassword enviado via AJAX
  • ajax.php handler SJI: propaga $_GET['strPassword'] para insertProject
  • engine/project.php insertProject(): nova assinatura php public function insertProject($strEmail, $strName, $strAlias, $blnSendNotificationMail = true, $blnCreateForArchiveRestore = false, $strUserPassword = '') { // ... $strPassword = ($strUserPassword !== '' && $strUserPassword !== null) ? $strUserPassword : $this->strings->generateRandomString(8); // INSERT com MD5($strPassword) }

E-mail de boas-vindas

  • A senha não é mais enviada em texto puro no e-mail
  • O e-mail contém apenas: e-mail cadastrado + URL do projeto + link do guia
  • Chamada de sendMail envolvida em @ para falhar silenciosamente quando SMTP não está configurado

Parâmetros AJAX

A função formSubmit() passou a usar encodeURIComponent em vez de EliminateSpecialChars, preservando corretamente senhas com caracteres especiais (&, +, %, etc.).

"Esqueci minha senha"

Mantido como está — continua dependendo de SMTP configurado. Decisão consciente: é caso de exceção, não fluxo principal.

Reset manual de senha

Enquanto SMTP estiver vazio, reset via SQL direto:

docker compose exec db mysql -u viconsaga -psecret viconsaga \
  -e "UPDATE user SET strPassword = MD5('novaSenha123') WHERE strEmail = 'x@y.com';"

Compatibilidade com MySQL moderno

O projeto original usa queries incompatíveis com only_full_group_by (ativo por padrão no MySQL 5.7+). Isso fazia queries inteiras falharem silenciosamente, resultando em telas vazias, popups incompletos ou árvores com apenas o nó pai.

Bug 1 — Listagem de projetos do usuário logado

Arquivo: engine/user.php, função selectUserProjects Sintoma: logado, a página /site/projects mostrava "Nenhum projeto encontrado"; sem login, os projetos apareciam normalmente.

Query original (quebrada):

SELECT DISTINCT ... FROM project p, user u
WHERE u.strEmail = (SELECT u.strEmail FROM user u WHERE u.idUser=N) -- alias 'u' colide com o externo
   OR u.strName = (SELECT u.strName FROM user u WHERE u.idUser=N)
GROUP BY p.idProject  -- rejeitado por only_full_group_by

Correção: - Alias das subqueries renomeado para u2 (elimina conflito de escopo) - Trocado = por IN nas subqueries (tolera múltiplos resultados) - Reescrito join implícito como INNER JOIN explícito - GROUP BY removido (DISTINCT já garante unicidade) - && trocado por AND

Bug 2 — Campos de formulário de múltipla escolha não aparecem no popup

Arquivo: engine/formrecord.php, função selectFormRecordData Sintoma: ao clicar em um ponto no mapa, apareciam apenas metadados (ID, URL, Criado, Coordenadas) — os campos de formulário como "Categoria: Público" não eram exibidos.

Causa: a query UNION que monta os dados do registro tinha GROUP BY frfa.idFormRecordField na parte de alternatives. Com only_full_group_by, a UNION inteira era rejeitada pelo MySQL, retornando zero linhas.

Correção: remoção do GROUP BY desnecessário (os dados já são únicos pela combinação idFormRecordField + idFormFieldAlternative).

Bug 3 — Lista lateral de registros não expande

Arquivo: engine/formrecord.php, função selectFormRecordsTreeView Sintoma: ao abrir a janela lateral "Registros", aparecia apenas o nó pai do formulário com a contagem (Territórios Atuacao Picaps (71)), mas ao tentar expandir não aparecia nenhum dos 71 registros individuais.

Causa: a query principal que lista os registros do formulário tinha GROUP BY fr.idFormRecord com múltiplas colunas não agrupadas (idShapeType, idUser, intOrder, strValue, dblLatitude, etc.). Com only_full_group_by, a query falhava silenciosamente e retornava zero registros filhos.

Correção: envolver a query em um SELECT externo usando ANY_VALUE() para as colunas não-agrupadas:

SELECT 
    idFormRecord,
    ANY_VALUE(idShapeType) AS idShapeType,
    ANY_VALUE(strValue) AS strValue,
    ANY_VALUE(dblLatitude) AS dblLatitude,
    -- ... etc
FROM (
    SELECT ... FROM formrecord ... ORDER BY ... LIMIT 500
) t
GROUP BY idFormRecord

O ORDER BY fica na subquery interna, garantindo que o primeiro valor apanhado por ANY_VALUE() seja o que importa visualmente.

Observação geral

Este padrão de bug pode existir em outras queries do projeto. A função selectFormRecordsTreeView tem outras 3 queries com o mesmo padrão (GROUP BY fr.idFormRecord sem agregação), mas todas são sobrescritas por atribuições posteriores de $query — ficam no código como "variantes" comentadas/substituídas. Se um dia alguém descomentar uma delas, vai cair no mesmo bug.

Se aparecer nova tela vazia ou funcionalidade "que não retorna nada", a primeira coisa a investigar são queries com GROUP BY em colunas não agrupadas.


Logos, paletas e SVG

Bug crítico: SVG como logo default

Sintoma: usuário comum (não-admin) tentava renomear projeto ou alterar tamanho de marcador, o botão "Salvar" ficava preso em "Aguarde". Admin funcionava normalmente.

Causa encadeada: 1. Ao trocar strProjectImageNoLogo de PNG para img/logo-symbol.svg, a função arrGetImagePallete() em engine/strings.php (linha ~1093) era chamada com um caminho SVG. 2. arrGetImagePallete só suporta GIF ($size[2] == 1), JPEG (== 2) e PNG (== 3). Para SVG, $size[2] é indefinido, e imagecreatefromXXX nunca é chamado. 3. imagecopyresampled($null, ...) lança erro fatal PHP. 4. O HTML da página admManageProject.php era truncado no meio, antes de renderizar div_permissions. 5. Para usuários comuns (não-admin), a seção de Permissões não carregava, mas o botão "Salvar" continuava visível (vem antes do truncamento). 6. Ao clicar Salvar, o JavaScript projectUpdate() tentava ler .checked de 7 campos que não existiam no DOM, estourava TypeError: Cannot read properties of null, e o botão ficava em "Aguarde" para sempre porque nunca recebia .button('reset').

Correção: fallback em arrGetImagePallete que retorna a paleta baseada na cor base do site quando o arquivo não é raster (SVG, BMP, WebP) ou não existe:

function arrGetImagePallete($strFileName) {
    if (isset($strFileName)) {
        if (! file_exists($strFileName)) {
            return array(str_replace('#', '', $this->strSiteBaseColor) => 1);
        }
        $size = @GetImageSize($strFileName);
        // $size[2]: 1=GIF, 2=JPEG, 3=PNG. Outros não suportados.
        if (! $size || ! in_array($size[2], array(1, 2, 3))) {
            return array(str_replace('#', '', $this->strSiteBaseColor) => 1);
        }
        // ... resto original
    }
}

Lição

Qualquer mudança que afete logos default precisa verificar se as funções que processam imagens no projeto suportam o novo formato. SVG é cada vez mais comum, mas a biblioteca GD do PHP não trata SVG.


Importação de dados (CSV/XLSX)

Bug CSV — vírgula decimal vira zero

Sintoma: ao importar CSV exportado do Excel brasileiro, todos os pontos apareciam próximos à costa da África (coordenada 0°, 0°).

Causa: o parsing de coordenadas em arrExtractCSVShapeObject tinha dois passos em conflito:

  1. Na leitura do CSV (importer.php ~linha 65), vírgulas dentro de aspas são substituídas por pipe | para não quebrarem o explode(','). Ou seja, "-29,1728559" vira "-29|1728559".
  2. Em arrExtractCSVShapeObject, a conversão tentava apenas str_replace(",", ".") — mas a vírgula já tinha virado pipe. Resultado: "-29|1728559" continua não-numérico, is_numeric() retorna false, lat/lng viram 0.

Correção em engine/importer.php, arrExtractCSVShapeObject:

// Remove aspas que vêm do CSV quando valores são quotados
$lat = trim($lat, " \t\n\r\0\x0B\"'");
$lng = trim($lng, " \t\n\r\0\x0B\"'");
// Desfaz tanto pipe (vírgula decimal preservada) quanto vírgula (caso ainda exista)
$lat = str_replace(array('|', ','), '.', $lat);
$lng = str_replace(array('|', ','), '.', $lng);

Agora o valor "-29|1728559" é convertido corretamente para -29.1728559.

Bug XLSX — ZipArchive library is not enabled

Sintoma: importação direta de arquivo .xlsx exibia tela em branco.

Causa: arquivos XLSX são tecnicamente ZIPs (contêm XMLs internamente). A biblioteca PHPExcel precisa da extensão zip do PHP para descompactá-los. A imagem base php:7.4-apache do Docker não vem com essa extensão.

Log do Apache mostrava:

PHP Fatal error: Uncaught Exception: ZipArchive library is not enabled
in /var/www/html/js/excel/Classes/PHPExcel/Reader/Excel2007.php:240

Correção no Dockerfile: instalar libzip-dev (dependência do sistema) e adicionar zip à lista de extensões PHP compiladas. Também adicionei gd (imagens), pois é usada em arrGetImagePallete para logos PNG/JPEG reais.

RUN apt-get update && apt-get install -y --no-install-recommends \
        libzip-dev libpng-dev libjpeg-dev libfreetype-dev \
    && rm -rf /var/lib/apt/lists/*

RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install mysqli pdo pdo_mysql zip gd

Após essa alteração é necessário rebuild (docker compose up -d --build).


Upload de arquivos e permissões

Bug de upload de fotos

Sintoma: ao adicionar foto ao registro, barra de progresso completava, aparecia "Salvo", mas a contagem de Arquivos Anexos continuava 0.

Diagnóstico via DevTools: 1. Network mostrava POST para /js/fileupload/index.php com status 200 (chegou ao servidor) 2. Aba "Response" revelava: Warning: mkdir(): Permission denied in UploadHandler.php line 1158 Warning: move_uploaded_file(../../tmp/foto.jpeg): failed to open stream

Causa: o Dockerfile não criava as pastas tmp/, projects/, sensors/, support/, archives/ nem definia permissões para www-data (usuário do Apache).

Correção no Dockerfile:

RUN mkdir -p /var/www/html/tmp /var/www/html/projects \
             /var/www/html/support /var/www/html/archives /var/www/html/sensors \
 && chown -R www-data:www-data /var/www/html/tmp /var/www/html/projects \
             /var/www/html/support /var/www/html/archives /var/www/html/sensors \
 && chmod -R 775 /var/www/html/tmp /var/www/html/projects \
             /var/www/html/support /var/www/html/archives /var/www/html/sensors

As pastas também foram criadas no empacotamento (cada uma com um .gitkeep) para garantir presença desde o primeiro build.


Google Maps API

Integração atual

O projeto usa a API Google Maps JavaScript v3 (versão legacy) em 6 arquivos: - site/index.php - index.php (raiz) - recordCreate.php - mapLocate.php - mobile.php - recordPrint.php

Biblioteca carregada com:

<script src="https://maps.googleapis.com/maps/api/js?key=<?=$database->strGoogleAPIKey;?>&v=3.exp&libraries=visualization,places,geometry"></script>

Configuração do projeto no Google Cloud Console

  1. Criar projeto: vigiante-php-picaps no Google Cloud Console
  2. Ativar 3 APIs (em APIs & Services → Library):
  3. Maps JavaScript API
  4. Places API (a clássica, não "Places API (New)" — o código usa legacy)
  5. Geocoding API
  6. Habilitar billing: obrigatório. Há US$ 200/mês de crédito grátis, o que cobre aproximadamente 28.000 carregamentos de mapa.
  7. Criar API Key (Credentials → Create → API Key)
  8. Restringir a chave (após criação):
  9. Application restrictions: Websites / HTTP referrers
  10. Sites autorizados:
    • http://localhost:8180/* (dev local)
    • http://localhost/* (dev alternativo)
    • https://vigiante.com/*
    • https://www.vigiante.com/*
    • https://*.vigiante.com/* (subdomínios futuros)
  11. API restrictions: apenas Maps JavaScript API, Places API (clássica), Geocoding API

Configuração da chave no connection.inc

Antes de rodar docker compose up pela primeira vez (ou em qualquer deploy novo), edite connection.inc na raiz do projeto:

$strGoogleAPIKey = 'AIzaSy...'; // substitua pela chave real

Se esse passo for pulado, o mapa carrega mas aparece uma marca d'água "For development purposes only" cobrindo toda a imagem e nenhum marcador funciona corretamente.

Atenção ao formato: a chave começa com AIzaSy (6 chars). Erros comuns: - Prefixo AP acidentalmente adicionado → InvalidKeyMapError - Espaços antes/depois - Aspas simples/duplas invertidas por autocompletar do editor

Atenção em updates: a chave não está no zip (fica em connection.inc com configurações de banco). Cada vez que você substituir a pasta do projeto numa VPS já em produção, copie o connection.inc antigo antes:

cp /opt/vigiante/connection.inc ~/connection.inc.vps
# ... substituir projeto ...
sudo cp ~/connection.inc.vps /opt/vigiante/connection.inc

Erros típicos e diagnóstico

Erro (console F12) Causa Solução
For development purposes only na marca d'água Chave não configurada ou sem billing Adicionar chave válida com billing ativo
InvalidKeyMapError Chave inválida ou com typo Verificar connection.inc
RefererNotAllowedMapError URL não consta nas restrições Sites Adicionar na API Key
LegacyApiNotActivatedMapError Places API (clássica) não ativada Ativar em APIs & Services → Library (procurar "Places API" sem "(New)")
ApiNotActivatedMapError API específica não habilitada Verificar todas 3 APIs do projeto

Após ativar APIs, aguardar até 5 minutos para propagação antes de testar.

Avisos de deprecation (não bloqueantes)

  • google.maps.Marker — deprecado em Feb/2024, recomendado migrar para google.maps.marker.AdvancedMarkerElement
  • google.maps.places.SearchBox — legacy desde Mar/2025

Ambos continuam funcionais. Google garante mínimo 12 meses de aviso antes de descontinuar. Migração é refactor significativo em main.js, index.php, home.php, recordCreate.php, recordEdit.php — não urgente.


Infraestrutura Docker

Estrutura do projeto

viconsaga_vigiante-master/
├── Dockerfile
├── docker-compose.yml
├── docker/
│   ├── apache-vhost.conf
│   ├── custom-php.ini
│   └── mysql-custom.cnf
├── schema/
│   └── viconsaga.sql
├── connection.inc
├── index.php
├── site/
├── engine/
├── js/
├── img/
├── tmp/        # upload temporário
├── projects/   # pastas de projetos
├── sensors/
├── support/
├── archives/
└── ...

Dockerfile (versão final)

FROM php:7.4-apache

# Bibliotecas de sistema necessárias para extensões PHP:
# - libzip-dev: requerida pela extensão zip (usada para ler .xlsx, que é um ZIP)
# - libpng-dev, libjpeg-dev, libfreetype-dev: requeridas pela extensão gd (imagens)
RUN apt-get update && apt-get install -y --no-install-recommends \
        libzip-dev libpng-dev libjpeg-dev libfreetype-dev \
    && rm -rf /var/lib/apt/lists/*

# Extensões PHP:
# - mysqli, pdo, pdo_mysql: acesso ao banco
# - zip: leitura de .xlsx
# - gd: manipulação de imagens (paleta de logos)
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install mysqli pdo pdo_mysql zip gd

RUN a2enmod rewrite
COPY docker/apache-vhost.conf /etc/apache2/sites-available/000-default.conf
RUN a2ensite 000-default

COPY . /var/www/html

# Pastas de upload com permissão para www-data (obrigatório)
RUN mkdir -p /var/www/html/tmp /var/www/html/projects \
             /var/www/html/support /var/www/html/archives /var/www/html/sensors \
 && chown -R www-data:www-data /var/www/html/tmp /var/www/html/projects \
             /var/www/html/support /var/www/html/archives /var/www/html/sensors \
 && chmod -R 775 /var/www/html/tmp /var/www/html/projects \
             /var/www/html/support /var/www/html/archives /var/www/html/sensors

COPY docker/custom-php.ini /usr/local/etc/php/conf.d/custom-php.ini
WORKDIR /var/www/html
EXPOSE 80
CMD ["apache2-foreground"]

docker-compose.yml (deploy local)

services:
  app:
    build:
      context: .
    volumes:
      - ./:/var/www/html
      - ./docker/apache-vhost.conf:/etc/apache2/sites-available/000-default.conf:ro
    ports:
      - "8180:80"
    depends_on:
      - db
    environment:
      - DB_HOST=db
      - DB_NAME=viconsaga
      - DB_USER=viconsaga
      - DB_PASSWORD=secret
    networks:
      - viconsaga-network

  db:
    image: mysql:8
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: viconsaga
      MYSQL_USER: viconsaga
      MYSQL_PASSWORD: secret
    volumes:
      - db_data:/var/lib/mysql
      - ./docker/mysql-custom.cnf:/etc/mysql/conf.d/mysql-custom.cnf
      - ./schema/viconsaga.sql:/docker-entrypoint-initdb.d/viconsaga.sql
    ports:
      - "3307:3306"
    networks:
      - viconsaga-network

networks:
  viconsaga-network:
    driver: bridge

volumes:
  db_data:

Subir localmente (primeira vez)

# 1. Editar connection.inc com a chave Google Maps
# 2. Subir
docker compose down -v
docker compose up -d --build
# Aguardar ~20s para MySQL importar o schema

Acesso: http://localhost:8180


Deploy em VPS Oracle Cloud

Cenário: VPS Oracle Cloud (Ubuntu) com múltiplos serviços dockerizados (Portainer, Nginx Proxy Manager, WordPress, n8n, Gitlab, etc.) e NPM publicando tudo via subdomínios *.ruiogawa.net.

Armadilhas encontradas

1. Docker rootless vs Docker tradicional

Sintoma: containers subiam normalmente, mas nem pelo IP público nem pelo NPM era possível acessar.

Causa: Sistema tinha duas instalações do Docker paralelas: - Docker rootless (executado como usuário, pasta ~ sem sudo) - Docker tradicional (com sudo, pasta /opt)

Docker rootless usa rootlesskit para mapear portas, não cria regras iptables padrão, e é isolado do Docker tradicional.

Diagnóstico:

sudo ss -tlnp | grep 8180
# Se aparecer "rootlesskit" como processo → Docker rootless

Solução: mover para o Docker tradicional:

docker compose down -v           # no rootless
sudo mv viconsaga_vigiante-master /opt/vigiante
cd /opt/vigiante
sudo docker compose up -d --build

2. Oracle Cloud só roteia tráfego externo nas portas 80 e 443

Apesar de Security List poder liberar qualquer porta, a configuração da subnet nessa VPS específica só permite tráfego externo em 80/443. Tentativas de curl http://IP_PUBLICO:8180 de fora dão timeout mesmo com regra ingress correta.

Implicação: todos os serviços precisam ser publicados via NPM (80/443). Acesso direto ao IP público em portas customizadas não funciona.

3. Redes Docker isoladas

Sintoma: NPM não conseguia fazer proxy para o Vigiante usando IP_PUBLICO:8180.

Causa: cada docker compose up cria uma rede Docker isolada (ex: vigiante_viconsaga-network). O NPM estava em npm-ruiogawa_default. Redes separadas = containers não se veem por nome.

Solução: declarar a rede do NPM como externa no docker-compose.yml do Vigiante, conectando o container app a ambas as redes. Esta alteração é específica da VPS e não vai no zip padrão:

services:
  app:
    # ...
    networks:
      - viconsaga-network
      - npm-ruiogawa_default
  db:
    # ...
    networks:
      - viconsaga-network   # DB fica isolado, sem acesso ao NPM

networks:
  viconsaga-network:
    driver: bridge
  npm-ruiogawa_default:
    external: true   # rede já existe, apenas conecta

volumes:
  db_data:

4. Arquivos enviados pelo Filebrowser vêm como root:root

Sintoma: ao fazer upload do zip via Filebrowser e tentar unzip na VPS, erro:

error: cannot open zipfile [ viconsaga_vigiante.zip ]
       Permission denied

Causa: o Filebrowser roda dentro de container como root (UID 0), então arquivos enviados ficam com -rw-r----- root root. O usuário ubuntu não consegue ler nem executar.

Solução: ajustar permissões antes de usar:

sudo chown ubuntu:ubuntu /home/ubuntu/viconsaga_vigiante.zip
# ou, se preferir manter o dono:
sudo chmod 644 /home/ubuntu/viconsaga_vigiante.zip

Configuração do Proxy Host no NPM

Com o container na mesma rede do NPM, configurar o Proxy Host como:

Campo Valor
Domain Names vigiante.com, www.vigiante.com
Scheme http
Forward Hostname/IP vigiante-app-1 (nome do container)
Forward Port 80 (porta interna do Apache, não 8180)
Cache Assets opcional (recomendado ligado)
Block Common Exploits ligado
Websockets Support ligado

Importante: usar o nome do container, não IP. Docker atribui IPs dinamicamente; se o container for recriado o IP muda, mas o nome continua.

SSL/HTTPS

Na aba SSL do Proxy Host, solicitar certificado Let's Encrypt. O NPM faz renovação automática.

Ordem de execução no primeiro deploy

sudo su -
cd /opt
# extrair zip como vigiante
cd vigiante

# 1. Editar docker-compose.yml adicionando a rede externa do NPM (ver acima)
# 2. Editar connection.inc com a chave Google Maps
# 3. Subir
docker compose up -d --build

# Confirmar redes
docker inspect vigiante-app-1 | grep -A2 Networks

# Configurar Proxy Host no NPM via web UI e solicitar Let's Encrypt
# Acessar https://vigiante.com

Procedimentos de atualização

Depende do tipo de mudança. Saber qual caso se aplica economiza muito tempo.

Tabela de decisão

Situação Comando Tempo
Mudança só de código PHP/CSS/JS/HTML down && up -d ~5s
Mudança no Dockerfile ou pasta docker/ down && up -d --build ~30s (com cache) a 2min (sem cache)
Reset do banco de dados down -v && up -d --build ~30s + setup inicial
Reset total (raro, última opção) down -v && system prune -a && up -d --build vários minutos

Como os arquivos do projeto são montados como volume (./:/var/www/html), mudanças de código são visíveis imediatamente pelo container sem rebuild. --build só é necessário quando a imagem precisa ser reconstruída (mudança de extensões PHP, configs do Apache, etc.).

Subir uma nova versão na VPS preservando dados

Roteiro testado e validado:

# 1. Backup do banco por via das dúvidas
cd /opt/vigiante
sudo docker compose exec db mysqldump --no-tablespaces \
    -u viconsaga -psecret viconsaga \
    > ~/vigiante-backup-$(date +%Y%m%d-%H%M).sql
ls -lh ~/vigiante-backup-*.sql

# 2. Preservar docker-compose.yml customizado (rede externa do NPM)
cp /opt/vigiante/docker-compose.yml ~/docker-compose.yml.vps

# 3. Preservar connection.inc (chave Google Maps, credenciais)
cp /opt/vigiante/connection.inc ~/connection.inc.vps

# 4. Upload do novo zip via Filebrowser para /home/ubuntu/

# 5. Ajustar permissão do zip (Filebrowser sobe como root:root)
sudo chown ubuntu:ubuntu /home/ubuntu/viconsaga_vigiante.zip

# 6. Parar containers sem apagar volume (sem -v!)
cd /opt/vigiante
sudo docker compose down

# 7. Substituir arquivos
cd ~
unzip -o viconsaga_vigiante.zip
sudo rm -rf /opt/vigiante
sudo mv viconsaga_vigiante-master /opt/vigiante

# 8. Restaurar configs customizadas
sudo cp ~/docker-compose.yml.vps /opt/vigiante/docker-compose.yml
sudo cp ~/connection.inc.vps /opt/vigiante/connection.inc

# 9. Subir (com --build se Dockerfile mudou)
cd /opt/vigiante
sudo docker compose up -d --build

O volume vigiante_db_data é reanexado automaticamente porque mantém o mesmo nome — o banco continua com todos os dados.

Quando começar do zero

cd /opt/vigiante
sudo docker compose down -v        # -v apaga volume do banco
# (extrair novo zip como nos passos 4-8 acima, pulando o backup)
sudo docker compose up -d --build

Verificações pós-deploy

# Containers up
sudo docker ps | grep vigiante

# App conectado nas DUAS redes (interna + NPM)
sudo docker inspect vigiante-app-1 | grep -A2 Networks

# Apache responde
curl -I http://localhost:8180
# Esperado: HTTP/1.1 302 Found ... Location: site/home

# Banco tem dados (exemplo)
sudo docker compose exec db mysql --default-character-set=latin1 \
    -u viconsaga -psecret viconsaga \
    -e "SELECT COUNT(*) FROM formrecord;"

# Log rápido de erros recentes
sudo docker compose logs --tail=50 app 2>&1 | grep -iE "error|warning|fatal"

Restaurar backup (se der ruim)

cat ~/vigiante-backup-AAAAMMDD-HHMM.sql | \
    sudo docker compose exec -T db mysql -u viconsaga -psecret viconsaga

Questões pendentes e notas de manutenção

Ainda dependem de configuração externa

  • SMTP não configurado — funcionalidade "Esqueci minha senha" só opera com notifierSMTPHost, notifierEmail e notifierEmailPassword preenchidos em connection.inc
  • Street View e Static Maps — seguem endpoints Google proprietários que não estão entre as 3 APIs ativadas; chamadas falharão silenciosamente

Dívida técnica relevante

  • APIs Google Maps legacy (Marker, SearchBox) — manter monitoramento de deprecation
  • Bug only_full_group_by — padrão pode existir em outras queries além das 3 já corrigidas. A função selectFormRecordsTreeView tem outras 3 variantes comentadas com o mesmo problema; se alguém descomentar, vai quebrar.
  • Replacement chars residuais em comentários/strings PT-BR de engine/strings.php (invisíveis ao usuário, não corrigidos para evitar regressões)

Arquivos físicos não apagados

  • help/vicon-saga-mobile.pdf continua no disco mesmo após a remoção do link no menu (limpeza opcional)

Limpeza futura sugerida

  • Migrar para google.maps.marker.AdvancedMarkerElement (Marker deprecation Feb/2024)
  • Migrar de SearchBox para Places API (New) se houver reescrita (pode exigir refactor grande)
  • Revisar outras queries com GROUP BY para compatibilidade com modo estrito MySQL
  • Adicionar testes automatizados mínimos (hoje não há)

Credenciais e configuração sensível

Item Arquivo Observação
Chave Google Maps connection.inc Restringir por domínio
Senha DB viconsaga connection.inc + docker-compose.yml Trocar em produção
MYSQL_ROOT_PASSWORD docker-compose.yml Trocar em produção
SMTP (futuro) connection.inc Gmail App Password recomendado

Recomenda-se mover essas credenciais para variáveis de ambiente ou .env antes de colocar o código em repositório público.

Backups

  • Banco: sudo docker compose exec db mysqldump --no-tablespaces -u viconsaga -psecret viconsaga > backup.sql
  • Uploads: sudo tar czf uploads.tgz /opt/vigiante/{projects,tmp,sensors,support,archives}
  • Agendar via cron na VPS para rotação diária/semanal

Ícones customizados no mapa — bugs corrigidos

Dois bugs interdependentes impediam o uso de ícones customizados (enviados via upload para projects/<idProject>/) no mapa principal. Ícones do pacote padrão (img/markers/) funcionavam normalmente porque são pré-carregados no HTML da página antes do mapa renderizar.


Bug 1 — Ícones customizados invisíveis no mapa (home.php)

Arquivo: home.php, função mapPlotMarkers()

Sintoma: marcadores de formulários com ícone customizado não apareciam no mapa. Formulários com ícone padrão (img/markers/) funcionavam normalmente.

Causa: o código media as dimensões do ícone lendo img.width e img.height de um elemento <img> criado dinamicamente e imediatamente descartado. Para ícones do pacote padrão, o browser já os tem em cache (referenciados no HTML via <img src="..."> no menu de formulários), então retornam as dimensões reais. Para ícones customizados em projects/, que nunca são pré-carregados no HTML, o browser retorna 0 para ambas as dimensões, resultando em new google.maps.Size(0, 0) e ícone invisível.

Bug secundário no escalonamento: após substituir img.width/height por img.naturalWidth/naturalHeight, a fórmula de escalonamento proporcional também estava incorreta — intHeight era sobrescrito antes de ser usado no cálculo de intWidth, fazendo todos os ícones ficarem achatados horizontalmente:

// ERRADO — intHeight já é intIconMaxSize quando intWidth é calculado:
intHeight = intIconMaxSize;
intWidth  = Math.round(intIconMaxSize * intWidth / intHeight); // divide por intIconMaxSize!

Correção: usar naturalWidth/naturalHeight com fallback para intIconMaxSize, e calcular intWidth com as dimensões originais antes de sobrescrever intHeight:

// Em home.php — função mapPlotMarkers(), bloco "Get Marker Dimensions"
if (strMarkerFullPath != arrResult[i]['strMarkerFullPath']) {
    strMarkerFullPath = arrResult[i]['strMarkerFullPath'];
    var img = new Image();
    img.src = arrResult[i]['strMarkerFullPath'];
    var imgW = img.naturalWidth  || intIconMaxSize;
    var imgH = img.naturalHeight || intIconMaxSize;
    intWidth  = imgW;
    intHeight = imgH;
    if ((imgW > intIconMaxSize) || (imgH > intIconMaxSize)) {
        intWidth  = Math.round(intIconMaxSize * imgW / imgH); // usa imgH original
        intHeight = intIconMaxSize;
    }
}

Nota: img.naturalWidth/img.naturalHeight retornam as dimensões reais se a imagem já estiver em cache, ou 0 se ainda não tiver carregado. O fallback || intIconMaxSize garante um tamanho válido em qualquer caso, consistente com o comportamento do restante do código (setIcon, mapUpdateRecordIcon, etc.).


Bug 2 — Upload de ícone customizado falha silenciosamente (engine/form.php)

Arquivo: engine/form.php, função uploadFileMarker()

Sintoma: ao fazer upload de um ícone pela tela de Gerenciar Formulários, a interface indicava sucesso (barra de progresso completava, AJAX retornava 200), mas o ícone não aparecia na listagem e nenhum registro era criado no banco.

Causa: a função copiava o arquivo temporário para projects/<idProject>/<nomeAleatorio>.ext usando copy(), mas não verificava nem criava o diretório de destino. Se projects/<idProject>/ não existia (por exemplo, após um redeploy sem restauração dos uploads ou num projeto que nunca teve ícone personalizado), o copy() retornava FALSE silenciosamente e o INSERT INTO marker nunca era executado.

Correção: criar o diretório antes do copy() caso não exista:

// Em engine/form.php — função uploadFileMarker(), antes do copy()
$strDestinationDir = $this->strings->strProjectFolder . '/' . $idProject;
if (!is_dir($strDestinationDir)) {
    mkdir($strDestinationDir, 0775, true);
    chown($strDestinationDir, 'www-data');
}
if (copy($strSourceFileFullPath, $strDestinationFileFullPath) !== FALSE) {
    // ... INSERT INTO marker
}

Formatos aceitos para ícones de marker

Atributo Valor
Formatos aceitos JPG, JPEG, PNG, GIF (validado em UploadHandler.php e no JS de markers.php)
SVG ❌ Não aceito pelo uploader (accept_file_types restringe a raster)
Tamanho máximo em disco 10 MB (strings.php → intMaxploadFileSizeMB)
Resolução Sem restrição — sistema escala automaticamente para intMarkerSizePx do projeto
Recomendado PNG com fundo transparente (JPG funciona mas exibe fundo branco no mapa)

Recuperação de ícones perdidos após redeploy

Se arquivos de projects/ foram perdidos (container recriado com -v sem backup), os registros no banco continuam existindo mas apontam para arquivos inexistentes (404). Para resolver:

1. Identificar markers órfãos:

sudo docker compose exec db mysql --default-character-set=latin1 \
    -u viconsaga -psecret viconsaga \
    -e "SELECT idMarker, strMarkerFileName FROM marker WHERE idProject = X;"

2. Identificar quais formulários referenciam esses markers:

sudo docker compose exec db mysql --default-character-set=latin1 \
    -u viconsaga -psecret viconsaga \
    -e "SELECT f.idForm, f.strName, f.idMarker, m.strMarkerFileName
        FROM form f JOIN marker m ON m.idMarker = f.idMarker
        WHERE m.idProject = X;"

3. Deletar markers sem vínculo de formulário (não têm FK constraint):

DELETE FROM marker WHERE idProject = X AND strMarkerFileName IN ('arquivo.png', ...);

4. Para markers com vínculo: fazer upload do novo ícone pela interface, obter o novo idMarker, reatribuir os formulários e então deletar o marker antigo:

UPDATE form SET idMarker = NOVO_ID WHERE idForm IN (A, B, C);
DELETE FROM marker WHERE idProject = X AND strMarkerFileName IN ('arquivo_perdido.png');

Referências rápidas

Arquivos com alterações substanciais

  • site/home.php, site/sobre.php, site/projects.php, site/features.php, site/index.php, site/join.php, site/.htaccess
  • index.php (raiz), install.php, .htaccess (raiz)
  • help/index.php
  • ajax.php (conversor UTF-8 → ISO-8859-1 global)
  • engine/project.php (insertProject com senha custom)
  • engine/user.php (selectUserProjects — bug GROUP BY)
  • engine/formrecord.php (selectFormRecordData + selectFormRecordsTreeView — bugs GROUP BY)
  • engine/strings.php (arrGetImagePallete — suporte a SVG)
  • engine/importer.php (arrExtractCSVShapeObject — vírgula decimal)
  • engine/form.php (uploadFileMarker — criação automática do diretório do projeto)
  • home.php (mapPlotMarkers — leitura de dimensões de ícone customizado)
  • connection.inc
  • css/main.css (Bauhaus93 → Arial)
  • Dockerfile (extensões zip, gd, pastas de upload com permissão)
  • docker-compose.yml, docker/apache-vhost.conf

Assets novos ou substituídos

  • img/logo.svg, img/logo-symbol.svg, img/logofav.png
  • img/site/picaps-logo.png
  • img/site/team/{fernanda,jeferson,marcelo}.png, img/site/team/rui.jpeg
  • img/backgrounds/{1,1-150x150,1-small,1b,1b-150x150,vigiante-bg}.jpg
  • img/background-pattern-gray.png

Pastas criadas no build (com permissão 775 para www-data)

  • tmp/ (upload temporário)
  • projects/ (mídia por projeto)
  • sensors/, support/, archives/

Comandos úteis no dia-a-dia

# Logs do Apache
sudo docker compose logs -f app

# Logs do MySQL
sudo docker compose logs -f db

# Shell dentro do container app
sudo docker compose exec app bash

# MySQL CLI (com encoding correto pra ver acentos)
sudo docker compose exec db mysql --default-character-set=latin1 \
    -u viconsaga -psecret viconsaga

# Reiniciar apenas o app (sem recriar volume do DB)
sudo docker compose restart app

# Rebuild após alteração do Dockerfile
sudo docker compose up -d --build

# Ver redes conectadas ao container
sudo docker inspect vigiante-app-1 | grep -A2 Networks

# Conectar container em rede existente (caso do NPM)
sudo docker network connect npm-ruiogawa_default vigiante-app-1

# Reset de senha de usuário
sudo docker compose exec db mysql -u viconsaga -psecret viconsaga \
    -e "UPDATE user SET strPassword = MD5('novaSenha123') WHERE strEmail = 'x@y.com';"

# Corrigir texto com double-encoding UTF-8 (Saúde → Saúde)
sudo docker compose exec db mysql -u viconsaga -psecret viconsaga \
    -e "UPDATE project SET strName = CONVERT(CAST(CONVERT(strName USING BINARY) AS CHAR CHARACTER SET utf8) USING latin1) WHERE idProject = N;"

# Ajustar permissão de arquivo subido via Filebrowser
sudo chown ubuntu:ubuntu /home/ubuntu/arquivo.zip

# Verificar markers órfãos (arquivo existe no banco mas não no disco)
sudo docker compose exec db mysql --default-character-set=latin1 \
    -u viconsaga -psecret viconsaga \
    -e "SELECT m.idMarker, m.idProject, m.strMarkerFileName, f.idForm, f.strName
        FROM marker m LEFT JOIN form f ON f.idMarker = m.idMarker
        WHERE m.idProject = X;"

# Reatribuir marker de formulário
sudo docker compose exec db mysql -u viconsaga -psecret viconsaga \
    -e "UPDATE form SET idMarker = NOVO_ID WHERE idForm = Y;"