feat: adicionar sistema de backup e badge editável na página inicial

This commit is contained in:
Erik
2025-11-29 12:22:56 -03:00
parent b73eb6c3eb
commit 99530200b4
13 changed files with 1511 additions and 41 deletions

353
docs/BACKUP_SYSTEM.md Normal file
View File

@@ -0,0 +1,353 @@
# Backup & Restore System - Guia de Implementação
## 📋 Visão Geral
Este sistema permite criar, gerenciar e fazer download de backups completos do banco de dados PostgreSQL e arquivos MinIO através de uma interface web integrada ao painel administrativo.
## 🎯 Características
- ✅ Backup completo do PostgreSQL com `pg_dump`
- ✅ Backup dos dados do MinIO
- ✅ Compactação automática em `tar.gz`
- ✅ Interface intuitiva no painel admin
- ✅ Download de backups
- ✅ Remoção de backups antigos
- ✅ Histórico com datas e tamanhos
- ✅ Funcionamento multi-ambiente (Docker, Local, Dokploy)
## 📁 Estrutura de Arquivos
```
frontend/
├── src/
│ ├── app/
│ │ ├── api/
│ │ │ └── backup/
│ │ │ ├── route.ts # POST/GET/DELETE - CRUD de backups
│ │ │ └── download/
│ │ │ └── route.ts # GET - Download de backups
│ │ └── admin/
│ │ └── configuracoes/
│ │ └── page.tsx # Interface de configurações com backup
│ └── components/
│ └── admin/
│ └── BackupManager.tsx # Componente UI do gerenciador
└── .backups/ # Diretório onde os backups são salvos
```
## 🔧 Variáveis de Ambiente Necessárias
Adicione ao seu `.env` ou ao `docker-compose.yml`:
```env
# PostgreSQL
POSTGRES_USER=admin
POSTGRES_PASSWORD=adminpassword
POSTGRES_DB=occto_db
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
# MinIO
MINIO_ENDPOINT=minio
MINIO_PORT=9000
MINIO_ACCESS_KEY=admin
MINIO_SECRET_KEY=adminpassword
MINIO_BUCKET_NAME=occto-images
MINIO_USE_SSL=false
```
## 🚀 Como Usar
### 1. **No Painel Admin**
1. Acesse `/admin/configuracoes`
2. Desça até a seção **"Backup & Restauração"**
3. Clique em **"Criar Backup Agora"** para iniciar um novo backup
4. Visualize o histórico de backups salvos
5. Download de um backup específico: clique no ícone de download
6. Remover um backup: clique no ícone de lixeira
### 2. **Via API (Programaticamente)**
```javascript
// Criar backup
const response = await fetch('/api/backup', {
method: 'POST',
headers: {
'Authorization': 'Bearer seu_token_aqui',
'Content-Type': 'application/json'
}
});
// Listar backups
const response = await fetch('/api/backup', {
headers: {
'Authorization': 'Bearer seu_token_aqui'
}
});
// Download de backup
const response = await fetch('/api/backup/download?file=backup-2025-11-28.tar.gz', {
headers: {
'Authorization': 'Bearer seu_token_aqui'
}
});
// Remover backup
const response = await fetch('/api/backup?id=backup-2025-11-28', {
method: 'DELETE',
headers: {
'Authorization': 'Bearer seu_token_aqui'
}
});
```
## 🏗️ Replicação em Outro Projeto Next.js
Se você quer implementar este sistema em outro projeto Next.js:
### Passo 1: Copiar os Arquivos
```bash
# Copiar as rotas da API
cp -r frontend/src/app/api/backup seu_projeto/src/app/api/
# Copiar o componente UI
cp frontend/src/components/admin/BackupManager.tsx seu_projeto/src/components/admin/
```
### Passo 2: Adicionar ao Docker Compose
Se estiver usando Docker:
```yaml
services:
postgres:
image: postgres:12-alpine
container_name: seu_postgres
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: adminpassword
POSTGRES_DB: seu_db
volumes:
- postgres_data:/var/lib/postgresql/data
# Montar diretório de backups
- ./backups:/app/.backups
networks:
- seu_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U admin"]
interval: 10s
timeout: 5s
retries: 5
seu_frontend:
# ... suas configurações
volumes:
# Compartilhar diretório de backups
- ./backups:/app/.backups
depends_on:
postgres:
condition: service_healthy
```
### Passo 3: Integrar na Página de Configurações
```tsx
// Sua página de configurações
import { BackupManager } from '@/components/admin/BackupManager';
export default function ConfiguracoesPage() {
return (
<div>
{/* Suas outras configurações */}
{/* Adicionar seção de backup */}
<div className="border-t pt-8 mt-8">
<h2 className="text-2xl font-bold mb-6">Backup & Restauração</h2>
<BackupManager />
</div>
</div>
);
}
```
### Passo 4: Autenticação
O sistema espera um token no header `Authorization: Bearer token`.
Você pode:
**Opção A:** Usar o token do usuário logado
```typescript
// No componente BackupManager
const token = localStorage.getItem('auth_token');
```
**Opção B:** Criar um middleware de autenticação
```typescript
// api/backup/route.ts
function authenticateRequest(request: NextRequest): boolean {
const token = request.headers.get('authorization');
// Implementar sua lógica de autenticação
return validateToken(token);
}
export async function POST(request: NextRequest) {
if (!authenticateRequest(request)) {
return NextResponse.json({ error: 'Não autorizado' }, { status: 401 });
}
// ... resto do código
}
```
**Opção C:** Usar autenticação de sessão (exemplo com NextAuth)
```typescript
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
export async function POST(request: NextRequest) {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json({ error: 'Não autorizado' }, { status: 401 });
}
// ... resto do código
}
```
## 📊 Estrutura do Backup
Um backup `.tar.gz` contém:
```
backup-2025-11-28/
├── database.sql # Dump completo do PostgreSQL
├── minio-data/ # Cópia de todos os arquivos do MinIO
│ └── [arquivos...]
└── metadata.json # Informações do backup
{
"timestamp": "2025-11-28T10:30:45.123Z",
"database": "occto_db",
"hostname": "postgres",
"minioEndpoint": "minio",
"version": "1.0"
}
```
## 🔐 Segurança
### Recomendações:
1. **Autenticação**
- Sempre valide a autenticação nas rotas de backup
- Use tokens JWT ou sessões seguras
2. **Autorização**
- Apenas administradores devem ter acesso
- Adicione verificação de roles/permissões
3. **Armazenamento de Backups**
- Considere armazenar em local protegido
- Implemente limpeza automática de backups antigos
- Criptografe backups sensíveis
4. **Path Traversal**
- O código já implementa validação com `path.resolve()`
- Nunca permita caminhos arbitrários
### Exemplo de Middleware de Autorização:
```typescript
import { NextRequest, NextResponse } from 'next/server';
function isAdmin(request: NextRequest): boolean {
const token = request.headers.get('authorization');
// Validar se o token corresponde a um admin
return validateAdminToken(token);
}
export async function POST(request: NextRequest) {
if (!isAdmin(request)) {
return NextResponse.json(
{ error: 'Apenas administradores podem criar backups' },
{ status: 403 }
);
}
// ... resto do código
}
```
## 🐛 Troubleshooting
### Erro: "pg_dump: comando não encontrado"
**Solução:** Certifique-se de que PostgreSQL Tools está instalado no container ou host.
```dockerfile
FROM node:18-alpine
# Adicionar PostgreSQL client
RUN apk add --no-cache postgresql-client
WORKDIR /app
# ... resto do Dockerfile
```
### Erro: "MinIO data não encontrado"
**Solução:** Verifique o caminho do volume do MinIO no docker-compose.
```yaml
volumes:
- ./minio_data:/data # Caminho correto no container
- ./backups:/app/.backups
```
### Erro 401 "Não autorizado"
**Solução:** Verifique se o token está sendo enviado corretamente.
```javascript
// Sempre enviar token
const token = localStorage.getItem('auth_token');
if (!token) {
console.error('Token não encontrado');
return;
}
fetch('/api/backup', {
headers: {
'Authorization': `Bearer ${token}`
}
});
```
## 📈 Melhorias Futuras
- [ ] Agendamento automático de backups (cron)
- [ ] Envio automático para S3/Cloud Storage
- [ ] Compressão em background
- [ ] Restauração de backups via interface
- [ ] Notificações via email
- [ ] Verificação de integridade de backup
- [ ] Versionamento de backups
- [ ] Limpeza automática de backups antigos
## 📞 Suporte
Para mais informações ou dúvidas sobre implementação, consulte a documentação oficial:
- [Next.js API Routes](https://nextjs.org/docs/app/building-your-application/routing/route-handlers)
- [PostgreSQL pg_dump](https://www.postgresql.org/docs/current/app-pgdump.html)
- [MinIO Client](https://docs.min.io/minio/baremetal/)
---
**Versão:** 1.0
**Data:** Novembro 2025
**Compatibilidade:** Next.js 13.4+ com App Router

View File

@@ -166,3 +166,163 @@ frontend/src/app/api/projects/[id]/
**Branch**: `cms-1.1` **Branch**: `cms-1.1`
**Status**: ✅ Produção **Status**: ✅ Produção
---
## CMS 1.2 - Atualizações (28/11/2025)
### 📱 WhatsApp Dinâmico
#### API de Informações de Contato (`/api/contact-info`)
- Nova rota que busca número do WhatsApp dinamicamente do CMS
- Busca dados da página `contato` slug
- Cache de 1 minuto para otimizar performance
- Fallback para número padrão `(35) 9882-9445` com link `https://wa.me/5535988229445`
- Retorna JSON: `{ whatsapp: string, whatsappLink: string }`
#### Integração no Botão Flutuante
- `WhatsAppButton.tsx` agora busca número da API `/api/contact-info`
- Abre WhatsApp diretamente com número do CMS
- Exibe label traduzido `whatsapp.label`
#### Integração no Header
- Botão "Fale Conosco" no header desktop agora abre WhatsApp diretamente
- Menu mobile também integrado
- Ambos buscam número da API em tempo real
#### Tradução do Label
- Adicionada chave `whatsapp.label` em todos os locales:
- PT: "Fale Conosco"
- EN: "Contact Us"
- ES: "Contáctenos"
- Adicionada no `LanguageContext.tsx` para fallback
---
### 🌙 Dark Mode no Painel Admin
#### Novo Botão de Tema
- Adicionado botão sol/lua no header do painel admin
- Localizado ao lado das notificações
- Mesmo comportamento do botão no site público
- Integrado com `useTheme` hook
#### Funcionalidade
- Toggle claro/escuro funcional em todo o admin
- Persistência de preferência via `next-themes`
- Ícone muda conforme tema: `ri-sun-line` (dark) / `ri-moon-line` (light)
---
### 🔗 Correção de Links do Dashboard
#### Links das Mensagens
- Card "Mensagens" → `/admin/mensagens`
- Botão "Ver todas" → `/admin/mensagens`
- Cada item de mensagem → `/admin/mensagens`
- Antes: apontavam para `/admin/contatos` (rota inexistente)
#### Estrutura de Rotas
- Confirmado que rota correta é `/admin/mensagens`
- Não existe `/admin/contatos` no projeto
---
### ✅ Correções Finais de WhatsApp
#### Formato Correto do Número
- **Número fornecido**: `+55 35 9882-9445`
- **Formato wa.me**: `5535988229445`
- `55` = código Brasil
- `35` = DDD
- `988229445` = número com 9 dígitos (padrão celular BR)
#### Atualização em Todos os Arquivos
- API `/api/contact-info/route.ts`
- Componente `WhatsAppButton.tsx`
- Componente `Header.tsx`
- Todos agora usam `5535988229445` como padrão
---
### 📁 Arquivos Modificados (28/11)
```
frontend/src/app/api/
└── contact-info/
└── route.ts # ✨ NOVO - API WhatsApp dinâmico
frontend/src/components/
├── WhatsAppButton.tsx # Integração API contact-info
└── Header.tsx # Integração API contact-info
frontend/src/app/admin/
├── layout.tsx # Dark mode + links corrigidos
└── page.tsx # Links mensagens corrigidos
frontend/src/contexts/
└── LanguageContext.tsx # Adicionado whatsapp.label
frontend/src/locales/
├── pt.json # Adicionado whatsapp.label
├── en.json # Adicionado whatsapp.label
└── es.json # Adicionado whatsapp.label
```
---
## Commits Realizados (28/11/2025)
### Commit 1: WhatsApp Dinâmico
```
feat: WhatsApp dinâmico do CMS
- Criada API /api/contact-info que busca número do CMS
- Header e botão flutuante agora puxam número dinamicamente
- Número padrão: (35) 9882-9445
```
### Commit 2: Tradução WhatsApp
```
fix: WhatsApp label tradução e número correto (35) 9882-9445
- Adicionada chave whatsapp.label nos arquivos de locale (pt, en, es)
- Adicionada chave whatsapp.label no LanguageContext
```
### Commit 3: Formato Correto
```
fix: número WhatsApp correto 5535988229445
- Corrigido número padrão em todos os arquivos
- Formato correto: 55 (Brasil) + 35 (DDD) + 988229445 (número)
```
### Commit 4: Dark Mode e Links
```
fix: dark mode no admin, links mensagens dashboard, WhatsApp correto
- Adicionado botão de dark mode no header do painel admin
- Corrigido links do dashboard: /admin/contatos -> /admin/mensagens
- Corrigido número WhatsApp: 5535988229445 (formato correto BR)
```
---
## 🚀 Status de Deployment
### Ambiente de Produção
- **Domínio**: www.octtoengenharia.com.br
- **Docker Compose**: `docker-compose.yml` (production)
- **Banco**: PostgreSQL `occto_db`
- **Storage**: MinIO `occto_minio`
- **Frontend**: `occto_frontend`
- **Network**: `dokploy-network`
### Infraestrutura
- **Versão PostgreSQL**: 12-alpine
- **Versão MinIO**: RELEASE.2023-09-04T19-57-37Z
- **Framework**: Next.js 15.1
- **ORM**: Prisma
- **Deploy Platform**: Dokploy (com auto-deploy)
---
**Branch**: `cms-1.1`
**Status**: ✅ Produção (Deploy 28/11/2025)

View File

@@ -54,7 +54,11 @@ export default function Home() {
const hero = content?.hero || { const hero = content?.hero || {
title: 'Engenharia de Excelência para Seus Projetos', title: 'Engenharia de Excelência para Seus Projetos',
subtitle: 'Soluções completas em engenharia veicular, mecânica e segurança do trabalho com mais de 15 anos de experiência.', subtitle: 'Soluções completas em engenharia veicular, mecânica e segurança do trabalho com mais de 15 anos de experiência.',
buttonText: 'Conheça Nossos Serviços' buttonText: 'Conheça Nossos Serviços',
badge: {
text: 'Coca-Cola',
show: true
}
}; };
const features = content?.features || { const features = content?.features || {
@@ -163,10 +167,12 @@ export default function Home() {
<div className="container mx-auto px-4 relative z-20"> <div className="container mx-auto px-4 relative z-20">
<div className="max-w-3xl"> <div className="max-w-3xl">
{hero.badge?.show && (
<div className="inline-flex items-center gap-3 bg-white/10 backdrop-blur-md border border-white/20 rounded-full px-5 py-2 mb-8 hover:bg-white/20 transition-colors cursor-default"> <div className="inline-flex items-center gap-3 bg-white/10 backdrop-blur-md border border-white/20 rounded-full px-5 py-2 mb-8 hover:bg-white/20 transition-colors cursor-default">
<i className="ri-verified-badge-fill text-primary text-xl"></i> <i className="ri-verified-badge-fill text-primary text-xl"></i>
<span className="text-sm font-bold tracking-wider uppercase text-white">{t('home.officialProvider')} <span className="text-primary">Coca-Cola</span></span> <span className="text-sm font-bold tracking-wider uppercase text-white">{t('home.officialProvider')} <span className="text-primary">{hero.badge.text}</span></span>
</div> </div>
)}
<h1 className="text-5xl md:text-6xl font-bold font-headline mb-6 leading-tight"> <h1 className="text-5xl md:text-6xl font-bold font-headline mb-6 leading-tight">
{hero.title} {hero.title}

View File

@@ -8,51 +8,51 @@ export default function PrivacyPolicy() {
return ( return (
<main className="py-20 bg-white dark:bg-secondary transition-colors duration-300"> <main className="py-20 bg-white dark:bg-secondary transition-colors duration-300">
<div className="container mx-auto px-4 max-w-4xl"> <div className="container mx-auto px-4 max-w-4xl">
<h1 className="text-4xl font-bold font-headline text-secondary dark:text-white mb-8">{t('footer.privacyPolicy')}</h1> <h1 className="text-4xl font-bold font-headline text-secondary dark:text-white mb-8">{t('privacy.title')}</h1>
<div className="prose prose-lg text-gray-600 dark:text-gray-300"> <div className="prose prose-lg text-gray-600 dark:text-gray-300">
<p className="mb-6"> <p className="mb-6">
A Octto Engenharia valoriza a privacidade de seus usuários e clientes. Esta Política de Privacidade descreve como coletamos, usamos e protegemos suas informações pessoais ao utilizar nosso site e serviços. {t('privacy.intro')}
</p> </p>
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">1. Coleta de Informações</h2> <h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">{t('privacy.section1.title')}</h2>
<p className="mb-4"> <p className="mb-4">
Coletamos informações que você nos fornece diretamente, como quando preenche nosso formulário de contato, solicita um orçamento ou se inscreve em nossa newsletter. As informações podem incluir nome, e-mail, telefone e detalhes sobre sua empresa ou projeto. {t('privacy.section1.content')}
</p> </p>
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">2. Uso das Informações</h2> <h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">{t('privacy.section2.title')}</h2>
<p className="mb-4"> <p className="mb-4">
Utilizamos as informações coletadas para: {t('privacy.section2.intro')}
</p> </p>
<ul className="list-disc pl-6 mb-6 space-y-2"> <ul className="list-disc pl-6 mb-6 space-y-2">
<li>Responder a suas consultas e solicitações de orçamento;</li> <li>{t('privacy.section2.items.0')}</li>
<li>Fornecer informações sobre nossos serviços de engenharia e laudos técnicos;</li> <li>{t('privacy.section2.items.1')}</li>
<li>Melhorar a experiência do usuário em nosso site;</li> <li>{t('privacy.section2.items.2')}</li>
<li>Cumprir obrigações legais e regulatórias.</li> <li>{t('privacy.section2.items.3')}</li>
</ul> </ul>
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">3. Proteção de Dados</h2> <h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">{t('privacy.section3.title')}</h2>
<p className="mb-4"> <p className="mb-4">
Adotamos medidas de segurança técnicas e organizacionais adequadas para proteger seus dados pessoais contra acesso não autorizado, alteração, divulgação ou destruição. {t('privacy.section3.content')}
</p> </p>
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">4. Compartilhamento de Informações</h2> <h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">{t('privacy.section4.title')}</h2>
<p className="mb-4"> <p className="mb-4">
Não vendemos, trocamos ou transferimos suas informações pessoais para terceiros, exceto quando necessário para a prestação de nossos serviços (ex: parceiros técnicos envolvidos em um projeto específico) ou quando exigido por lei. {t('privacy.section4.content')}
</p> </p>
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">5. Cookies</h2> <h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">{t('privacy.section5.title')}</h2>
<p className="mb-4"> <p className="mb-4">
Nosso site pode utilizar cookies para melhorar a navegação e entender como os visitantes interagem com nosso conteúdo. Você pode desativar os cookies nas configurações do seu navegador, se preferir. {t('privacy.section5.content')}
</p> </p>
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">6. Contato</h2> <h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">{t('privacy.section6.title')}</h2>
<p className="mb-4"> <p className="mb-4">
Se você tiver dúvidas sobre esta Política de Privacidade, entre em contato conosco através do e-mail: contato@octto.com.br. {t('privacy.section6.content')}
</p> </p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-12"> <p className="text-sm text-gray-500 dark:text-gray-400 mt-12">
Última atualização: Novembro de 2025. {t('privacy.lastUpdate')}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -8,45 +8,45 @@ export default function TermsOfUse() {
return ( return (
<main className="py-20 bg-white dark:bg-secondary transition-colors duration-300"> <main className="py-20 bg-white dark:bg-secondary transition-colors duration-300">
<div className="container mx-auto px-4 max-w-4xl"> <div className="container mx-auto px-4 max-w-4xl">
<h1 className="text-4xl font-bold font-headline text-secondary dark:text-white mb-8">{t('footer.termsOfUse')}</h1> <h1 className="text-4xl font-bold font-headline text-secondary dark:text-white mb-8">{t('terms.title')}</h1>
<div className="prose prose-lg text-gray-600 dark:text-gray-300"> <div className="prose prose-lg text-gray-600 dark:text-gray-300">
<p className="mb-6"> <p className="mb-6">
Bem-vindo ao site da Octto Engenharia. Ao acessar e utilizar este site, você concorda em cumprir e estar vinculado aos seguintes Termos de Uso. Se você não concordar com qualquer parte destes termos, por favor, não utilize nosso site. {t('terms.intro')}
</p> </p>
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">1. Uso do Site</h2> <h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">{t('terms.section1.title')}</h2>
<p className="mb-4"> <p className="mb-4">
O conteúdo deste site é apenas para fins informativos gerais sobre nossos serviços de engenharia mecânica, laudos e projetos. Reservamo-nos o direito de alterar ou descontinuar qualquer aspecto do site a qualquer momento. {t('terms.section1.content')}
</p> </p>
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">2. Propriedade Intelectual</h2> <h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">{t('terms.section2.title')}</h2>
<p className="mb-4"> <p className="mb-4">
Todo o conteúdo presente neste site, incluindo textos, gráficos, logotipos, ícones, imagens e software, é propriedade da Octto Engenharia ou de seus fornecedores de conteúdo e é protegido pelas leis de direitos autorais do Brasil e internacionais. {t('terms.section2.content')}
</p> </p>
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">3. Limitação de Responsabilidade</h2> <h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">{t('terms.section3.title')}</h2>
<p className="mb-4"> <p className="mb-4">
A Octto Engenharia não se responsabiliza por quaisquer danos diretos, indiretos, incidentais ou consequenciais resultantes do uso ou da incapacidade de uso deste site ou de qualquer informação nele contida. As informações técnicas fornecidas no site não substituem a consulta profissional e a emissão de laudos técnicos específicos para cada caso. {t('terms.section3.content')}
</p> </p>
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">4. Links para Terceiros</h2> <h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">{t('terms.section4.title')}</h2>
<p className="mb-4"> <p className="mb-4">
Nosso site pode conter links para sites de terceiros. Estes links são fornecidos apenas para sua conveniência. A Octto Engenharia não tem controle sobre o conteúdo desses sites e não assume responsabilidade por eles. {t('terms.section4.content')}
</p> </p>
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">5. Alterações nos Termos</h2> <h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">{t('terms.section5.title')}</h2>
<p className="mb-4"> <p className="mb-4">
Podemos revisar estes Termos de Uso a qualquer momento. Ao utilizar este site, você concorda em ficar vinculado à versão atual desses Termos de Uso. {t('terms.section5.content')}
</p> </p>
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">6. Legislação Aplicável</h2> <h2 className="text-2xl font-bold font-headline text-secondary dark:text-white mt-8 mb-4">{t('terms.section6.title')}</h2>
<p className="mb-4"> <p className="mb-4">
Estes termos são regidos e interpretados de acordo com as leis da República Federativa do Brasil. Qualquer disputa relacionada a estes termos será submetida à jurisdição exclusiva dos tribunais competentes. {t('terms.section6.content')}
</p> </p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-12"> <p className="text-sm text-gray-500 dark:text-gray-400 mt-12">
Última atualização: Novembro de 2025. {t('terms.lastUpdate')}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -2,6 +2,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useToast } from '@/contexts/ToastContext'; import { useToast } from '@/contexts/ToastContext';
import { BackupManager } from '@/components/admin/BackupManager';
const PRESET_COLORS = [ const PRESET_COLORS = [
{ name: 'Laranja (Padrão)', value: '#FF6B35', gradient: 'from-orange-500 to-orange-600' }, { name: 'Laranja (Padrão)', value: '#FF6B35', gradient: 'from-orange-500 to-orange-600' },
@@ -252,6 +253,20 @@ export default function ConfiguracoesPage() {
</p> </p>
</div> </div>
</div> </div>
{/* Backup Manager Section */}
<div className="border-t-2 border-gray-200 dark:border-white/10 pt-12">
<div className="flex items-center gap-4 mb-8">
<div className="w-12 h-12 bg-linear-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center shadow-lg shadow-blue-500/30">
<i className="ri-database-backup-line text-2xl text-white"></i>
</div>
<div>
<h2 className="text-2xl font-bold font-headline text-secondary dark:text-white">Backup & Restauração</h2>
<p className="text-gray-500 dark:text-gray-400 mt-1">Gerencie backups completos do seu banco de dados e arquivos</p>
</div>
</div>
<BackupManager />
</div>
</div> </div>
); );
} }

View File

@@ -250,6 +250,10 @@ interface HomeContent {
subtitle: string; subtitle: string;
buttonText: string; buttonText: string;
imageUrl?: string; imageUrl?: string;
badge?: {
text: string;
show: boolean;
};
}; };
features: { features: {
pretitle: string; pretitle: string;
@@ -307,7 +311,11 @@ export default function EditHomePage() {
hero: { hero: {
title: 'Engenharia de Ponta para Seus Projetos', title: 'Engenharia de Ponta para Seus Projetos',
subtitle: 'Soluções completas em engenharia veicular, mecânica e segurança do trabalho.', subtitle: 'Soluções completas em engenharia veicular, mecânica e segurança do trabalho.',
buttonText: 'Conheça Nossos Serviços' buttonText: 'Conheça Nossos Serviços',
badge: {
text: 'Coca-Cola',
show: true
}
}, },
features: { features: {
pretitle: 'Diferenciais', pretitle: 'Diferenciais',
@@ -371,7 +379,11 @@ export default function EditHomePage() {
if (data.content) { if (data.content) {
// Mesclar com valores padrão para garantir que todas as propriedades existam // Mesclar com valores padrão para garantir que todas as propriedades existam
setFormData(prevData => ({ setFormData(prevData => ({
hero: data.content.hero || prevData.hero, hero: {
...prevData.hero,
...data.content.hero,
badge: data.content.hero?.badge || prevData.hero.badge
},
features: data.content.features || prevData.features, features: data.content.features || prevData.features,
services: data.content.services || prevData.services, services: data.content.services || prevData.services,
about: data.content.about || prevData.about, about: data.content.about || prevData.about,
@@ -602,6 +614,64 @@ export default function EditHomePage() {
className="w-full px-4 py-3 bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl text-gray-900 dark:text-white focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all" className="w-full px-4 py-3 bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl text-gray-900 dark:text-white focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all"
/> />
</div> </div>
<div className="border-t border-gray-200 dark:border-white/10 pt-6 mt-6">
<h3 className="text-sm font-bold text-secondary dark:text-white mb-4 flex items-center gap-2">
<i className="ri-verified-badge-fill text-primary"></i>
Badge (Crachá) no Banner
</h3>
<div className="space-y-4">
<div className="flex items-center gap-3 p-4 bg-gray-50 dark:bg-white/5 rounded-xl border border-gray-200 dark:border-white/10">
<input
type="checkbox"
checked={formData.hero.badge?.show || false}
onChange={(e) => setFormData({
...formData,
hero: {
...formData.hero,
badge: {
...(formData.hero.badge || { text: '', show: false }),
show: e.target.checked
}
}
})}
className="w-5 h-5 accent-primary cursor-pointer"
/>
<label className="text-sm font-bold text-gray-700 dark:text-gray-300 cursor-pointer">
Exibir badge no banner principal
</label>
</div>
{formData.hero.badge?.show && (
<div>
<label className="block text-sm font-bold text-gray-700 dark:text-gray-300 mb-2">
Texto da Badge (ex: Coca-Cola, Parceiro Oficial, etc.)
</label>
<input
type="text"
value={formData.hero.badge?.text || ''}
onChange={(e) => setFormData({
...formData,
hero: {
...formData.hero,
badge: {
...(formData.hero.badge || { text: '', show: false }),
text: e.target.value
}
}
})}
placeholder="Digite o nome da empresa ou parceiro"
maxLength={50}
className="w-full px-4 py-3 bg-white dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl text-gray-900 dark:text-white focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all"
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
{formData.hero.badge?.text?.length || 0}/50 caracteres
</p>
</div>
)}
</div>
</div>
</div> </div>
</div> </div>
)} )}

View File

@@ -0,0 +1,70 @@
import { NextRequest, NextResponse } from 'next/server';
import * as fs from 'fs';
import * as path from 'path';
import { createReadStream } from 'fs';
const BACKUP_DIR = path.join(process.cwd(), '.backups');
/**
* GET /api/backup/download?file=backup-filename.tar.gz
* Faz download de um backup específico
*/
export async function GET(request: NextRequest) {
try {
const authHeader = request.headers.get('authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return NextResponse.json(
{ success: false, error: 'Não autorizado' },
{ status: 401 }
);
}
const { searchParams } = new URL(request.url);
const filename = searchParams.get('file');
if (!filename) {
return NextResponse.json(
{ success: false, error: 'Arquivo não especificado' },
{ status: 400 }
);
}
// Validar que o arquivo está dentro do diretório de backups (prevenir path traversal)
const backupPath = path.resolve(path.join(BACKUP_DIR, filename));
const resolvedBackupDir = path.resolve(BACKUP_DIR);
if (!backupPath.startsWith(resolvedBackupDir)) {
return NextResponse.json(
{ success: false, error: 'Acesso negado' },
{ status: 403 }
);
}
if (!fs.existsSync(backupPath)) {
return NextResponse.json(
{ success: false, error: 'Arquivo não encontrado' },
{ status: 404 }
);
}
const stat = fs.statSync(backupPath);
const fileStream = createReadStream(backupPath);
return new NextResponse(fileStream as any, {
headers: {
'Content-Type': 'application/gzip',
'Content-Disposition': `attachment; filename="${filename}"`,
'Content-Length': stat.size.toString(),
},
});
} catch (error) {
console.error('[BACKUP] Erro ao fazer download:', error);
return NextResponse.json(
{
success: false,
error: (error as Error).message
},
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,334 @@
import { NextRequest, NextResponse } from 'next/server';
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { createReadStream } from 'fs';
// Variáveis de ambiente
const POSTGRES_USER = process.env.POSTGRES_USER || 'admin';
const POSTGRES_PASSWORD = process.env.POSTGRES_PASSWORD || 'adminpassword';
const POSTGRES_DB = process.env.POSTGRES_DB || 'occto_db';
const POSTGRES_HOST = process.env.POSTGRES_HOST || 'postgres';
const POSTGRES_PORT = process.env.POSTGRES_PORT || '5432';
const MINIO_ENDPOINT = process.env.MINIO_ENDPOINT || 'minio';
const MINIO_PORT = process.env.MINIO_PORT || '9000';
const MINIO_ACCESS_KEY = process.env.MINIO_ACCESS_KEY || 'admin';
const MINIO_SECRET_KEY = process.env.MINIO_SECRET_KEY || 'adminpassword';
const MINIO_BUCKET_NAME = process.env.MINIO_BUCKET_NAME || 'occto-images';
const MINIO_USE_SSL = process.env.MINIO_USE_SSL === 'true';
// Diretório para armazenar backups
const BACKUP_DIR = path.join(process.cwd(), '.backups');
// Criar diretório de backups se não existir
if (!fs.existsSync(BACKUP_DIR)) {
fs.mkdirSync(BACKUP_DIR, { recursive: true });
}
interface BackupInfo {
id: string;
timestamp: string;
date: string;
size: number;
filename: string;
status: 'success' | 'error';
message: string;
}
interface BackupResponse {
success: boolean;
message: string;
backup?: BackupInfo;
error?: string;
}
/**
* POST /api/backup
* Cria um backup completo do PostgreSQL e MinIO
*/
export async function POST(request: NextRequest) {
try {
// Validar autenticação (você pode adicionar verificação de token)
const authHeader = request.headers.get('authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return NextResponse.json(
{ success: false, error: 'Não autorizado' },
{ status: 401 }
);
}
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('-').slice(0, 4).join('-');
const backupId = `backup-${timestamp}`;
const backupPath = path.join(BACKUP_DIR, backupId);
// Criar pasta do backup
fs.mkdirSync(backupPath, { recursive: true });
const backupInfo: BackupInfo = {
id: backupId,
timestamp: new Date().toISOString(),
date: new Date().toLocaleDateString('pt-BR'),
size: 0,
filename: backupId,
status: 'success',
message: ''
};
// 1. Fazer backup do PostgreSQL
try {
console.log('[BACKUP] Iniciando backup do PostgreSQL...');
const pgBackupPath = path.join(backupPath, 'database.sql');
const pgCommand = `PGPASSWORD="${POSTGRES_PASSWORD}" pg_dump -h ${POSTGRES_HOST} -U ${POSTGRES_USER} -d ${POSTGRES_DB} > "${pgBackupPath}"`;
execSync(pgCommand, { stdio: 'pipe', env: { ...process.env, PGPASSWORD: POSTGRES_PASSWORD } });
console.log('[BACKUP] PostgreSQL backup concluído');
} catch (error) {
console.error('[BACKUP] Erro ao fazer backup do PostgreSQL:', error);
backupInfo.status = 'error';
backupInfo.message += `Erro PostgreSQL: ${(error as Error).message}. `;
}
// 2. Fazer backup do MinIO (copiar os dados)
try {
console.log('[BACKUP] Iniciando backup do MinIO...');
const minioBackupPath = path.join(backupPath, 'minio-data');
// Se estiver usando Docker, copiar do volume
// Se estiver local, copiar do diretório minio_data
const minioDataPath = path.join(process.cwd(), '..', 'minio_data');
if (fs.existsSync(minioDataPath)) {
// Copiar recursivamente
copyDirSync(minioDataPath, minioBackupPath);
console.log('[BACKUP] MinIO backup concluído');
} else {
console.warn('[BACKUP] Diretório MinIO não encontrado em:', minioDataPath);
backupInfo.message += `Aviso: MinIO local não encontrado. `;
}
} catch (error) {
console.error('[BACKUP] Erro ao fazer backup do MinIO:', error);
backupInfo.message += `Erro MinIO: ${(error as Error).message}. `;
}
// 3. Criar arquivo metadata.json
const metadataPath = path.join(backupPath, 'metadata.json');
fs.writeFileSync(metadataPath, JSON.stringify({
timestamp: backupInfo.timestamp,
database: POSTGRES_DB,
hostname: POSTGRES_HOST,
minioEndpoint: MINIO_ENDPOINT,
version: '1.0'
}, null, 2));
// 4. Calcular tamanho do backup
const size = calculateDirSize(backupPath);
backupInfo.size = size;
// 5. Compactar backup (opcional - melhor para armazenamento)
const compressBackup = true;
if (compressBackup) {
try {
console.log('[BACKUP] Compactando backup...');
const tarCommand = `tar -czf "${path.join(BACKUP_DIR, backupId)}.tar.gz" -C "${BACKUP_DIR}" "${backupId}"`;
execSync(tarCommand, { stdio: 'pipe' });
// Remover pasta original após compactar
fs.rmSync(backupPath, { recursive: true, force: true });
backupInfo.filename = `${backupId}.tar.gz`;
console.log('[BACKUP] Compactação concluída');
} catch (error) {
console.warn('[BACKUP] Erro ao compactar:', error);
backupInfo.message += `Aviso: Não foi possível compactar. `;
}
}
if (backupInfo.status === 'error' && backupInfo.message) {
return NextResponse.json<BackupResponse>(
{
success: false,
message: 'Backup concluído com erros',
backup: backupInfo,
error: backupInfo.message
},
{ status: 207 } // Multi-status
);
}
return NextResponse.json<BackupResponse>(
{
success: true,
message: 'Backup realizado com sucesso',
backup: backupInfo
},
{ status: 201 }
);
} catch (error) {
console.error('[BACKUP] Erro geral:', error);
return NextResponse.json<BackupResponse>(
{
success: false,
message: 'Erro ao criar backup',
error: (error as Error).message
},
{ status: 500 }
);
}
}
/**
* GET /api/backup
* Lista os backups disponíveis
*/
export async function GET(request: NextRequest) {
try {
const authHeader = request.headers.get('authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return NextResponse.json(
{ success: false, error: 'Não autorizado' },
{ status: 401 }
);
}
const files = fs.readdirSync(BACKUP_DIR);
const backups: BackupInfo[] = [];
for (const file of files) {
const filePath = path.join(BACKUP_DIR, file);
const stat = fs.statSync(filePath);
if (stat.isFile()) {
const timestamp = file.replace('backup-', '').replace('.tar.gz', '');
backups.push({
id: file.replace('.tar.gz', ''),
timestamp: new Date(timestamp.replace(/-/g, ':')).toISOString(),
date: new Date(timestamp.replace(/-/g, ':')).toLocaleDateString('pt-BR'),
size: stat.size,
filename: file,
status: 'success',
message: 'Backup disponível'
});
}
}
// Ordenar por data (mais recente primeiro)
backups.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
return NextResponse.json({
success: true,
backups,
count: backups.length
});
} catch (error) {
console.error('[BACKUP] Erro ao listar backups:', error);
return NextResponse.json(
{
success: false,
error: (error as Error).message
},
{ status: 500 }
);
}
}
/**
* DELETE /api/backup?id=backup-id
* Remove um backup específico
*/
export async function DELETE(request: NextRequest) {
try {
const authHeader = request.headers.get('authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return NextResponse.json(
{ success: false, error: 'Não autorizado' },
{ status: 401 }
);
}
const { searchParams } = new URL(request.url);
const backupId = searchParams.get('id');
if (!backupId) {
return NextResponse.json(
{ success: false, error: 'ID do backup não fornecido' },
{ status: 400 }
);
}
const backupPath = path.join(BACKUP_DIR, `${backupId}.tar.gz`);
if (!fs.existsSync(backupPath)) {
return NextResponse.json(
{ success: false, error: 'Backup não encontrado' },
{ status: 404 }
);
}
fs.unlinkSync(backupPath);
return NextResponse.json({
success: true,
message: 'Backup removido com sucesso'
});
} catch (error) {
console.error('[BACKUP] Erro ao deletar backup:', error);
return NextResponse.json(
{
success: false,
error: (error as Error).message
},
{ status: 500 }
);
}
}
/**
* Função auxiliar: copiar diretório recursivamente
*/
function copyDirSync(src: string, dest: string) {
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, { recursive: true });
}
const files = fs.readdirSync(src);
for (const file of files) {
const srcPath = path.join(src, file);
const destPath = path.join(dest, file);
const stat = fs.statSync(srcPath);
if (stat.isDirectory()) {
copyDirSync(srcPath, destPath);
} else {
fs.copyFileSync(srcPath, destPath);
}
}
}
/**
* Função auxiliar: calcular tamanho do diretório
*/
function calculateDirSize(dirPath: string): number {
let size = 0;
try {
const files = fs.readdirSync(dirPath);
for (const file of files) {
const filePath = path.join(dirPath, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
size += calculateDirSize(filePath);
} else {
size += stat.size;
}
}
} catch (error) {
console.error('Erro ao calcular tamanho:', error);
}
return size;
}

View File

@@ -0,0 +1,270 @@
"use client";
import { useState, useEffect } from 'react';
import { useToast } from '@/contexts/ToastContext';
interface BackupInfo {
id: string;
timestamp: string;
date: string;
size: number;
filename: string;
status: 'success' | 'error';
message: string;
}
interface BackupsListResponse {
success: boolean;
backups: BackupInfo[];
count: number;
}
export function BackupManager() {
const [loading, setLoading] = useState(false);
const [backups, setBackups] = useState<BackupInfo[]>([]);
const [listLoading, setListLoading] = useState(true);
const { success, error: showError } = useToast();
// Buscar lista de backups ao carregar
useEffect(() => {
fetchBackups();
}, []);
const fetchBackups = async () => {
try {
setListLoading(true);
const token = localStorage.getItem('auth_token') || '';
const response = await fetch('/api/backup', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const data: BackupsListResponse = await response.json();
setBackups(data.backups || []);
} else {
showError('Erro ao carregar backups');
}
} catch (err) {
showError('Erro ao carregar backups: ' + (err as Error).message);
} finally {
setListLoading(false);
}
};
const createBackup = async () => {
try {
setLoading(true);
const token = localStorage.getItem('auth_token') || '';
const response = await fetch('/api/backup', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (response.ok || response.status === 201) {
success('Backup criado com sucesso!');
// Atualizar lista de backups
await new Promise(resolve => setTimeout(resolve, 1000));
await fetchBackups();
} else {
showError(data.error || 'Erro ao criar backup');
}
} catch (err) {
showError('Erro ao criar backup: ' + (err as Error).message);
} finally {
setLoading(false);
}
};
const deleteBackup = async (backupId: string) => {
if (!confirm('Tem certeza que deseja remover este backup?')) {
return;
}
try {
const token = localStorage.getItem('auth_token') || '';
const response = await fetch(`/api/backup?id=${backupId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
if (response.ok) {
success('Backup removido com sucesso!');
await fetchBackups();
} else {
showError(data.error || 'Erro ao remover backup');
}
} catch (err) {
showError('Erro ao remover backup: ' + (err as Error).message);
}
};
const downloadBackup = async (filename: string) => {
try {
const token = localStorage.getItem('auth_token') || '';
const response = await fetch(`/api/backup/download?file=${filename}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} else {
showError('Erro ao baixar backup');
}
} catch (err) {
showError('Erro ao baixar backup: ' + (err as Error).message);
}
};
const formatSize = (bytes: number) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
};
const formatDate = (isoString: string) => {
return new Date(isoString).toLocaleString('pt-BR');
};
return (
<div className="space-y-6">
{/* Botão de Criar Backup */}
<div className="bg-gradient-to-r from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/20 border border-blue-200 dark:border-blue-800 rounded-2xl p-6">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div>
<h3 className="text-lg font-bold text-gray-900 dark:text-white mb-2 flex items-center gap-2">
<i className="ri-database-backup-line text-blue-600 dark:text-blue-400 text-2xl"></i>
Criar Novo Backup
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
Realize um backup completo do banco de dados e arquivos MinIO
</p>
</div>
<button
onClick={createBackup}
disabled={loading}
className="px-6 py-3 bg-blue-600 text-white rounded-lg font-bold hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors whitespace-nowrap flex items-center gap-2"
>
{loading ? (
<>
<i className="ri-loader-4-line animate-spin"></i>
Criando...
</>
) : (
<>
<i className="ri-download-cloud-2-line"></i>
Criar Backup Agora
</>
)}
</button>
</div>
</div>
{/* Lista de Backups */}
<div>
<h3 className="text-lg font-bold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<i className="ri-archive-line text-primary"></i>
Backups Salvos ({backups.length})
</h3>
{listLoading ? (
<div className="text-center py-8">
<i className="ri-loader-4-line animate-spin text-primary text-3xl"></i>
<p className="text-gray-600 dark:text-gray-400 mt-2">Carregando backups...</p>
</div>
) : backups.length === 0 ? (
<div className="text-center py-12 bg-gray-50 dark:bg-white/5 rounded-xl border border-gray-200 dark:border-white/10">
<i className="ri-file-archive-line text-gray-400 text-4xl mb-3 block"></i>
<p className="text-gray-600 dark:text-gray-400">Nenhum backup realizado ainda</p>
</div>
) : (
<div className="space-y-3">
{backups.map((backup) => (
<div
key={backup.id}
className="bg-white dark:bg-secondary p-4 rounded-xl border border-gray-200 dark:border-white/10 hover:shadow-md transition-shadow flex items-center justify-between gap-4"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2">
<div className={`w-3 h-3 rounded-full ${
backup.status === 'success'
? 'bg-green-500'
: 'bg-red-500'
}`}></div>
<h4 className="font-bold text-gray-900 dark:text-white truncate">
{backup.filename}
</h4>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400">
<i className="ri-calendar-line mr-1"></i>
{formatDate(backup.timestamp)}
<span className="mx-2"></span>
<i className="ri-hard-drive-line mr-1"></i>
{formatSize(backup.size)}
</p>
{backup.message && (
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
{backup.message}
</p>
)}
</div>
<div className="flex items-center gap-2 shrink-0">
<button
onClick={() => downloadBackup(backup.filename)}
title="Baixar backup"
className="p-2 hover:bg-gray-100 dark:hover:bg-white/10 rounded-lg transition-colors text-blue-600 dark:text-blue-400"
>
<i className="ri-download-line text-xl"></i>
</button>
<button
onClick={() => deleteBackup(backup.id)}
title="Remover backup"
className="p-2 hover:bg-red-100 dark:hover:bg-red-900/20 rounded-lg transition-colors text-red-600 dark:text-red-400"
>
<i className="ri-delete-bin-6-line text-xl"></i>
</button>
</div>
</div>
))}
</div>
)}
</div>
{/* Informações */}
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-xl p-4">
<p className="text-sm text-amber-900 dark:text-amber-200">
<i className="ri-information-line mr-2"></i>
Os backups incluem banco de dados PostgreSQL e todos os arquivos do MinIO.
Armazene-os em local seguro para recuperação em caso de necessidade.
</p>
</div>
</div>
);
}

View File

@@ -194,6 +194,70 @@
"accept": "Accept", "accept": "Accept",
"decline": "Decline" "decline": "Decline"
}, },
"privacy": {
"title": "Privacy Policy",
"intro": "OCCTO Engineering values the privacy of its users and clients. This Privacy Policy describes how we collect, use and protect your personal information when using our website and services.",
"section1": {
"title": "1. Information Collection",
"content": "We collect information that you provide directly to us, such as when you fill out our contact form, request a quote or subscribe to our newsletter. The information may include name, email, phone and details about your company or project."
},
"section2": {
"title": "2. Use of Information",
"intro": "We use the collected information to:",
"items": [
"Respond to your inquiries and quote requests;",
"Provide information about our engineering services and technical reports;",
"Improve the user experience on our website;",
"Comply with legal and regulatory obligations."
]
},
"section3": {
"title": "3. Data Protection",
"content": "We adopt appropriate technical and organizational security measures to protect your personal information against unauthorized access, alteration, disclosure or destruction."
},
"section4": {
"title": "4. Sharing Information",
"content": "We do not sell, trade or transfer your personal information to third parties, except when necessary for the provision of our services (eg: technical partners involved in a specific project) or when required by law."
},
"section5": {
"title": "5. Cookies",
"content": "Our website may use cookies to improve navigation and understand how visitors interact with our content. You can disable cookies in your browser settings if you prefer."
},
"section6": {
"title": "6. Contact",
"content": "If you have questions about this Privacy Policy, please contact us via email: contato@octto.com.br."
},
"lastUpdate": "Last updated: November 2025."
},
"terms": {
"title": "Terms of Use",
"intro": "Welcome to OCCTO Engineering's website. By accessing and using this website, you agree to comply with and be bound by the following Terms of Use. If you disagree with any part of these terms, please do not use our website.",
"section1": {
"title": "1. Website Use",
"content": "The content of this website is for general informational purposes only about our mechanical engineering services, reports and projects. We reserve the right to modify or discontinue any aspect of the website at any time."
},
"section2": {
"title": "2. Intellectual Property",
"content": "All content on this website, including texts, graphics, logos, icons, images and software, is owned by OCCTO Engineering or its content providers and is protected by Brazilian and international copyright laws."
},
"section3": {
"title": "3. Limitation of Liability",
"content": "OCCTO Engineering is not responsible for any direct, indirect, incidental or consequential damages resulting from the use or inability to use this website or any information contained therein. The technical information provided on the website does not substitute professional consultation and the issuance of specific technical reports for each case."
},
"section4": {
"title": "4. Links to Third Parties",
"content": "Our website may contain links to third-party websites. These links are provided for your convenience only. OCCTO Engineering has no control over the content of these sites and assumes no responsibility for them."
},
"section5": {
"title": "5. Changes to Terms",
"content": "We may revise these Terms of Use at any time. By using this website, you agree to be bound by the current version of these Terms of Use."
},
"section6": {
"title": "6. Applicable Law",
"content": "These terms are governed by and interpreted in accordance with the laws of the Federative Republic of Brazil. Any dispute related to these terms will be submitted to the exclusive jurisdiction of the competent courts."
},
"lastUpdate": "Last updated: November 2025."
},
"common": { "common": {
"loading": "Loading...", "loading": "Loading...",
"error": "Error", "error": "Error",

View File

@@ -194,6 +194,70 @@
"accept": "Aceptar", "accept": "Aceptar",
"decline": "Rechazar" "decline": "Rechazar"
}, },
"privacy": {
"title": "Política de Privacidad",
"intro": "OCCTO Ingeniería valora la privacidad de sus usuarios y clientes. Esta Política de Privacidad describe cómo recopilamos, utilizamos y protegemos su información personal al utilizar nuestro sitio web y servicios.",
"section1": {
"title": "1. Recopilación de Información",
"content": "Recopilamos información que nos proporciona directamente, como cuando completa nuestro formulario de contacto, solicita un presupuesto o se suscribe a nuestro boletín informativo. La información puede incluir nombre, correo electrónico, teléfono y detalles sobre su empresa o proyecto."
},
"section2": {
"title": "2. Uso de la Información",
"intro": "Utilizamos la información recopilada para:",
"items": [
"Responder a sus consultas y solicitudes de presupuesto;",
"Proporcionar información sobre nuestros servicios de ingeniería e informes técnicos;",
"Mejorar la experiencia del usuario en nuestro sitio web;",
"Cumplir con obligaciones legales y normativas."
]
},
"section3": {
"title": "3. Protección de Datos",
"content": "Adoptamos medidas de seguridad técnicas y organizacionales apropiadas para proteger su información personal contra acceso no autorizado, alteración, divulgación o destrucción."
},
"section4": {
"title": "4. Compartir Información",
"content": "No vendemos, canjeamos ni transferimos su información personal a terceros, excepto cuando sea necesario para la prestación de nuestros servicios (ej: socios técnicos involucrados en un proyecto específico) o cuando lo exija la ley."
},
"section5": {
"title": "5. Cookies",
"content": "Nuestro sitio web puede utilizar cookies para mejorar la navegación y entender cómo los visitantes interactúan con nuestro contenido. Puede desactivar las cookies en la configuración de su navegador si lo prefiere."
},
"section6": {
"title": "6. Contacto",
"content": "Si tiene preguntas sobre esta Política de Privacidad, comuníquese con nosotros a través de correo electrónico: contato@octto.com.br."
},
"lastUpdate": "Última actualización: Noviembre de 2025."
},
"terms": {
"title": "Términos de Uso",
"intro": "Bienvenido al sitio web de OCCTO Ingeniería. Al acceder y utilizar este sitio web, usted acepta cumplir y estar vinculado a los siguientes Términos de Uso. Si no está de acuerdo con ninguna parte de estos términos, por favor no utilice nuestro sitio web.",
"section1": {
"title": "1. Uso del Sitio",
"content": "El contenido de este sitio web es solo para propósitos informativos generales sobre nuestros servicios de ingeniería mecánica, informes y proyectos. Nos reservamos el derecho de alterar o descontinuar cualquier aspecto del sitio en cualquier momento."
},
"section2": {
"title": "2. Propiedad Intelectual",
"content": "Todo el contenido presente en este sitio web, incluidos textos, gráficos, logotipos, iconos, imágenes y software, es propiedad de OCCTO Ingeniería o de sus proveedores de contenido y está protegido por las leyes de derechos de autor de Brasil e internacionales."
},
"section3": {
"title": "3. Limitación de Responsabilidad",
"content": "OCCTO Ingeniería no se responsabiliza por ningún daño directo, indirecto, incidental o consecuente que resulte del uso o la incapacidad de usar este sitio web o cualquier información contenida en el mismo. La información técnica proporcionada en el sitio web no sustituye la consulta profesional y la emisión de informes técnicos específicos para cada caso."
},
"section4": {
"title": "4. Enlaces a Terceros",
"content": "Nuestro sitio web puede contener enlaces a sitios web de terceros. Estos enlaces se proporcionan solo para su conveniencia. OCCTO Ingeniería no tiene control sobre el contenido de estos sitios y no asume responsabilidad por ellos."
},
"section5": {
"title": "5. Cambios en los Términos",
"content": "Podemos revisar estos Términos de Uso en cualquier momento. Al utilizar este sitio web, usted acepta estar vinculado por la versión actual de estos Términos de Uso."
},
"section6": {
"title": "6. Ley Aplicable",
"content": "Estos términos se rigen e interpretan de acuerdo con las leyes de la República Federativa de Brasil. Cualquier disputa relacionada con estos términos será sometida a la jurisdicción exclusiva de los tribunales competentes."
},
"lastUpdate": "Última actualización: Noviembre de 2025."
},
"common": { "common": {
"loading": "Cargando...", "loading": "Cargando...",
"error": "Error", "error": "Error",

View File

@@ -194,6 +194,70 @@
"accept": "Aceitar", "accept": "Aceitar",
"decline": "Recusar" "decline": "Recusar"
}, },
"privacy": {
"title": "Política de Privacidade",
"intro": "A Octto Engenharia valoriza a privacidade de seus usuários e clientes. Esta Política de Privacidade descreve como coletamos, usamos e protegemos suas informações pessoais ao utilizar nosso site e serviços.",
"section1": {
"title": "1. Coleta de Informações",
"content": "Coletamos informações que você nos fornece diretamente, como quando preenche nosso formulário de contato, solicita um orçamento ou se inscreve em nossa newsletter. As informações podem incluir nome, e-mail, telefone e detalhes sobre sua empresa ou projeto."
},
"section2": {
"title": "2. Uso das Informações",
"intro": "Utilizamos as informações coletadas para:",
"items": [
"Responder a suas consultas e solicitações de orçamento;",
"Fornecer informações sobre nossos serviços de engenharia e laudos técnicos;",
"Melhorar a experiência do usuário em nosso site;",
"Cumprir obrigações legais e regulatórias."
]
},
"section3": {
"title": "3. Proteção de Dados",
"content": "Adotamos medidas de segurança técnicas e organizacionais adequadas para proteger seus dados pessoais contra acesso não autorizado, alteração, divulgação ou destruição."
},
"section4": {
"title": "4. Compartilhamento de Informações",
"content": "Não vendemos, trocamos ou transferimos suas informações pessoais para terceiros, exceto quando necessário para a prestação de nossos serviços (ex: parceiros técnicos envolvidos em um projeto específico) ou quando exigido por lei."
},
"section5": {
"title": "5. Cookies",
"content": "Nosso site pode utilizar cookies para melhorar a navegação e entender como os visitantes interagem com nosso conteúdo. Você pode desativar os cookies nas configurações do seu navegador, se preferir."
},
"section6": {
"title": "6. Contato",
"content": "Se você tiver dúvidas sobre esta Política de Privacidade, entre em contato conosco através do e-mail: contato@octto.com.br."
},
"lastUpdate": "Última atualização: Novembro de 2025."
},
"terms": {
"title": "Termos de Uso",
"intro": "Bem-vindo ao site da Octto Engenharia. Ao acessar e utilizar este site, você concorda em cumprir e estar vinculado aos seguintes Termos de Uso. Se você não concordar com qualquer parte destes termos, por favor, não utilize nosso site.",
"section1": {
"title": "1. Uso do Site",
"content": "O conteúdo deste site é apenas para fins informativos gerais sobre nossos serviços de engenharia mecânica, laudos e projetos. Reservamo-nos o direito de alterar ou descontinuar qualquer aspecto do site a qualquer momento."
},
"section2": {
"title": "2. Propriedade Intelectual",
"content": "Todo o conteúdo presente neste site, incluindo textos, gráficos, logotipos, ícones, imagens e software, é propriedade da Octto Engenharia ou de seus fornecedores de conteúdo e é protegido pelas leis de direitos autorais do Brasil e internacionais."
},
"section3": {
"title": "3. Limitação de Responsabilidade",
"content": "A Octto Engenharia não se responsabiliza por quaisquer danos diretos, indiretos, incidentais ou consequenciais resultantes do uso ou da incapacidade de uso deste site ou de qualquer informação nele contida. As informações técnicas fornecidas no site não substituem a consulta profissional e a emissão de laudos técnicos específicos para cada caso."
},
"section4": {
"title": "4. Links para Terceiros",
"content": "Nosso site pode conter links para sites de terceiros. Estes links são fornecidos apenas para sua conveniência. A Octto Engenharia não tem controle sobre o conteúdo desses sites e não assume responsabilidade por eles."
},
"section5": {
"title": "5. Alterações nos Termos",
"content": "Podemos revisar estes Termos de Uso a qualquer momento. Ao utilizar este site, você concorda em ficar vinculado à versão atual desses Termos de Uso."
},
"section6": {
"title": "6. Legislação Aplicável",
"content": "Estes termos são regidos e interpretados de acordo com as leis da República Federativa do Brasil. Qualquer disputa relacionada a estes termos será submetida à jurisdição exclusiva dos tribunais competentes."
},
"lastUpdate": "Última atualização: Novembro de 2025."
},
"common": { "common": {
"loading": "Carregando...", "loading": "Carregando...",
"error": "Erro", "error": "Erro",