docs: adiciona Deploy-V.1.0.4 — nomes de sistema e cliente congelados

VPS 4 Deploy Agent
2026-05-31 01:22:33 +02:00
parent 6c29b5dac9
commit 11eb76c1d7
2 changed files with 323 additions and 3 deletions
+319
@@ -0,0 +1,319 @@
# 🚀 Deploy V.1.0.4 — Nomes de Sistema e Cliente Congelados
> Versão **1.0.4** — inclui tudo da 1.0.3 e formaliza a **regra de nomes congelados**:
> o nome do sistema (`Score Client`) e o nome do executável do cliente Windows
> (`ScoreOdonto Dental Client Setup X.Y.Z.exe`) são **fixos e imutáveis** —
> apenas o número de versão pode mudar.
---
## 📋 Índice
1. [Arquitetura Multi-VPS](#1-arquitetura-multi-vps)
2. [Pipeline GitOps (Servidor — Modelo A)](#2-pipeline-gitops-modelo-a)
3. [O Listener do Webhook](#3-o-listener-do-webhook)
4. [Versionamento — Servidor](#4-versionamento--servidor)
5. [Cliente Windows — Build, Publicação e Auto-Update](#5-cliente-windows--build-publicacao-e-auto-update)
6. [🔒 Nomes Congelados — Regra Obrigatória](#6-nomes-congelados--regra-obrigatória)
7. [Erros comuns e soluções](#7-erros-comuns-e-soluções)
8. [Troubleshooting](#8-troubleshooting)
9. [Changelog](#9-changelog)
---
## 1. Arquitetura Multi-VPS
| Servidor | IP (VPN) | Função |
|---|---|---|
| **VPS 1 — App** | `10.99.0.1` | Produção (Docker rootless) + serve `/updates/` do cliente |
| **VPS 3 — Data/Git** | `10.99.0.3` | Gitea + PostgreSQL |
| **VPS 4 — Builder** | `10.99.0.4` | Build do servidor **e** do cliente Windows (cross-compile) |
| **Wasabi** | S3-compatível | Storage de imagens (não no disco) |
---
## 2. Pipeline GitOps (Modelo A)
```
git push origin main (rx.scoreodonto.com)
Gitea dispara webhook → VPS4:9003/webhook
listener-rx.js → deploy-prod.sh
VPS4: build React (Vite) → docker compose build app → docker save → app.tar.gz
VPS4: rsync app.tar.gz + docker-compose.yml + version.txt → VPS1
VPS1: docker load + docker compose up -d --no-deps app
✅ Deploy concluído (~30-40 s)
```
> ⚠️ O build do React roda com `NODE_ENV=development npm install --include=dev` porque
> o `vite` é devDependency e o listener roda com `NODE_ENV=production` (que pula devDeps).
---
## 3. O Listener do Webhook
**Arquivo:** `/home/deploy/stack/webhook-listener/listener-rx.js` (porta 9003)
- Valida token (`x-gitea-token` ou `?token=...`)
- **Trava de concorrência** (`isBuilding`) + **anti-loop** (ignora `chore(deploy): bump version`)
- Dispara `deploy-prod.sh` e reporta status ao Gitea (pending → success/failure)
> 📌 **O cliente Windows NÃO tem webhook.** O `deploy-client.sh` é **manual** (ver seção 5).
> Essa é a causa nº 1 de defasagem de versão do cliente.
---
## 4. Versionamento — Servidor
**Fonte de verdade:** `dental-server/version.txt` (lido em runtime, injetado no sidebar).
`package.json` **nunca** é tocado no bump (preserva cache do Docker). Detalhes na 1.0.2.
```bash
# Versão servida (dentro do container é a fonte de verdade):
ssh deploy@10.99.0.1 "docker exec rx_scoreodonto_app cat /app/version.txt"
curl -s https://rx.scoreodonto.com/ | grep -o 'v[0-9]\.[0-9]\.[0-9]'
```
---
## 5. Cliente Windows — Build, Publicação e Auto-Update
Projeto: **`rx.scoreodonto.com-client`** (Electron + electron-updater).
### 5.1. Fonte de verdade
Tudo parte do `package.json` do cliente:
```bash
grep '"version"' /home/deploy/stack/rx.scoreodonto.com-client/package.json
# "version": "1.2.7"
```
O `electron-builder` lê esse número e gera **3 artefatos** por versão:
`ScoreOdonto Dental Client Setup X.Y.Z.exe`, `...exe.blockmap` e `latest.yml`.
### 5.2. As DUAS pastas (e por que confundem)
| Pasta | Onde | O que é |
|---|---|---|
| `rx.scoreodonto.com-client/dist/` | **VPS4** | **Saída do build.** Tem todos os `.exe` já gerados. |
| `rx.scoreodonto.com/dental-server/updates/` | **VPS1** | **O que é servido** em `https://rx.scoreodonto.com/updates/`. É o que as clínicas enxergam. |
> 🔑 **Buildar (gerar no `dist/`) ≠ publicar (copiar para `updates/` na VPS1).**
> O cliente só "existe" para as clínicas quando o `latest.yml` da **VPS1** aponta para a nova versão.
### 5.3. Ciclo completo — `deploy-client.sh`
```bash
cd /home/deploy/stack/rx.scoreodonto.com-client
bash deploy-client.sh
```
O script faz, em ordem:
```
1. npm install
2. npx electron-builder --win --x64 --publish never → gera dist/*.exe + dist/latest.yml (VPS4)
3. valida que dist/ tem .exe e latest.yml
4. rsync dist/*.exe dist/*.yml dist/*.blockmap → VPS1:/.../dental-server/updates/ ← PUBLICAÇÃO
```
**Só termina o trabalho quando o passo 4 roda.** Se você buildar manualmente
(`npm run dist`) e parar aí, o `dist/` na VPS4 fica novo, mas a VPS1 continua na versão antiga.
### 5.4. Fluxo de auto-update nas clínicas
```
App do cliente inicia
electron-updater: GET https://rx.scoreodonto.com/updates/latest.yml
versão do yml > versão instalada? → baixa o .exe e instala ao reiniciar
→ senão, nada acontece
```
### 5.5. Verificação rápida (3 pontos têm que bater)
```bash
# 1. Fonte (código):
grep '"version"' /home/deploy/stack/rx.scoreodonto.com-client/package.json
# 2. Publicado na VPS1 (o que conta):
ssh deploy@10.99.0.1 "head -1 /home/deploy/stack/rx.scoreodonto.com/dental-server/updates/latest.yml"
# 3. O que o auto-updater realmente baixa (HTTP público):
curl -s https://rx.scoreodonto.com/updates/latest.yml | head -1
```
Se os três não baterem → rode `deploy-client.sh` até o fim.
---
## 6. 🔒 Nomes Congelados — Regra Obrigatória
> ⛔ **Esta seção define regras imutáveis do projeto. Agentes de IA e desenvolvedores
> NÃO devem alterar os nomes abaixo sem aprovação explícita do responsável pelo projeto.**
### 6.1. Nome do sistema (painel web)
| Local | Valor fixo |
|---|---|
| `dental-client/index.html``<title>` | `Score Client — Gerenciador de Imagens` |
| `src/components/Sidebar.jsx` → cabeçalho | `Score Client` |
| `src/pages/LoginPage.jsx``<h1>` | `Score Client` |
| `src/pages/DownloadPage.jsx``<h2>` | `Score Client — Cliente Windows` |
**O nome `Score Client` é fixo.** Nunca substituir por variações como `RF Dental`, `RFDental`,
`ScoreOdonto`, `RX Dental` ou qualquer outro. Se houver refatoração de UI, manter exatamente
`Score Client`.
### 6.2. Nome do executável do cliente Windows
O `electron-builder` gera o instalador com o nome definido em `package.json` → campo `productName`:
```json
{
"productName": "ScoreOdonto Dental Client"
}
```
Isso resulta em arquivos com o padrão:
```
ScoreOdonto Dental Client Setup X.Y.Z.exe
ScoreOdonto Dental Client Setup X.Y.Z.exe.blockmap
latest.yml → path: ScoreOdonto Dental Client Setup X.Y.Z.exe
```
**Apenas `X.Y.Z` (o número de versão) pode mudar.** O prefixo `ScoreOdonto Dental Client Setup `
é imutável. Alterar o `productName` no `package.json` quebra:
- O `latest.yml` gerado (nome do `path` muda → auto-updater não encontra o arquivo)
- A URL de download no painel web (que lê `path:` do `latest.yml` dinamicamente)
- O histórico de instalações nas clínicas (Windows trata como produto diferente)
### 6.3. URL de download — como funciona (e por que não usar nome hardcoded)
O `DownloadPage.jsx` lê o nome real do arquivo a partir do `latest.yml` **em tempo de execução**:
```javascript
fetch('/updates/latest.yml')
.then(r => r.text())
.then(t => {
const vMatch = t.match(/version:\s*([\d.]+)/);
if (vMatch) setVersion(vMatch[1]); // ex: "1.2.6"
const pMatch = t.match(/^path:\s*(.+)$/m);
if (pMatch) setExeFilename(pMatch[1].trim()); // ex: "ScoreOdonto Dental Client Setup 1.2.6.exe"
});
// URL final construída:
const downloadUrl = `/updates/${encodeURIComponent(exeFilename)}`;
```
> ✅ Isso garante que o botão de download sempre aponta para o arquivo correto da versão em produção,
> sem hardcode de nome — mas **depende de que o `productName` no `package.json` seja mantido exato**.
### 6.4. Checklist antes de qualquer refatoração de UI ou cliente
- [ ] O título da aba do browser ainda é `Score Client — Gerenciador de Imagens`?
- [ ] O cabeçalho da sidebar ainda exibe `Score Client`?
- [ ] O `<h1>` da tela de login ainda é `Score Client`?
- [ ] O `productName` no `package.json` do cliente ainda é `ScoreOdonto Dental Client`?
- [ ] O `latest.yml` em produção tem `path: ScoreOdonto Dental Client Setup X.Y.Z.exe`?
Se qualquer item acima for `NÃO` → reverter antes de continuar.
---
## 7. Erros comuns e soluções
### ❌ Erro A — Cliente em uma versão, servidor servindo outra (mais antiga)
**Sintoma:** `package.json` = `1.2.7`, mas `updates/latest.yml` na VPS1 = `1.2.6`.
As clínicas não recebem o update e novas instalações baixam a versão velha.
**Causa:** a versão foi buildada no `dist/` da VPS4 mas **o `rsync → VPS1` (passo 4 do
`deploy-client.sh`) não rodou**. Buildar não publica.
**Diagnóstico:**
```bash
# Build existe no dist/ (VPS4)?
ls -1 /home/deploy/stack/rx.scoreodonto.com-client/dist/*.exe | sort -V | tail -1
cat /home/deploy/stack/rx.scoreodonto.com-client/dist/latest.yml | head -1 # ex: 1.2.7
# Mas a VPS1 está atrás?
ssh deploy@10.99.0.1 "head -1 /home/deploy/.../dental-server/updates/latest.yml" # ex: 1.2.6
```
Versões diferentes = build não publicado.
**Solução (rápida, sem rebuild — artefato já existe e é válido):**
```bash
cd /home/deploy/stack/rx.scoreodonto.com-client
rsync -avz dist/*.exe dist/*.yml dist/*.blockmap \
deploy@10.99.0.1:/home/deploy/stack/rx.scoreodonto.com/dental-server/updates/
```
**Solução (canônica):** `bash deploy-client.sh` (rebuilda e publica).
---
### ❌ Erro B — Botão de download diz "arquivo não disponível"
**Sintoma:** ao clicar em "Baixar Instalador" na página `/download`, o browser retorna 404.
**Causa mais comum:** o `path:` no `latest.yml` não bate com o nome real do arquivo em `updates/`.
Isso acontece se o `productName` foi alterado no `package.json` do cliente, gerando um `.exe`
com nome diferente do que o `latest.yml` antigo listava.
**Diagnóstico:**
```bash
# Nome que o painel web vai pedir:
ssh deploy@10.99.0.1 "grep '^path:' /home/deploy/stack/rx.scoreodonto.com/dental-server/updates/latest.yml"
# ex: path: ScoreOdonto Dental Client Setup 1.2.6.exe
# Arquivos que existem de fato:
ssh deploy@10.99.0.1 "ls /home/deploy/stack/rx.scoreodonto.com/dental-server/updates/*.exe"
```
Se os nomes não baterem → o `productName` foi alterado ou o `rsync` não foi executado.
**Solução:** restaurar o `productName` para `ScoreOdonto Dental Client` e rodar `deploy-client.sh`.
---
### ❌ Erro C — `vite: not found` no deploy do servidor
**Causa:** listener roda com `NODE_ENV=production``npm install` pula devDependencies (vite).
**Solução:** no `deploy-prod.sh`, `NODE_ENV=development npm install --include=dev` antes do `vite build`.
### ❌ Erro D — Imagens do RX não aparecem (placeholder cinza, sem erro no DevTools)
**Causa:** endpoint de thumbnail retornava JSON `{url}` enquanto o `<img src>` apontava direto para ele.
**Solução:** o endpoint serve **bytes** (`image/jpeg`); gera a thumb on-the-fly e faz backfill no Wasabi.
### ❌ Erro E — Wasabi 403 após trocar credenciais
**Causa:** salvar credenciais no banco **não recarregava** o cliente S3 em memória.
**Solução:** `storage-config` chama `storage.loadConfigFromDb()` após salvar.
---
## 8. Troubleshooting
| Problema | Diagnóstico | Solução |
|---|---|---|
| **Cliente desatualizado nas clínicas** | `package.json``updates/latest.yml` na VPS1? | Rodar `deploy-client.sh` **até o passo 4** (ou rsync do `dist/`) |
| **Botão download → 404** | `path:` no `latest.yml` ≠ arquivo real em `updates/`? | Verificar `productName`; restaurar para `ScoreOdonto Dental Client` e rebuildar |
| Build do cliente falha | Wine/electron-builder na VPS4 | Ver log; `npx electron-builder --win --x64 --publish never` |
| Clínica não atualiza mesmo com yml novo | versão instalada ≥ yml? | Auto-update só sobe versão; conferir semver |
| Deploy do servidor `vite: not found` | `NODE_ENV=production` no install | `--include=dev` no `deploy-prod.sh` |
| Imagens em branco | thumb retornando JSON? | Endpoint deve servir **bytes** image/jpeg |
| Wasabi 403 | credenciais recarregadas? | `loadConfigFromDb()` após salvar config |
| Deploy do servidor lento (3+ min) | Modelo B? | Modelo A (build na VPS4) |
---
## 9. Changelog
| Versão | Data | O que mudou |
|---|---|---|
| **1.0.4** | 2026-05-31 | **Nomes congelados:** formaliza que `Score Client` (sistema) e `ScoreOdonto Dental Client Setup` (executável) são imutáveis. Documenta o Erro B (404 no download por nome errado), checklist de refatoração e como o `DownloadPage.jsx` resolve o nome dinamicamente via `latest.yml`. |
| 1.0.3 | 2026-05-31 | Ciclo completo do Cliente Windows (build `dist/` na VPS4 ≠ publicação `updates/` na VPS1). Documenta a armadilha "buildado mas não publicado", verificação em 3 pontos, e o `vite: not found`. |
| 1.0.2 | 2026-05-30 | RX: switch para Modelo A (build VPS4), erros comuns, Wasabi. Deploy ~30-40s. |
| 1.0.1 | 2026-05-30 | Consolidação inicial: deploy + versionamento + segurança. |
| 1.0.0 | 2026-05-30 | Versão inicial. |
+4 -3
@@ -4,12 +4,13 @@ Documentação da automação de deploy GitOps (Gitea + Multi-VPS) e versionamen
## Versão atual
- **[Deploy V.1.0.3](Deploy-V.1.0.3)** ← versão mais recente — ciclo completo do **Cliente Windows**
(build na VPS4 vs publicação na VPS1), armadilha "buildado mas não publicado", verificação em 3 pontos,
além de tudo do servidor (Modelo A, Wasabi, versionamento).
- **[Deploy V.1.0.4](Deploy-V.1.0.4)** ← versão mais recente — **nomes congelados**: `Score Client`
(sistema) e `ScoreOdonto Dental Client Setup` (executável) são imutáveis; apenas o número de versão
pode mudar. Inclui checklist de refatoração, Erro B (404 no download) e tudo da 1.0.3.
## Arquivo de versões anteriores
- [Deploy V.1.0.3](Deploy-V.1.0.3) — ciclo completo do Cliente Windows (build na VPS4 vs publicação na VPS1).
- [Deploy V.1.0.2](Deploy-V.1.0.2) — Modelo A otimizado para Wasabi (deploy ~30-40s).
- [Deploy V.1.0.1](Deploy-V.1.0.1) — consolidação: deploy + versionamento + segurança.
- [Deploy V.1.0.0](Deploy-V.1.0.0) — versão inicial (apenas deploy, sem versionamento).