Vigiante — Documentação de Customização
Registro completo 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 8, infraestrutura Docker, deploy em VPS Oracle Cloud e funcionalidades novas.
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–Maio 2026
Sumário
- Identidade visual
- Conteúdo e textos
- Roteamento e URLs
- Encoding e caracteres inválidos
- Autenticação e criação de projeto
- Compatibilidade com MySQL 8
- Logos, paletas e SVG
- Importação de dados CSV e XLSX
- Upload de arquivos e permissões
- Exportação CSV
- Download de arquivos — output.php
- QR Code
- Página de compartilhamento de formulário
- Google Maps API
- Infraestrutura Docker
- Deploy em VPS Oracle Cloud
- Script de deploy automatizado
- Questões pendentes e notas de manutenção
Identidade visual
Cores
- Primária:
#00427e(azul Vigiante) - Gradiente navbar site público:
#002648 → #003a6e - Gradiente navbar admin (install.php):
#256aa0 → #3788c4 - Secundária / sucesso:
#008B44(verde) - Destaque logo:
#FF5B04(laranja)
Logos
img/logo.svg— Logo completa (símbolo + texto "Vigiante" laranja)img/logo-symbol.svg— Símbolo sem texto, usado como default de projetos sem logoimg/logofav.png— Favicon 512×512
Backgrounds
Originais substituídos por gradientes claros neutros:
- img/backgrounds/1.jpg, 1-150x150.jpg, 1-small.jpg, 1b.jpg, 1b-150x150.jpg
- img/background-pattern-gray.png
- img/backgrounds/vigiante-bg.jpg — novo background default
Alteração em engine/project.php:
public $strProjectImageNoLogo = 'img/logo-symbol.svg';
public $strProjectImageNoBackground = 'img/backgrounds/vigiante-bg.jpg';
Fontes
Removida a Bauhaus93 de todos os títulos. Substituída por Arial, Helvetica, sans-serif.
css/main.css:
.project-title {
font-family: Arial, Helvetica, sans-serif;
font-size: 18px;
font-weight: 600;
letter-spacing: 0;
}
site/projects.php:
.project-info .title {
font-family: Arial, Helvetica, sans-serif;
font-size: 15px;
font-weight: 600;
}
Página install.php (administração)
CSS para distinguir visualmente da área pública:
.navbar-inner {
background-color: #2d7bb6 !important;
background-image: linear-gradient(to bottom, #256aa0, #3788c4) !important;
}
Conteúdo e textos
Página Sobre (site/sobre.php)
Criada do zero com: histórico institucional, contato, equipe Vicon SAGA (Jorge Xavier, Tiago Marino, Gabriel Lousada) e 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.
Home (site/home.php)
3 seções: logo PICAPS + "Nossa Atuação" + "Territórios Saudáveis, Sustentáveis e Solidários". Fundo branco uniforme em todas as seções.
Ajuda (help/index.php)
- Removido: "Vicon SAGA Mobile" (menu + submenu)
- Renomeado: "Criação e Consulta Vicon Web" → "Criação e Consulta"
Footer (index.php)
- Replacement chars (
EF BF BD) →•e× - Item "Mobile" removido
Páginas removidas
contact.php e download.php (obsoletas)
Roteamento e URLs
.htaccess raiz
# Desativado (mascarava erros 404 como home)
#ErrorDocument 404 /site/home
# Adicionado: URLs /site/ bypassam regras da raiz
RewriteRule ^site/ - [L]
# QR Code público
RewriteRule ^qr/([0-9]+)$ qr.php?idForm=$1 [L,NC]
site/.htaccess
Adicionada rota ^sobre$ e catch-all:
RewriteRule ^sobre$ index.php?s=sobre [L,NC]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^([a-zA-Z0-9_-]+)$ index.php?s=$1 [L,NC,QSA]
Router site/index.php
Trocado file_exists($_GET['s'].'.php') por file_exists(__DIR__.'/'.$_GET['s'].'.php'). Adicionado fallback via parse_url() quando $_GET['s'] não chega.
Encoding e caracteres inválidos
connection.inc
Convertido de CR (Mac Classic) para LF — PHP lia o arquivo todo como uma linha.
Conversor global UTF-8 → ISO-8859-1 em ajax.php
Browsers enviam UTF-8; o sistema é ISO-8859-1. Sem conversão, "Saúde" virava "Saúde" no banco.
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);
Adicionado no topo do ajax.php, antes de qualquer handler.
Visualização correta no terminal MySQL
O banco armazena em ISO-8859-1. Para ver acentos corretamente no terminal:
sudo docker compose exec db mysql --default-character-set=latin1 \
-u viconsaga -psecret viconsaga
Correção de dados com double-encoding
Para registros gravados antes do fix:
UPDATE project
SET strName = CONVERT(CAST(CONVERT(strName USING BINARY) AS CHAR CHARACTER SET utf8) USING latin1)
WHERE idProject = N;
Autenticação e criação de projeto
Problema original
Sistema gerava senha aleatória de 4 chars e enviava por SMTP. Sem SMTP, usuário ficava sem acesso.
Solução
Usuário define a própria senha no formulário de criação (site/join.php):
- Mínimo 8 caracteres, pelo menos 1 letra e 1 número, confirmação obrigatória
- Validação client-side antes do AJAX
- Senha não mais enviada em texto puro por e-mail
- sendMail() envolvida em @ para falhar silenciosamente
engine/project.php — insertProject()
public function insertProject($strEmail, $strName, $strAlias,
$blnSendNotificationMail = true, $blnCreateForArchiveRestore = false,
$strUserPassword = '') {
$strPassword = ($strUserPassword !== '' && $strUserPassword !== null)
? $strUserPassword
: $this->strings->generateRandomString(8);
}
Reset manual de senha
sudo docker compose exec db mysql -u viconsaga -psecret viconsaga \
-e "UPDATE user SET strPassword = MD5('novaSenha123') WHERE strEmail = 'x@y.com';"
Compatibilidade com MySQL 8
MySQL 8 ativa only_full_group_by por padrão. Queries com GROUP BY incompleto falhavam silenciosamente, causando telas vazias ou funcionalidades quebradas.
Bug 1 — Projetos não aparecem para usuário logado
Arquivo: engine/user.php → selectUserProjects()
Sintoma: página /site/projects vazia quando logado.
Correção: alias u → u2 nas subqueries, = → IN, join explícito, GROUP BY removido.
Bug 2 — Campos de múltipla escolha ausentes no popup
Arquivo: engine/formrecord.php → selectFormRecordData()
Sintoma: ao clicar num ponto, campos como "Categoria: Público" não apareciam.
Correção: removido GROUP BY frfa.idFormRecordField (dados já únicos pela combinação de chaves).
Bug 3 — Lista lateral de registros não expande
Arquivo: engine/formrecord.php → selectFormRecordsTreeView()
Sintoma: lateral mostrava só o nó pai do formulário com contagem, sem registros individuais.
Correção: GROUP BY fr.idFormRecord substituído por SELECT externo com ANY_VALUE():
SELECT idFormRecord, ANY_VALUE(idShapeType) AS idShapeType, ANY_VALUE(strValue) AS strValue, ...
FROM (SELECT ... ORDER BY ... LIMIT 500) t
GROUP BY idFormRecord
Bug 4 — Exportação CSV retorna só cabeçalho
Arquivo: engine/formrecord.php → selectFormRecords()
Causa: filtro de data comparava DATE(fr.dtLastUpdate) >= '2026-04-20T22:09:58'. MySQL converte o lado esquerdo (DATE) para DATETIME adicionando 00:00:00, resultando em '2026-04-20 00:00:00' >= '2026-04-20 22:09:58' = FALSE — excluindo todos os registros.
Correção:
" . ($dtDateFrom ? " AND DATE(fr.dtLastUpdate) >= '" . substr($dtDateFrom, 0, 10) . "'" : "") . "
" . ($dtDateTo ? " AND DATE(fr.dtLastUpdate) <= '" . substr($dtDateTo, 0, 10) . "'" : "") . "
Padrão de diagnóstico
Se uma tela aparecer vazia sem erro visível, verificar logs do Apache por Error 1055 e procurar GROUP BY na query correspondente:
sudo docker compose logs --tail=100 app 2>&1 | grep -iE "1055|group by"
Logos, paletas e SVG
Bug: SVG causa fatal que trava o botão Salvar
Causa encadeada:
1. strProjectImageNoLogo = 'img/logo-symbol.svg'
2. arrGetImagePallete() em engine/strings.php só suporta GIF/JPEG/PNG
3. Com SVG: imagecopyresampled(null) → fatal error PHP
4. HTML de admManageProject.php truncado antes de div_permissions
5. projectUpdate() no JS tenta .checked de 7 elementos inexistentes → TypeError → botão preso em "Aguarde"
Correção em engine/strings.php:
if (! file_exists($strFileName)) {
return array(str_replace('#', '', $this->strSiteBaseColor) => 1);
}
$size = @GetImageSize($strFileName);
if (! $size || ! in_array($size[2], array(1, 2, 3))) {
return array(str_replace('#', '', $this->strSiteBaseColor) => 1);
}
Importação de dados CSV e XLSX
Bug CSV — vírgula decimal vira zero
Causa: o parser protege vírgulas dentro de aspas substituindo por pipe | (evitar quebra no explode). Depois, str_replace(",",".") não converte o pipe. "-29|1728559" não é numérico → coordenada vira 0 → ponto vai para 0°, 0° (costa africana).
Correção em engine/importer.php → arrExtractCSVShapeObject():
$lat = trim($lat, " \t\n\r\0\x0B\"'");
$lng = trim($lng, " \t\n\r\0\x0B\"'");
$lat = str_replace(array('|', ','), '.', $lat);
$lng = str_replace(array('|', ','), '.', $lng);
Bug XLSX — tela branca
Causa: XLSX é ZIP internamente. Extensão PHP zip não instalada na imagem base.
Log:
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:
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
Upload de arquivos e permissões
Bug — fotos não salvas
Sintoma: barra de progresso completa, "Salvo" aparece, mas "Arquivos: 0".
Causa: pastas tmp/, projects/, sensors/, support/, archives/ não criadas com permissão para www-data.
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
Observação importante: o volume mount ./:/var/www/html no docker-compose.yml sobrescreve as permissões do Dockerfile para pastas que existem no host. Isso afeta funcionalidades que precisam escrever em tmp/ (como QR code). Ver seção QR Code para a solução adotada.
Exportação CSV
Fluxo do sistema
- JS chama
RCSV(por lote de 500 registros) → gera arquivo temporário com dados - JS chama
RCSVM→ merge dos arquivos temporários + adiciona cabeçalho → retorna link - JS chama
output.php→ faz download do arquivo final
Bugs corrigidos
print $file; fantasma em engine/report.php → reportCSVMerge():
// ANTES (bug):
while ($line = fgets($in)) {
print $file; // variável inexistente, corrompia o buffer
fwrite($out, $line);
}
// DEPOIS:
while ($line = fgets($in)) {
fwrite($out, $line);
}
Variável errada em generateReportCSV():
// ANTES (bug — $arrFormsFields undefined):
$arrFormRecordsDatas = $this->formrecord->selectFormRecordData($arrIDs, $arrFormsFields, false);
// DEPOIS — array() vazio força auto-carregamento com índice idForm correto:
$arrFormRecordsDatas = $this->formrecord->selectFormRecordData($arrIDs, array(), false);
Encoding do CSV: adicionado BOM UTF-8 e conversão ISO-8859-1 → UTF-8 para o Excel abrir sem caracteres quebrados:
fwrite($out, "\xEF\xBB\xBF"); // BOM
fwrite($out, mb_convert_encoding($strHeader, 'UTF-8', 'ISO-8859-1'));
// Para cada linha de dados:
fwrite($out, mb_convert_encoding($line, 'UTF-8', 'ISO-8859-1'));
Download de arquivos — output.php
Bug — erros de header e buffer
Sintoma:
Notice: ob_end_clean(): failed to delete buffer. No buffer to delete in output.php on line 2
Warning: Cannot modify header information - headers already sent
Causa: arquivo iniciava com <? (short open tag). ob_end_clean() gerava Notice quando não havia buffer, que era impresso antes dos header().
Correção:
<?php
while (ob_get_level() > 0) ob_end_clean(); // sem Notice se não há buffer
$_GET['strFileName'] = trim($_GET['strFileName']);
$ext = explode(".", $_GET['strFileName']);
$strExt = isset($ext[1]) ? $ext[1] : '';
// ... headers conforme extensão ...
$fh = fopen($_GET['strFileName'], "rb");
if ($fh) { fpassthru($fh); fclose($fh); }
QR Code
Problema original
Clique no ícone QR fazia download direto sem mostrar a imagem. Múltiplos bugs encadeados foram corrigidos iterativamente.
Bug 1 — 403 do WAF (Nginx Proxy Manager)
O parâmetro strURL=http://vigiante.com/... na query string era bloqueado pela regra "Block Common Exploits" do NPM (interpretado como SSRF).
Solução: o servidor constrói a URL internamente pelo idForm, eliminando o parâmetro da query string.
Bug 2 — QRcode::png() contamina o buffer
O último parâmetro true em QRcode::png($url, $file, 8, 8, true) faz "save AND print", enviando o PNG binário para o output buffer antes do JSON. JSON.parse recebia [PNG binário]{"strFilePathTMP":"..."} e falhava.
Solução: usar QRcode::text() que retorna o frame sem gerar output, e construir a imagem GD manualmente:
$frame = QRcode::text($strQRURL, false, QR_ECLEVEL_H, 8, 4);
if ($frame) {
$intPix = 8; $intMargin = 4;
$h = count($frame); $w = strlen($frame[0]);
$imgW = $w + 2*$intMargin; $imgH = $h + 2*$intMargin;
$baseImg = imagecreate($imgW, $imgH);
$colWhite = imagecolorallocate($baseImg, 255, 255, 255);
$colBlack = imagecolorallocate($baseImg, 0, 0, 0);
imagefill($baseImg, 0, 0, $colWhite);
for ($y=0; $y<$h; $y++)
for ($x=0; $x<$w; $x++)
if ($frame[$y][$x]=='1') imagesetpixel($baseImg, $x+$intMargin, $y+$intMargin, $colBlack);
$imgQRCode = imagecreate($imgW*$intPix, $imgH*$intPix);
imagecopyresized($imgQRCode, $baseImg, 0, 0, 0, 0, $imgW*$intPix, $imgH*$intPix, $imgW, $imgH);
imagedestroy($baseImg);
// Capturar PNG em memória
ob_start(); imagepng($imgQRCode); $strPNGFinal = ob_get_clean();
imagedestroy($imgQRCode);
$strBase64 = 'data:image/png;base64,' . base64_encode($strPNGFinal);
@file_put_contents($arrResult['strFilePathTMP'], $strPNGFinal);
}
Nível QR_ECLEVEL_H (máxima redundância) é necessário quando há overlay no centro.
Bug 3 — encodeArray() corrompe o base64
encodeArray() aplica utf8_encode() em todos os valores, corrompendo o base64. Solução: separar o base64 do array antes de encodificar:
$arrEncoded = $strings->encodeArray($arrResult);
$arrEncoded['strBase64'] = $strBase64; // adicionado DEPOIS do encodeArray
echo json_encode($arrEncoded);
Bug 4 — Permissão de escrita em tmp/ (volume Docker)
O volume mount ./:/var/www/html sobrescreve as permissões do Dockerfile. www-data não consegue escrever em tmp/ via filesystem do host. A abordagem em memória (sem disco) resolve o problema para exibir o QR. O @file_put_contents tenta salvar para o download, falhando silenciosamente se não conseguir.
Bug 5 — Modal com height 0 (HTML aninhado incorretamente)
O modal #modalQRCode estava dentro do #modalEmbed por falta de </div> de fechamento. Com height 0, era invisível mesmo com display:block.
Modal atual (index.php)
<!-- Modal QR Code -->
<div class="modal hide fade hideSelection" id="modalQRCode"
style="width:320px;left:50%;transform:translateX(-50%);margin-left:0;border-radius:8px">
<div class="modal-body" style="text-align:center;padding:16px 20px 8px">
<button type="button" class="close" data-dismiss="modal"
style="position:absolute;top:8px;right:12px;font-size:20px">×</button>
<p id="pQRCodeLabel" style="font-weight:600;margin:0 0 10px;font-size:15px;color:#00427e"></p>
<div id="divQRCodeLoading">...</div>
<img id="imgQRCode" src="" style="display:none;width:260px;height:260px">
<div>
<a href="#" class="btn" data-dismiss="modal">Fechar</a>
<a id="btnQRCodeDownload" class="btn btn-success" style="display:none" onclick="qrCodeDownload()">Download</a>
</div>
</div>
</div>
O transform:translateX(-50%) centraliza em qualquer largura de tela (desktop e celular).
Função JS (js/main.js)
function qrCode(strURL, strLabel, strFormName) {
strQRCodeFilePath = '';
document.getElementById('pQRCodeLabel').innerHTML = strFormName || strLabel;
// ... mostra modal com loading ...
$.ajax({ url: 'ajax.php?chrAction=HQRC&strLabel=' + encodeURIComponent(strLabel), dataType: 'json' })
.done(function(arrResult) {
if (arrResult['strBase64']) {
document.getElementById('imgQRCode').src = arrResult['strBase64'];
// ... mostra imagem e botão Download ...
}
});
}
Página pública de QR para celular (qr.php)
Acessível sem autenticação em vigiante.com/qr/{idForm}. Exibe o QR em tela cheia (90vw × 90vh) para ser lido por outro celular. Rota adicionada no .htaccess.
Página de compartilhamento de formulário
Problema — layout quebrado em vigiante.com/share/...
Causa: strSiteURL em engine/strings.php usava REQUEST_SCHEME para detectar HTTPS. O Nginx Proxy Manager faz proxy via HTTP internamente, então REQUEST_SCHEME = 'http' dentro do container. Todos os assets (Bootstrap 4 JS, CSS) carregavam com http:// em página HTTPS → Mixed Content → navegador bloqueava JS → tabs sem funcionar, layout quebrado.
Correção em engine/strings.php:
$strHttp = 'http';
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https') {
$strHttp = 'https';
} elseif (isset($_SERVER['REQUEST_SCHEME']) && strtolower($_SERVER['REQUEST_SCHEME']) === 'https') {
$strHttp = 'https';
} elseif (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
$strHttp = 'https';
}
O NPM envia o header X-Forwarded-Proto: https, que agora é verificado primeiro.
Google Maps API
Configuração do projeto no Google Cloud Console
- Criar projeto:
vigiante-php-picaps - Ativar APIs em APIs & Services → Library:
- Maps JavaScript API
- Places API (clássica, NÃO a "(New)")
- Geocoding API
- Maps Static API (necessária para relatório HTML com miniaturas)
- Habilitar billing (US$ 200/mês de crédito gratuito)
- Criar API Key e restringir por domínio
Configuração da chave
// connection.inc
$strGoogleAPIKey = 'AIzaSy...';
Atenção em updates: connection.inc não está no zip. Sempre preservar antes do deploy:
cp /opt/vigiante/connection.inc ~/connection.inc.vps
Erros típicos
| Erro | Causa | Solução |
|---|---|---|
| Marca d'água "For development purposes only" | Chave sem billing | Habilitar billing |
InvalidKeyMapError |
Chave com typo | Verificar connection.inc |
RefererNotAllowedMapError |
URL não listada | Adicionar domínio nas restrições |
LegacyApiNotActivatedMapError |
Places API clássica não ativada | Ativar Places API (sem "(New)") |
| Miniaturas em branco no relatório HTML | Maps Static API não ativada | Ativar Maps Static API |
Infraestrutura Docker
Dockerfile (versão final)
FROM php:7.4-apache
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
RUN a2enmod rewrite
COPY docker/apache-vhost.conf /etc/apache2/sites-available/000-default.conf
RUN a2ensite 000-default
COPY . /var/www/html
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"]
Observação: as permissões de pastas definidas no Dockerfile valem apenas para arquivos do contexto de build. Pastas montadas via volume (./:/var/www/html) usam as permissões do host.
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:
Deploy em VPS Oracle Cloud
Armadilhas encontradas
1. Docker rootless vs Docker tradicional
Diagnóstico: sudo ss -tlnp | grep 8180 — se aparecer rootlesskit, está no Docker errado.
Solução: mover para /opt/ e usar sudo docker compose.
2. Oracle Cloud só roteia 80 e 443 externamente
Mesmo com Security List liberando outras portas, tráfego externo só flui por 80/443. Todos os serviços devem ser publicados via NPM.
3. Redes Docker isoladas (NPM não enxerga Vigiante)
Solução: adicionar rede do NPM como externa no docker-compose.yml:
services:
app:
networks:
- viconsaga-network
- npm-ruiogawa_default
db:
networks: [viconsaga-network]
networks:
viconsaga-network:
driver: bridge
npm-ruiogawa_default:
external: true
4. Filebrowser envia arquivos como root:root
sudo chown ubuntu:ubuntu ~/viconsaga_vigiante.zip
Configuração do Proxy Host no NPM
| Campo | Valor |
|---|---|
| Domain Names | vigiante.com, www.vigiante.com |
| Scheme | http |
| Forward Hostname/IP | vigiante-app-1 (nome do container) |
| Forward Port | 80 (Apache interno, NÃO 8180) |
| Block Common Exploits | Ligado |
| Websockets Support | Ligado |
Script de deploy automatizado
Arquivo: deploy-vigiante.sh (disponível junto com o zip).
Configuração inicial (única vez)
sudo chown ubuntu:ubuntu ~/deploy-vigiante.sh
chmod +x ~/deploy-vigiante.sh
Uso
# Só código mudou (maioria dos casos):
sudo bash ~/deploy-vigiante.sh
# Dockerfile ou pasta docker/ mudou:
sudo bash ~/deploy-vigiante.sh --build
O que o script faz automaticamente
- Verifica que o zip existe em
/home/ubuntu/viconsaga_vigiante.zip - Backup do banco em
/home/ubuntu/vigiante-backups/db-TIMESTAMP.sql - Salva
docker-compose.ymleconnection.inccom timestamp - Para containers (
downsem-v— banco preservado) - Ajusta permissão do zip (
chown ubuntu:ubuntu) - Extrai e substitui o código
- Restaura
docker-compose.ymleconnection.inc - Sobe containers (com
--buildse solicitado) - Verifica containers, redes e resposta HTTP
- Mantém últimos 10 backups, remove os mais antigos
O que o script preserva
- Banco de dados: nunca apagado
docker-compose.yml: restaurado com redes do NPMconnection.inc: restaurado com chave Google Maps e credenciais
Questões pendentes e notas de manutenção
Dependem de configuração externa
- SMTP: "Esqueci minha senha" só funciona com
notifierSMTPHost,notifierEmailenotifierEmailPasswordconfigurados emconnection.inc - Maps Static API: necessária para miniaturas no relatório HTML — habilitar no Google Cloud Console
Dívida técnica
- APIs Google Maps legacy (
Marker,SearchBox) — monitorar deprecation - Bug
only_full_group_by— pode existir em outras queries além das 4 corrigidas - Encoding ISO-8859-1 — sistema legado; migração para UTF-8 seria refactor grande
Credenciais sensíveis
| 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 |
Backups
# Banco
sudo docker compose exec db mysqldump --no-tablespaces \
-u viconsaga -psecret viconsaga > backup.sql
# Uploads (fotos, anexos)
sudo tar czf uploads.tgz /opt/vigiante/{projects,tmp,sensors,support,archives}
Referências rápidas
Arquivos com alterações substanciais
ajax.php— conversor UTF-8→ISO-8859-1, handler QR (HQRC), handler CSV (RCSV/RCSVM)output.php— fix de headers e bufferengine/strings.php— arrGetImagePallete (SVG), detecção X-Forwarded-Protoengine/formrecord.php— 3 bugs GROUP BY + filtro de dataengine/user.php— selectUserProjects GROUP BYengine/report.php— generateReportCSV, reportCSVMerge, encoding UTF-8engine/importer.php— arrExtractCSVShapeObject (vírgula decimal, XLSX)engine/project.php— insertProject com senha customengine/form.php— strShareURLsite/join.php,site/projects.php,site/sobre.php,site/home.phpindex.php— modal QR, roteamento, footerqr.php— página pública QR (arquivo novo)js/main.js— função qrCode, qrCodeDownload.htaccess— rota/qr/Ncss/main.css— fontesDockerfile— extensões zip, gd, permissõesdocker-compose.yml,docker/apache-vhost.confdeploy-vigiante.sh— script de deploy (arquivo novo)
Comandos úteis
# Logs Apache em tempo real
sudo docker compose logs -f app
# MySQL CLI com encoding correto
sudo docker compose exec db mysql --default-character-set=latin1 \
-u viconsaga -psecret viconsaga
# Ver redes do container
sudo docker inspect vigiante-app-1 | grep -A2 Networks
# Verificar resposta HTTP
curl -I http://localhost:8180
# Logs de erro recentes
sudo docker compose logs --tail=50 app 2>&1 | grep -iE "error|warning|fatal"
Backup automatizado — Backblaze B2
Por que Backblaze B2
O Backblaze B2 foi escolhido em vez do Google Drive por duas razões principais: a Application Key não expira (diferente do OAuth do Drive que precisa de renovação periódica e browser para reautenticar), e o custo é muito menor para armazenamento crescente (10 GB gratuitos permanentes, depois US$ 0,006/GB/mês).
Estrutura no B2
vigiante-backups/
db/
db-YYYYMMDD-HHMM.sql.gz ← dump completo compactado do banco
files/
current/ ← espelho completo do /opt/vigiante (incremental)
snapshots/YYYYMMDD-HHMM/ ← versões anteriores de arquivos alterados/deletados
Configuração inicial (única vez)
1. Criar conta e bucket no Backblaze:
- Acessa https://www.backblaze.com → criar conta gratuita
- B2 Cloud Storage → Buckets → Create a Bucket
- Nome: vigiante-backups | Files: Private
- Account → Application Keys → Add a New Application Key
- Nome: vigiante-rclone | Bucket: vigiante-backups | Permissions: Read and Write
- Anotar keyID e applicationKey — aparecem só uma vez
2. Instalar e configurar rclone na VPS:
curl https://rclone.org/install.sh | sudo bash
rclone config
# → n → Name: b2 → Storage: b2
# → account: SEU-keyID
# → key: SEU-applicationKey
# → demais: Enter
# Copiar config para root (necessário porque os scripts rodam com sudo)
sudo mkdir -p /root/.config/rclone
sudo cp ~/.config/rclone/rclone.conf /root/.config/rclone/rclone.conf
# Testar
sudo rclone lsd b2:vigiante-backups
3. Instalar os scripts:
# Fazer upload de backup-vigiante.sh e restore-vigiante.sh via Filebrowser
sudo chown ubuntu:ubuntu ~/backup-vigiante.sh ~/restore-vigiante.sh
chmod +x ~/backup-vigiante.sh ~/restore-vigiante.sh
# Teste manual
sudo bash ~/backup-vigiante.sh
4. Agendar no cron:
sudo crontab -e
# Adicionar:
0 3 * * * sudo bash /home/ubuntu/backup-vigiante.sh >> /var/log/vigiante-backup.log 2>&1
O que é incluído no backup
O script sincroniza todo /opt/vigiante — código PHP, connection.inc, docker-compose.yml, uploads, imagens de marcadores, etc.
Exclusões (regeneráveis ou temporários):
- .git/
- engine/qrcode/cache/
- tmp/*.png e tmp/*.jpg
- *.log
O banco de dados recebe tratamento separado via mysqldump compactado com gzip -9, garantindo consistência transacional (--single-transaction).
Comportamento incremental
Na primeira execução: envia tudo (~110 MB no caso do Vigiante/PICAPS).
Nas execuções seguintes: envia apenas o que mudou (checksum SHA1). Arquivos deletados ou alterados no servidor são preservados em snapshots/TIMESTAMP/ em vez de serem removidos do B2.
Como restaurar
Usar o script interativo restore-vigiante.sh:
sudo bash ~/restore-vigiante.sh
Menu com 5 opções:
1. Banco de dados — lista os dumps disponíveis, escolhe um, restaura via mysql
2. Aplicação completa — sincroniza current/ de volta para /opt/vigiante
3. Arquivo específico de snapshot — restaura uma versão anterior de um arquivo
4. Tudo — banco + aplicação
5. Listar backups — mostra dumps, snapshots e uso total no B2
Restauração manual do banco:
rclone copy "b2:vigiante-backups/db/db-YYYYMMDD-HHMM.sql.gz" /tmp/
gunzip -c /tmp/db-YYYYMMDD-HHMM.sql.gz | \
sudo docker compose -f /opt/vigiante/docker-compose.yml exec -T db \
mysql -u viconsaga -psecret viconsaga
Observações
- O script mantém por padrão 2 dumps do banco e 2 snapshots de arquivos. Ajustável via
KEEP_DB_COPIESeKEEP_SNAPSHOTSno topo dobackup-vigiante.sh. - Verificar o log periodicamente:
tail -50 /var/log/vigiante-backup.log - Se o
sudo rclonefalhar com autenticação, re-copiar a config:sudo cp ~/.config/rclone/rclone.conf /root/.config/rclone/rclone.conf
Remoção do botão "Cadastre-se"
Arquivo: index.php
Quando o usuário não estava logado e acessava a página de um projeto, aparecia um botão verde "Cadastre-se" na navbar. Esse botão abria um modal de cadastro inline (userJoinShow()) que foi desativado pois o fluxo correto de cadastro de novos projetos é via https://vigiante.com/site/join.
Correção: removido o bloco condicional que renderizava o botão:
<!-- removido -->
<?php if (($arrProject['intAllowJoinAsLevel'] > 0) && ($_SESSION['idUser'] <= 0)) { ?>
<a class="btn btn-success" onClick="userJoinShow();">Cadastre-se</a>
<?php } ?>
O modal e as funções JS userJoinShow() / userJoinSubmit() foram mantidos no código por ora (não causam problema, apenas ficam inacessíveis pela interface).