chore: atualiza setup, backup e toolchain

This commit is contained in:
Felipe Coutinho
2026-03-20 18:43:03 +00:00
parent e4dd221709
commit 3e0ce15258
10 changed files with 1144 additions and 335 deletions

7
.gitignore vendored
View File

@@ -104,11 +104,10 @@ docker-compose.override.yml
.claude/
.gemini/
.cursor/
CLAUDE.md
AGENTS.md
QWEN.md
claude.md
agents.md
AGENTS.md
# === Backups locais ===
/backup/
# === Backups e Temporários ===
*.bak

318
CLAUDE.md Normal file
View File

@@ -0,0 +1,318 @@
# CLAUDE.md - OpenMonetis
> Self-hosted personal finance app (Next.js 16, React 19, PostgreSQL, Drizzle ORM, Better Auth, Tailwind 4, shadcn/ui).
> Portuguese UI, English folders/imports. Linter: Biome 2.x. Package manager: pnpm.
## Related Projects
- **OpenMonetis Companion** (`~/github/openmonetis-companion`): Android app que captura notificacoes de apps bancarios e envia para o OpenMonetis via API. Os itens chegam na feature `inbox` para revisao.
---
## Critical Rules
1. **Sempre filtrar por `userId`** em queries.
2. **Usar `getAdminPayerId(userId)`** de `src/shared/lib/payers/get-admin-id.ts` ao inves de JOIN com `payers` para descobrir o admin.
3. **Periods** usam formato `YYYY-MM` (ex: `"2025-11"`). Utils em `src/shared/utils/period/`.
4. **Moeda**: R$ com 2 decimais. DB: `numeric(12, 2)`. Utils em `src/shared/utils/currency.ts`.
5. **Revalidation**: usar `revalidateForEntity("entity")` de `src/shared/lib/actions/helpers.ts` apos mutations.
6. **Versionamento**: registrar mudancas no `CHANGELOG.md` seguindo Keep a Changelog.
7. **Comunicacao**: responder em portugues clara e direta com o time.
---
## Architecture
### Feature-First
- `src/app/`: roteamento, layouts, loading states e paginas finas
- `src/features/`: codigo de dominio por feature
- `src/shared/`: tudo que e genuinamente reutilizado entre features
- `src/db/`: schema do banco
### Regra Feature vs Shared
Use esta pergunta:
> Se eu deletar esta feature, este arquivo deveria sumir junto?
- Sim: vai para `src/features/<feature>/`
- Nao: vai para `src/shared/`
### Features nao importam outras features
Se um contrato cruza dominios, ele deve morar em `src/shared/`.
Exemplos comuns:
- auth: `src/shared/lib/auth/*`
- db: `src/shared/lib/db.ts`
- revalidation helpers: `src/shared/lib/actions/*`
- payers cross-domain helpers: `src/shared/lib/payers/*`
- period/currency/date: `src/shared/utils/*`
- shadcn/ui: `src/shared/components/ui/*`
---
## Directory Structure
```text
src/
├── app/
│ ├── (auth)/
│ │ ├── login/page.tsx
│ │ └── signup/page.tsx
│ ├── (dashboard)/
│ │ ├── dashboard/
│ │ ├── transactions/
│ │ ├── cards/
│ │ │ └── [cardId]/invoice/
│ │ ├── accounts/
│ │ │ └── [accountId]/statement/
│ │ ├── categories/
│ │ │ ├── [categoryId]/
│ │ │ └── history/
│ │ ├── budgets/
│ │ ├── payers/
│ │ │ └── [payerId]/
│ │ ├── notes/
│ │ ├── insights/
│ │ ├── calendar/
│ │ ├── inbox/
│ │ ├── changelog/
│ │ ├── reports/
│ │ │ ├── category-trends/
│ │ │ ├── card-usage/
│ │ │ ├── installment-analysis/
│ │ │ └── establishments/
│ │ └── settings/
│ ├── (landing-page)/
│ ├── api/
│ ├── globals.css
│ └── layout.tsx
├── features/
│ ├── auth/
│ ├── landing/
│ ├── dashboard/
│ ├── transactions/
│ ├── cards/
│ ├── invoices/
│ ├── accounts/
│ ├── categories/
│ ├── budgets/
│ ├── payers/
│ ├── notes/
│ ├── insights/
│ ├── calendar/
│ ├── inbox/
│ ├── reports/
│ └── settings/
├── shared/
│ ├── components/
│ │ ├── ui/
│ │ ├── navigation/
│ │ ├── providers/
│ │ ├── month-picker/
│ │ ├── logo-picker/
│ │ ├── calculator/
│ │ ├── entity-avatar/
│ │ └── skeletons/
│ ├── hooks/
│ ├── lib/
│ │ ├── actions/
│ │ ├── auth/
│ │ ├── accounts/
│ │ ├── cards/
│ │ ├── calculator/
│ │ ├── categories/
│ │ ├── email/
│ │ ├── installments/
│ │ ├── invoices/
│ │ ├── logo/
│ │ ├── payers/
│ │ ├── schemas/
│ │ ├── transfers/
│ │ ├── types/
│ │ └── db.ts
│ └── utils/
│ ├── period/
│ ├── currency.ts
│ ├── date.ts
│ ├── financial-dates.ts
│ ├── percentage.ts
│ ├── category-colors.ts
│ ├── calendar.ts
│ ├── math.ts
│ ├── number.ts
│ ├── string.ts
│ ├── initials.ts
│ ├── icons.tsx
│ ├── export-branding.ts
│ ├── ui.ts
│ └── calculator.ts
└── db/
└── schema.ts
```
---
## Import Patterns
### Preferidos
```ts
import { getUser } from "@/shared/lib/auth/server";
import { revalidateForEntity } from "@/shared/lib/actions/helpers";
import { parsePeriodParam } from "@/shared/utils/period";
import { TransactionsPage } from "@/features/transactions/components/page/transactions-page";
import { fetchLancamentos } from "@/features/transactions/queries";
```
### Evitar
```ts
import { Something } from "@/components/...";
import { Something } from "@/lib/...";
import { something } from "@/app/(dashboard)/...";
```
---
## App Router Pattern
Paginas em `src/app/` devem ser finas:
```ts
import { getUser } from "@/shared/lib/auth/server";
import { TransactionsPage } from "@/features/transactions/components/page/transactions-page";
import { fetchLancamentos } from "@/features/transactions/queries";
export default async function Page() {
const user = await getUser();
const data = await fetchLancamentos([/* filters */]);
return <TransactionsPage {...data} />;
}
```
Layouts, `loading.tsx` e metadata continuam em `src/app/`.
---
## Naming
### Routes / folders
| Portugues | English |
|---|---|
| `lancamentos` | `transactions` |
| `cartoes` | `cards` |
| `contas` | `accounts` |
| `categorias` | `categories` |
| `orcamentos` | `budgets` |
| `pagadores` | `payers` |
| `anotacoes` | `notes` |
| `calendario` | `calendar` |
| `ajustes` | `settings` |
| `pre-lancamentos` | `inbox` |
| `relatorios/tendencias` | `reports/category-trends` |
| `relatorios/uso-cartoes` | `reports/card-usage` |
| `relatorios/analise-parcelas` | `reports/installment-analysis` |
| `relatorios/estabelecimentos` | `reports/establishments` |
| `contas/[contaId]/extrato` | `accounts/[accountId]/statement` |
| `cartoes/[cartaoId]/fatura` | `cards/[cardId]/invoice` |
| `categorias/historico` | `categories/history` |
| `changelog` | `settings/changelog` |
### Files
- preferir `kebab-case`
- preferir nomes em ingles
- manter nomes internos de tipos/funcoes somente quando a troca aumentar risco sem ganho real
---
## Commands
```bash
pnpm run dev
pnpm run build
pnpm run lint
pnpm run lint:fix
pnpm exec next typegen
pnpm exec tsc --noEmit
pnpm run db:generate
pnpm run db:push
pnpm run db:studio
pnpm run docker:up:db
```
---
## Revalidation
Arquivo: `src/shared/lib/actions/helpers.ts`
- atualizar sempre os paths em ingles
- lembrar de manter a tag `"dashboard"` para invalidacoes financeiras
---
## Auth
- `getUser()` / `getUserId()` em `src/shared/lib/auth/server.ts`
- sessao deduplicada por request com `React.cache()`
---
## Dashboard Fetcher
Padrao recomendado:
```ts
import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
export async function fetchData(userId: string, period: string) {
const adminPayerId = await getAdminPayerId(userId);
if (!adminPayerId) return [];
return db.query.transactions.findMany({
where: /* sempre com userId + adminPayerId + period */,
});
}
```
---
## New Feature Checklist
1. Criar a rota fina em `src/app/(dashboard)/<feature>/page.tsx`
2. Criar a feature em `src/features/<feature>/`
3. Separar:
- `components/`
- `queries.ts`
- `actions.ts`
- `types.ts` ou `schemas.ts` quando fizer sentido
4. Extrair para `src/shared/` tudo que for reutilizavel
5. Atualizar navegacao e `revalidateForEntity()` se a feature tiver CRUD
6. Rodar:
- `pnpm exec next typegen`
- `pnpm exec tsc --noEmit`
- `pnpm run lint`
---
## Response Style
Quando o time pedir avaliacao de plano ou feature:
1. Responder em portugues simples.
2. Listar 3-5 problemas principais.
3. Fechar com decisao pratica:
- aprova agora
- nao aprova agora
- o que ajustar antes de comecar codigo
Exemplo:
- "Nao aprovaria para comecar codigo imediatamente."
- "Primeiro ajustaria o doc com estes 5 pontos."

102
README.md
View File

@@ -6,10 +6,9 @@
Projeto pessoal de gestão financeira. Self-hosted, manual e open source.
</p>
> **📢 Este projeto foi renomeado de OpenSheets para OpenMonetis.** Se você conhecia o projeto pelo nome anterior, é o mesmo — só mudou o nome!
> **⚠️ Não há versão online hospedada.** Você precisa clonar o repositório e rodar localmente ou no seu próprio servidor.
[![Version](https://img.shields.io/badge/version-2.0.0-blue?style=flat-square)](CHANGELOG.md)
[![Next.js](https://img.shields.io/badge/Next.js-black?style=flat-square&logo=next.js)](https://nextjs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org/)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-blue?style=flat-square&logo=postgresql)](https://www.postgresql.org/)
@@ -29,7 +28,8 @@
## 📖 Índice
- [Sobre o Projeto](#-sobre-o-projeto)
- [Início Rápido](#-início-rápido)
- [Instalação via Script](#-instalação-via-script)
- [Início Rápido (manual)](#-início-rápido)
- [Scripts Disponíveis](#-scripts-disponíveis)
- [Docker](#-docker)
- [Variáveis de Ambiente](#-variáveis-de-ambiente)
@@ -60,7 +60,7 @@ A ideia é simples: ter um lugar onde consigo ver todas as minhas contas, cartõ
💰 **Contas e transações** — Contas bancárias, cartões, dinheiro. Receitas, despesas e transferências. Categorização, extratos detalhados e importação em massa.
📊 **Dashboard e relatórios**20+ widgets interativos, gráficos de evolução, comparativos por categoria, tendências, uso de cartões, top estabelecimentos. Exportação em PDF e Excel.
📊 **Dashboard e relatórios**Widgets interativos de métricas, gráficos de evolução, comparativos por categoria, tendências, uso de cartões, top estabelecimentos. Exportação em PDF e Excel.
💳 **Faturas de cartão** — Acompanhe faturas por período, controle limites e vencimentos.
@@ -78,13 +78,13 @@ A ideia é simples: ter um lugar onde consigo ver todas as minhas contas, cartõ
📲 **OpenMonetis Companion** — App Android que captura notificações bancárias (Nubank, Itaú, Bradesco, Inter, C6 e outros) e envia como pré-lançamentos para revisão. [Repositório](https://github.com/felipegcoutinho/openmonetis-companion).
⚙️ **Personalização** — Tema dark/light, modo privacidade e preferências por usuário.
⚙️ **Personalização** — Tema dark/light e modo privacidade.
### Stack técnica
- **Next.js** (App Router, Turbopack) + **React** + **TypeScript**
- **PostgreSQL** + **Drizzle ORM**
- **Better Auth** (email/senha + OAuth)
- **Better Auth** (email/senha, OAuth, Passkeys/WebAuthn)
- **shadcn/ui** (Radix UI) + **Tailwind CSS**
- **Docker** (multi-stage build)
- **Biome** (linting + formatting)
@@ -92,7 +92,30 @@ A ideia é simples: ter um lugar onde consigo ver todas as minhas contas, cartõ
---
## 🚀 Início Rápido
## Instalação via Script
A forma mais rápida de instalar. O script verifica dependências, configura o `.env` interativamente e sobe o banco automaticamente.
**Pré-requisito:** Node.js 22+
```bash
# Mac / Linux / WSL
curl -fsSL https://raw.githubusercontent.com/felipegcoutinho/openmonetis/main/setup.mjs -o setup.mjs && node setup.mjs
# Windows (PowerShell)
curl -o setup.mjs https://raw.githubusercontent.com/felipegcoutinho/openmonetis/main/setup.mjs ; node setup.mjs
```
O script irá:
- Verificar Node, pnpm, Git e Docker
- Perguntar se quer banco local (Docker) ou remoto (Supabase, Neon, etc.)
- Gerar o `BETTER_AUTH_SECRET` automaticamente
- Configurar opcionais: Google OAuth, e-mail, IA, domínio público
- Clonar o repositório, instalar dependências e aplicar o schema
---
## 🚀 Início Rápido (manual)
### Pré-requisitos
@@ -251,32 +274,49 @@ OPENROUTER_API_KEY=
## 🏗️ Arquitetura
O projeto segue arquitetura **feature-first** dentro de `src/`:
```
openmonetis/
├── app/ # Next.js App Router
│ ├── api/ # API Routes (auth, health, inbox)
├── (auth)/ # Login e cadastro
│ ├── (dashboard)/ # Rotas protegidas
└── (landing-page)/ # Página inicial pública
├── src/
│ ├── app/ # Next.js App Router (rotas finas)
│ ├── api/ # API Routes (auth, health, inbox)
├── (auth)/ # Login e cadastro
│ ├── (dashboard)/ # Rotas protegidas (transactions, cards, accounts, etc.)
│ │ └── (landing-page)/ # Página inicial pública
│ │
│ ├── features/ # Código de domínio por feature
│ │ ├── dashboard/ # Widgets, queries e métricas
│ │ ├── transactions/ # Lançamentos, ações em lote, exportação
│ │ ├── cards/ # Cartões de crédito
│ │ ├── invoices/ # Faturas
│ │ ├── accounts/ # Contas bancárias
│ │ ├── categories/ # Categorias e histórico
│ │ ├── budgets/ # Orçamentos
│ │ ├── payers/ # Pagadores e compartilhamento
│ │ ├── inbox/ # Pré-lançamentos do Companion
│ │ ├── insights/ # Análises com IA
│ │ ├── reports/ # Relatórios e exportações
│ │ ├── notes/ # Anotações
│ │ ├── calendar/ # Calendário financeiro
│ │ ├── settings/ # Ajustes do usuário
│ │ ├── landing/ # Landing page
│ │ └── auth/ # Formulários de autenticação
│ │
│ ├── shared/ # Código reutilizado entre features
│ │ ├── components/ # UI compartilhada (shadcn/ui, navigation, skeletons...)
│ │ ├── hooks/ # React hooks globais
│ │ ├── lib/ # Helpers de domínio (auth, db, payers, schemas, email...)
│ │ └── utils/ # Utilitários (currency, date, period, math, string...)
│ │
│ └── db/
│ └── schema.ts # Drizzle schema (fonte única de verdade)
├── components/ # React Components (~200 arquivos)
│ ├── ui/ # shadcn/ui (40+ componentes)
│ ├── dashboard/ # Widgets do dashboard (20+)
│ └── [feature]/ # Componentes por feature
├── lib/ # Lógica de negócio
│ ├── auth/ # Auth helpers
│ ├── dashboard/ # Fetchers do dashboard
│ ├── actions/ # Server Actions helpers
│ ├── schemas/ # Zod schemas
│ └── utils/ # Currency, date, period utils
├── db/schema.ts # Drizzle schema
├── hooks/ # React hooks customizados
├── public/ # Assets estáticos
├── scripts/ # Scripts utilitários
├── Dockerfile # Multi-stage build
├── docker-compose.yml # Orquestração
├── public/ # Assets estáticos (imagens, logos, fontes)
├── drizzle/ # Migrations geradas
├── scripts/ # Scripts utilitários (migrations, dev)
├── Dockerfile # Multi-stage build (~200MB, non-root)
├── docker-compose.yml # Orquestração app + PostgreSQL
└── proxy.ts # Middleware (auth + multi-domínio)
```
@@ -291,7 +331,7 @@ openmonetis/
5. **Push:** `git push origin feature/minha-feature`
6. Abra um **Pull Request**
Use TypeScript, commits semânticos e documente features novas.
Antes de começar, leia o [`CLAUDE.md`](CLAUDE.md) — ele documenta a arquitetura, convenções de nomenclatura, regras de queries e o checklist para novas features. Use TypeScript, commits semânticos e mantenha o `CHANGELOG.md` atualizado.
---

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.6/schema.json",
"$schema": "https://biomejs.dev/schemas/2.4.7/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",

View File

@@ -10,9 +10,6 @@ const nextConfig: NextConfig = {
turbopackFileSystemCacheForDev: true,
},
reactCompiler: true,
typescript: {
ignoreBuildErrors: true,
},
images: {
remotePatterns: [new URL("https://lh3.googleusercontent.com/**")],
},

View File

@@ -24,17 +24,18 @@
"docker:logs:app": "docker compose logs -f app",
"docker:logs:db": "docker compose logs -f db",
"docker:restart": "docker compose restart",
"docker:rebuild": "docker compose up --build --force-recreate"
"docker:rebuild": "docker compose up --build --force-recreate",
"backup": "bash scripts/backup.sh"
},
"dependencies": {
"@ai-sdk/anthropic": "^3.0.60",
"@ai-sdk/anthropic": "^3.0.62",
"@ai-sdk/google": "^3.0.51",
"@ai-sdk/openai": "^3.0.45",
"@ai-sdk/openai": "^3.0.46",
"@better-auth/passkey": "^1.5.5",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@openrouter/ai-sdk-provider": "^2.3.1",
"@openrouter/ai-sdk-provider": "^2.3.3",
"@radix-ui/react-alert-dialog": "1.1.15",
"@radix-ui/react-avatar": "1.1.11",
"@radix-ui/react-checkbox": "1.3.3",
@@ -66,9 +67,9 @@
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"drizzle-orm": "0.45.1",
"jspdf": "^4.2.0",
"jspdf": "^4.2.1",
"jspdf-autotable": "^5.0.7",
"next": "16.1.6",
"next": "16.1.7",
"next-themes": "0.4.6",
"pg": "8.20.0",
"radix-ui": "^1.4.3",
@@ -76,7 +77,7 @@
"react-day-picker": "^9.14.0",
"react-dom": "19.2.4",
"recharts": "3.8.0",
"resend": "^6.9.3",
"resend": "^6.9.4",
"sonner": "2.0.7",
"tailwind-merge": "3.5.0",
"vaul": "1.1.2",
@@ -84,15 +85,15 @@
"zod": "4.3.6"
},
"devDependencies": {
"@biomejs/biome": "2.4.7",
"@tailwindcss/postcss": "4.2.1",
"@biomejs/biome": "2.4.8",
"@tailwindcss/postcss": "4.2.2",
"@types/canvas-confetti": "^1.9.0",
"@types/node": "25.5.0",
"@types/pg": "^8.18.0",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"dotenv": "^17.3.1",
"drizzle-kit": "0.31.9",
"drizzle-kit": "0.31.10",
"tailwindcss": "4.2.1",
"tsx": "4.21.0",
"typescript": "5.9.3"

535
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

105
scripts/backup.sh Executable file
View File

@@ -0,0 +1,105 @@
#!/bin/bash
# ==============================================================
# openmonetis-backup.sh
# Backup automático do PostgreSQL para Google Drive via rclone
# Suporta: banco remoto (Supabase/etc) ou Docker local
# ==============================================================
set -euo pipefail
export TZ="America/Sao_Paulo"
# Raiz do projeto (um nível acima de scripts/)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
if [[ -f "$PROJECT_DIR/.env" ]]; then
set -a
# shellcheck disable=SC1091
source "$PROJECT_DIR/.env"
set +a
else
echo "ERRO: .env não encontrado em $PROJECT_DIR" >&2
exit 1
fi
# ============================================================
# CONFIGURAÇÃO — ajuste aqui
# ============================================================
# Modo de conexão: "remote" (Supabase/URL) ou "docker" (container local)
DB_MODE="remote"
# --- Modo remote ---
# Usa DATABASE_URL do .env (porta 6543 funciona com --no-owner --no-privileges)
REMOTE_DB_URL="${DATABASE_URL}"
# --- Modo docker ---
DOCKER_CONTAINER="openmonetis_postgres"
DOCKER_DB_NAME="openmonetis_db"
DOCKER_DB_USER="openmonetis"
# --- Destino e retenção ---
BACKUP_DIR="$PROJECT_DIR/backup"
GDRIVE_REMOTE="gdrive:BACKUP OPENMONETIS"
RETENTION_LOCAL_DAYS=7
RETENTION_REMOTE_DAYS=30
# ============================================================
# SCRIPT — não alterar abaixo
# ============================================================
TIMESTAMP=$(date +"%Y-%m-%d_%H-%M")
LOG_PREFIX="[$(date '+%Y-%m-%d %H:%M:%S')]"
log() { echo "$LOG_PREFIX $*"; }
mkdir -p "$BACKUP_DIR"
DUMP_FILE="$BACKUP_DIR/openmonetis_${TIMESTAMP}.dump"
SQL_FILE="$BACKUP_DIR/openmonetis_${TIMESTAMP}.sql.gz"
log "Iniciando backup (modo: $DB_MODE)..."
# --- Dump ---
if [[ "$DB_MODE" == "remote" ]]; then
# --no-owner --no-privileges: necessário no Supabase (roles gerenciados internamente)
pg_dump --format=custom --no-owner --no-privileges \
"$REMOTE_DB_URL" > "$DUMP_FILE"
pg_dump --no-owner --no-privileges \
"$REMOTE_DB_URL" | gzip > "$SQL_FILE"
elif [[ "$DB_MODE" == "docker" ]]; then
docker exec "$DOCKER_CONTAINER" pg_dump \
-U "$DOCKER_DB_USER" -Fc "$DOCKER_DB_NAME" > "$DUMP_FILE"
docker exec "$DOCKER_CONTAINER" pg_dump \
-U "$DOCKER_DB_USER" "$DOCKER_DB_NAME" | gzip > "$SQL_FILE"
else
log "ERRO: DB_MODE inválido ('$DB_MODE'). Use 'remote' ou 'docker'."
exit 1
fi
log "Dump concluído: $(du -sh "$DUMP_FILE" | cut -f1) (.dump) | $(du -sh "$SQL_FILE" | cut -f1) (.sql.gz)"
# --- Upload para Google Drive ---
if ! command -v rclone &>/dev/null; then
log "AVISO: rclone não encontrado. Pulando upload."
else
rclone copy "$BACKUP_DIR" "$GDRIVE_REMOTE" \
--include "openmonetis_*" \
--min-age 1s
log "Upload concluído → $GDRIVE_REMOTE"
# Limpeza remota
rclone delete "$GDRIVE_REMOTE" \
--min-age "${RETENTION_REMOTE_DAYS}d" \
--include "openmonetis_*"
log "Limpeza remota: mantidos últimos $RETENTION_REMOTE_DAYS dias."
fi
# --- Limpeza local ---
find "$BACKUP_DIR" -name "openmonetis_*" -mtime +"$RETENTION_LOCAL_DAYS" -delete
log "Limpeza local: mantidos últimos $RETENTION_LOCAL_DAYS dias."
log "Backup finalizado com sucesso."

View File

@@ -27,12 +27,23 @@ fi
if [ -f .env.example ]; then
cp .env.example .env
echo "✅ Arquivo .env criado a partir de .env.example"
echo ""
echo "⚠️ IMPORTANTE: Edite o arquivo .env e configure:"
echo " - DATABASE_URL"
echo " - BETTER_AUTH_SECRET (gere com: openssl rand -base64 32)"
echo " - Outras variáveis necessárias"
else
echo "❌ Erro: .env.example não encontrado!"
exit 1
fi
# Gerar BETTER_AUTH_SECRET automaticamente
if command -v openssl &> /dev/null; then
SECRET=$(openssl rand -base64 32)
sed -i.bak "s|BETTER_AUTH_SECRET=.*|BETTER_AUTH_SECRET=$SECRET|" .env && rm -f .env.bak
echo "✅ BETTER_AUTH_SECRET gerado automaticamente"
else
echo "⚠️ openssl não encontrado — configure BETTER_AUTH_SECRET manualmente:"
echo " openssl rand -base64 32"
fi
echo ""
echo "⚠️ IMPORTANTE: Edite o arquivo .env e configure:"
echo " - DATABASE_URL"
echo " - BETTER_AUTH_URL"
echo " - Demais variáveis opcionais (OAuth, e-mail, IA)"

365
setup.mjs Normal file
View File

@@ -0,0 +1,365 @@
#!/usr/bin/env node
/**
* OpenMonetis Setup Script
* Uso: node setup.mjs
*/
import { createInterface } from "readline";
import { execSync } from "child_process";
import { writeFileSync, existsSync } from "fs";
import { randomBytes } from "crypto";
import { resolve, join } from "path";
// ─── Cores e símbolos ────────────────────────────────────────────────────────
const c = {
reset: "\x1b[0m",
bold: "\x1b[1m",
dim: "\x1b[2m",
green: "\x1b[32m",
red: "\x1b[31m",
yellow: "\x1b[33m",
cyan: "\x1b[36m",
};
const sym = {
ok: `${c.green}${c.reset}`,
fail: `${c.red}${c.reset}`,
warn: `${c.yellow}!${c.reset}`,
arrow: `${c.cyan}${c.reset}`,
};
// ─── Helpers ─────────────────────────────────────────────────────────────────
function section(label) {
console.log(`\n${c.dim}── ${label} ${"─".repeat(Math.max(0, 48 - label.length))}${c.reset}`);
}
function runSilent(cmd) {
try {
return execSync(cmd, { stdio: "pipe" }).toString().trim();
} catch {
return null;
}
}
function run(cmd, opts = {}) {
execSync(cmd, { stdio: "pipe", ...opts });
}
function spinner(text) {
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
let i = 0;
const id = setInterval(() => {
process.stdout.write(`\r${c.cyan}${frames[i++ % frames.length]}${c.reset} ${text}`);
}, 80);
return {
stop: (msg) => { clearInterval(id); process.stdout.write(`\r${sym.ok} ${msg}\n`); },
fail: (msg) => { clearInterval(id); process.stdout.write(`\r${sym.fail} ${msg}\n`); },
};
}
const rl = createInterface({ input: process.stdin, output: process.stdout });
const ask = (q) => new Promise((res) => rl.question(q, res));
async function askDefault(question, defaultValue) {
const answer = await ask(`${question} [${c.dim}${defaultValue}${c.reset}]: `);
return answer.trim() || defaultValue;
}
async function askYesNo(question) {
const answer = await ask(`${question} ${c.dim}[s/N]${c.reset}: `);
return answer.trim().toLowerCase() === "s";
}
function abort(msg) {
console.log(`\n${sym.fail} ${msg}\n`);
rl.close();
process.exit(1);
}
// ─── Header ──────────────────────────────────────────────────────────────────
console.log(`
${c.bold}${c.cyan} OpenMonetis — Setup${c.reset}
${c.dim}Gestão financeira self-hosted${c.reset}
`);
// ─── ETAPA 1: Verificações do sistema ────────────────────────────────────────
section("Verificando sistema");
// Node
const nodeMajor = parseInt(process.versions.node.split(".")[0]);
if (nodeMajor < 22) {
console.log(`${sym.fail} Node.js ${process.versions.node} — requer 22+`);
console.log(` ${sym.arrow} https://nodejs.org`);
process.exit(1);
}
console.log(`${sym.ok} Node.js ${process.versions.node}`);
// pnpm
let pnpmVersion = runSilent("pnpm --version");
if (!pnpmVersion) {
process.stdout.write(`${sym.warn} pnpm não encontrado — instalando... `);
try {
run("npm install -g pnpm");
pnpmVersion = runSilent("pnpm --version");
process.stdout.write(`${sym.ok}\n`);
console.log(`${sym.ok} pnpm ${pnpmVersion}`);
} catch {
console.log(`\n${sym.fail} Falha ao instalar pnpm`);
console.log(` ${sym.arrow} npm install -g pnpm`);
process.exit(1);
}
} else {
console.log(`${sym.ok} pnpm ${pnpmVersion}`);
}
// Git
if (!runSilent("git --version")) {
console.log(`${sym.fail} Git não encontrado`);
console.log(` ${sym.arrow} https://git-scm.com`);
process.exit(1);
}
console.log(`${sym.ok} Git disponível`);
// Docker
const dockerAvailable = !!runSilent("docker --version");
if (dockerAvailable) {
console.log(`${sym.ok} Docker disponível`);
} else {
console.log(`${sym.warn} Docker não encontrado — banco local indisponível`);
}
// ─── ETAPA 2: Banco de dados ──────────────────────────────────────────────────
section("Banco de dados");
let databaseUrl;
let useLocalDocker = false;
if (dockerAvailable) {
console.log(` [1] PostgreSQL local via Docker ${c.dim}(recomendado)${c.reset}`);
console.log(` [2] URL remota ${c.dim}(Supabase, Neon, Railway...)${c.reset}\n`);
const dbChoice = await ask(`Escolha [1]: `);
if (dbChoice.trim() === "2") {
databaseUrl = await ask(`DATABASE_URL: `);
if (!databaseUrl.match(/^postgre(s|sql):\/\//)) {
abort("URL inválida — deve começar com postgresql:// ou postgres://");
}
} else {
useLocalDocker = true;
databaseUrl =
"postgresql://openmonetis:openmonetis_dev_password@localhost:5432/openmonetis_db";
console.log(`${sym.ok} Banco local selecionado`);
}
} else {
console.log(` ${c.dim}Insira a URL de um banco remoto (Supabase, Neon, Railway...)${c.reset}\n`);
databaseUrl = await ask(`DATABASE_URL: `);
if (!databaseUrl.match(/^postgre(s|sql):\/\//)) {
abort("URL inválida — deve começar com postgresql:// ou postgres://");
}
}
// ─── ETAPA 3: Autenticação ────────────────────────────────────────────────────
section("Autenticação");
const authSecret = randomBytes(32).toString("base64");
const betterAuthUrl = await askDefault("URL da aplicação", "http://localhost:3000");
console.log(`${sym.ok} BETTER_AUTH_SECRET gerado`);
console.log(`${sym.ok} BETTER_AUTH_URL: ${betterAuthUrl}`);
// ─── ETAPA 4: Opcionais ───────────────────────────────────────────────────────
section("Opcionais");
console.log(` ${c.dim}Deixe em branco e configure depois editando o .env${c.reset}\n`);
// Google OAuth
let googleClientId = "";
let googleClientSecret = "";
if (await askYesNo(" Google OAuth (login social)?")) {
googleClientId = await ask(" GOOGLE_CLIENT_ID: ");
googleClientSecret = await ask(" GOOGLE_CLIENT_SECRET: ");
}
// Resend
let resendApiKey = "";
let resendFromEmail = "";
if (await askYesNo(" E-mail via Resend (notificações e convites)?")) {
resendApiKey = await ask(" RESEND_API_KEY: ");
resendFromEmail = await ask(` RESEND_FROM_EMAIL [OpenMonetis <noreply@seudominio.com>]: `);
if (!resendFromEmail.trim()) resendFromEmail = "OpenMonetis <noreply@seudominio.com>";
}
// AI
let anthropicKey = "";
let openaiKey = "";
let googleAiKey = "";
let openrouterKey = "";
if (await askYesNo(" Insights com IA (Claude, GPT, Gemini, OpenRouter)?")) {
console.log(` ${c.dim}Deixe em branco o que não for usar${c.reset}`);
anthropicKey = await ask(" ANTHROPIC_API_KEY: ");
openaiKey = await ask(" OPENAI_API_KEY: ");
googleAiKey = await ask(" GOOGLE_GENERATIVE_AI_API_KEY: ");
openrouterKey = await ask(" OPENROUTER_API_KEY: ");
}
// Domínio público
let publicDomain = "";
if (await askYesNo(" Domínio público separado para a landing page?")) {
publicDomain = await ask(" PUBLIC_DOMAIN (ex: openmonetis.com): ");
}
rl.close();
// ─── ETAPA 5: Confirmar e executar ────────────────────────────────────────────
const targetDir = resolve("openmonetis");
section("Instalação");
console.log(`
${sym.arrow} Clonar repositório em ./openmonetis
${sym.arrow} Gerar .env
${sym.arrow} pnpm install${useLocalDocker ? `\n ${sym.arrow} Subir banco PostgreSQL (Docker)\n ${sym.arrow} Habilitar extensões` : ""}
${sym.arrow} pnpm db:push
`);
if (existsSync(targetDir)) {
abort("A pasta ./openmonetis já existe. Remova-a e tente novamente.");
}
// Clonar
let s = spinner("Clonando repositório...");
try {
run("git clone https://github.com/felipegcoutinho/openmonetis.git openmonetis");
s.stop("Repositório clonado");
} catch {
s.fail("Falha ao clonar repositório");
process.exit(1);
}
// Gerar .env
const val = (v, fallback = "") => v?.trim() || fallback;
const opt = (key, value) => (value?.trim() ? `${key}=${value}` : `# ${key}=`);
const envContent = [
`# Gerado por setup.mjs em ${new Date().toISOString()}`,
"",
"# === Database ===",
`DATABASE_URL=${databaseUrl}`,
"",
"# === Better Auth ===",
`BETTER_AUTH_SECRET=${authSecret}`,
`BETTER_AUTH_URL=${betterAuthUrl}`,
"",
"# === Portas ===",
"APP_PORT=3000",
"DB_PORT=5432",
"",
"# === PostgreSQL (Docker local) ===",
"POSTGRES_USER=openmonetis",
"POSTGRES_PASSWORD=openmonetis_dev_password",
"POSTGRES_DB=openmonetis_db",
"",
"# === Multi-domínio ===",
opt("PUBLIC_DOMAIN", publicDomain),
"",
"# === Google OAuth ===",
opt("GOOGLE_CLIENT_ID", googleClientId),
opt("GOOGLE_CLIENT_SECRET", googleClientSecret),
"",
"# === Email (Resend) ===",
opt("RESEND_API_KEY", resendApiKey),
resendFromEmail ? `RESEND_FROM_EMAIL="${resendFromEmail}"` : "# RESEND_FROM_EMAIL=",
"",
"# === AI Providers ===",
opt("ANTHROPIC_API_KEY", anthropicKey),
opt("OPENAI_API_KEY", openaiKey),
opt("GOOGLE_GENERATIVE_AI_API_KEY", googleAiKey),
opt("OPENROUTER_API_KEY", openrouterKey),
].join("\n");
writeFileSync(join(targetDir, ".env"), envContent);
console.log(`${sym.ok} .env gerado`);
// pnpm install
s = spinner("Instalando dependências...");
try {
run("pnpm install", { cwd: targetDir });
s.stop("Dependências instaladas");
} catch {
s.fail("Falha ao instalar dependências");
process.exit(1);
}
// Docker local
if (useLocalDocker) {
s = spinner("Subindo banco PostgreSQL...");
try {
run("pnpm docker:up:db", { cwd: targetDir });
s.stop("Banco iniciado");
} catch {
s.fail("Falha ao iniciar o banco");
process.exit(1);
}
// Aguardar postgres ficar pronto
s = spinner("Aguardando PostgreSQL ficar pronto...");
let ready = false;
for (let i = 0; i < 20; i++) {
try {
run("docker compose exec -T db pg_isready -U openmonetis", { cwd: targetDir });
ready = true;
break;
} catch {
await new Promise((r) => setTimeout(r, 1500));
}
}
if (!ready) {
s.fail("PostgreSQL não respondeu a tempo");
process.exit(1);
}
s.stop("PostgreSQL pronto");
// Extensões
s = spinner("Habilitando extensões do banco...");
try {
run("pnpm db:enableExtensions", { cwd: targetDir });
s.stop("Extensões habilitadas");
} catch {
s.fail("Falha ao habilitar extensões");
process.exit(1);
}
}
// db:push
s = spinner("Aplicando schema no banco...");
try {
run("pnpm db:push", { cwd: targetDir });
s.stop("Schema aplicado");
} catch {
s.fail("Falha ao aplicar schema");
process.exit(1);
}
// ─── Finalização ──────────────────────────────────────────────────────────────
console.log(`
${c.green}${c.bold} ✔ OpenMonetis instalado com sucesso!${c.reset}
${c.bold}Para iniciar:${c.reset}
cd openmonetis
pnpm dev${
useLocalDocker
? ` ${c.dim}→ desenvolvimento${c.reset}\n pnpm docker:up ${c.dim}→ produção local (app + banco)${c.reset}`
: ` ${c.dim}→ desenvolvimento${c.reset}`
}
${c.bold}Acesse:${c.reset} ${betterAuthUrl}
${c.bold}Docs:${c.reset} https://github.com/felipegcoutinho/openmonetis
`);