mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 02:51:46 +00:00
chore: atualiza setup, backup e toolchain
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -104,11 +104,10 @@ docker-compose.override.yml
|
|||||||
.claude/
|
.claude/
|
||||||
.gemini/
|
.gemini/
|
||||||
.cursor/
|
.cursor/
|
||||||
CLAUDE.md
|
|
||||||
AGENTS.md
|
|
||||||
QWEN.md
|
QWEN.md
|
||||||
claude.md
|
AGENTS.md
|
||||||
agents.md
|
# === Backups locais ===
|
||||||
|
/backup/
|
||||||
|
|
||||||
# === Backups e Temporários ===
|
# === Backups e Temporários ===
|
||||||
*.bak
|
*.bak
|
||||||
|
|||||||
318
CLAUDE.md
Normal file
318
CLAUDE.md
Normal 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
102
README.md
@@ -6,10 +6,9 @@
|
|||||||
Projeto pessoal de gestão financeira. Self-hosted, manual e open source.
|
Projeto pessoal de gestão financeira. Self-hosted, manual e open source.
|
||||||
</p>
|
</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.
|
> **⚠️ Não há versão online hospedada.** Você precisa clonar o repositório e rodar localmente ou no seu próprio servidor.
|
||||||
|
|
||||||
|
[](CHANGELOG.md)
|
||||||
[](https://nextjs.org/)
|
[](https://nextjs.org/)
|
||||||
[](https://www.typescriptlang.org/)
|
[](https://www.typescriptlang.org/)
|
||||||
[](https://www.postgresql.org/)
|
[](https://www.postgresql.org/)
|
||||||
@@ -29,7 +28,8 @@
|
|||||||
## 📖 Índice
|
## 📖 Índice
|
||||||
|
|
||||||
- [Sobre o Projeto](#-sobre-o-projeto)
|
- [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)
|
- [Scripts Disponíveis](#-scripts-disponíveis)
|
||||||
- [Docker](#-docker)
|
- [Docker](#-docker)
|
||||||
- [Variáveis de Ambiente](#-variáveis-de-ambiente)
|
- [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.
|
💰 **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.
|
💳 **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).
|
📲 **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
|
### Stack técnica
|
||||||
|
|
||||||
- **Next.js** (App Router, Turbopack) + **React** + **TypeScript**
|
- **Next.js** (App Router, Turbopack) + **React** + **TypeScript**
|
||||||
- **PostgreSQL** + **Drizzle ORM**
|
- **PostgreSQL** + **Drizzle ORM**
|
||||||
- **Better Auth** (email/senha + OAuth)
|
- **Better Auth** (email/senha, OAuth, Passkeys/WebAuthn)
|
||||||
- **shadcn/ui** (Radix UI) + **Tailwind CSS**
|
- **shadcn/ui** (Radix UI) + **Tailwind CSS**
|
||||||
- **Docker** (multi-stage build)
|
- **Docker** (multi-stage build)
|
||||||
- **Biome** (linting + formatting)
|
- **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
|
### Pré-requisitos
|
||||||
|
|
||||||
@@ -251,32 +274,49 @@ OPENROUTER_API_KEY=
|
|||||||
|
|
||||||
## 🏗️ Arquitetura
|
## 🏗️ Arquitetura
|
||||||
|
|
||||||
|
O projeto segue arquitetura **feature-first** dentro de `src/`:
|
||||||
|
|
||||||
```
|
```
|
||||||
openmonetis/
|
openmonetis/
|
||||||
├── app/ # Next.js App Router
|
├── src/
|
||||||
│ ├── api/ # API Routes (auth, health, inbox)
|
│ ├── app/ # Next.js App Router (rotas finas)
|
||||||
│ ├── (auth)/ # Login e cadastro
|
│ │ ├── api/ # API Routes (auth, health, inbox)
|
||||||
│ ├── (dashboard)/ # Rotas protegidas
|
│ │ ├── (auth)/ # Login e cadastro
|
||||||
│ └── (landing-page)/ # Página inicial pública
|
│ │ ├── (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)
|
├── public/ # Assets estáticos (imagens, logos, fontes)
|
||||||
│ ├── ui/ # shadcn/ui (40+ componentes)
|
├── drizzle/ # Migrations geradas
|
||||||
│ ├── dashboard/ # Widgets do dashboard (20+)
|
├── scripts/ # Scripts utilitários (migrations, dev)
|
||||||
│ └── [feature]/ # Componentes por feature
|
├── Dockerfile # Multi-stage build (~200MB, non-root)
|
||||||
│
|
├── docker-compose.yml # Orquestração app + PostgreSQL
|
||||||
├── 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
|
|
||||||
└── proxy.ts # Middleware (auth + multi-domínio)
|
└── proxy.ts # Middleware (auth + multi-domínio)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -291,7 +331,7 @@ openmonetis/
|
|||||||
5. **Push:** `git push origin feature/minha-feature`
|
5. **Push:** `git push origin feature/minha-feature`
|
||||||
6. Abra um **Pull Request**
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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": {
|
"vcs": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"clientKind": "git",
|
"clientKind": "git",
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ const nextConfig: NextConfig = {
|
|||||||
turbopackFileSystemCacheForDev: true,
|
turbopackFileSystemCacheForDev: true,
|
||||||
},
|
},
|
||||||
reactCompiler: true,
|
reactCompiler: true,
|
||||||
typescript: {
|
|
||||||
ignoreBuildErrors: true,
|
|
||||||
},
|
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [new URL("https://lh3.googleusercontent.com/**")],
|
remotePatterns: [new URL("https://lh3.googleusercontent.com/**")],
|
||||||
},
|
},
|
||||||
|
|||||||
21
package.json
21
package.json
@@ -24,17 +24,18 @@
|
|||||||
"docker:logs:app": "docker compose logs -f app",
|
"docker:logs:app": "docker compose logs -f app",
|
||||||
"docker:logs:db": "docker compose logs -f db",
|
"docker:logs:db": "docker compose logs -f db",
|
||||||
"docker:restart": "docker compose restart",
|
"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": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "^3.0.60",
|
"@ai-sdk/anthropic": "^3.0.62",
|
||||||
"@ai-sdk/google": "^3.0.51",
|
"@ai-sdk/google": "^3.0.51",
|
||||||
"@ai-sdk/openai": "^3.0.45",
|
"@ai-sdk/openai": "^3.0.46",
|
||||||
"@better-auth/passkey": "^1.5.5",
|
"@better-auth/passkey": "^1.5.5",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@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-alert-dialog": "1.1.15",
|
||||||
"@radix-ui/react-avatar": "1.1.11",
|
"@radix-ui/react-avatar": "1.1.11",
|
||||||
"@radix-ui/react-checkbox": "1.3.3",
|
"@radix-ui/react-checkbox": "1.3.3",
|
||||||
@@ -66,9 +67,9 @@
|
|||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"drizzle-orm": "0.45.1",
|
"drizzle-orm": "0.45.1",
|
||||||
"jspdf": "^4.2.0",
|
"jspdf": "^4.2.1",
|
||||||
"jspdf-autotable": "^5.0.7",
|
"jspdf-autotable": "^5.0.7",
|
||||||
"next": "16.1.6",
|
"next": "16.1.7",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"pg": "8.20.0",
|
"pg": "8.20.0",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
@@ -76,7 +77,7 @@
|
|||||||
"react-day-picker": "^9.14.0",
|
"react-day-picker": "^9.14.0",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
"recharts": "3.8.0",
|
"recharts": "3.8.0",
|
||||||
"resend": "^6.9.3",
|
"resend": "^6.9.4",
|
||||||
"sonner": "2.0.7",
|
"sonner": "2.0.7",
|
||||||
"tailwind-merge": "3.5.0",
|
"tailwind-merge": "3.5.0",
|
||||||
"vaul": "1.1.2",
|
"vaul": "1.1.2",
|
||||||
@@ -84,15 +85,15 @@
|
|||||||
"zod": "4.3.6"
|
"zod": "4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.4.7",
|
"@biomejs/biome": "2.4.8",
|
||||||
"@tailwindcss/postcss": "4.2.1",
|
"@tailwindcss/postcss": "4.2.2",
|
||||||
"@types/canvas-confetti": "^1.9.0",
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@types/node": "25.5.0",
|
"@types/node": "25.5.0",
|
||||||
"@types/pg": "^8.18.0",
|
"@types/pg": "^8.18.0",
|
||||||
"@types/react": "19.2.14",
|
"@types/react": "19.2.14",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
"drizzle-kit": "0.31.9",
|
"drizzle-kit": "0.31.10",
|
||||||
"tailwindcss": "4.2.1",
|
"tailwindcss": "4.2.1",
|
||||||
"tsx": "4.21.0",
|
"tsx": "4.21.0",
|
||||||
"typescript": "5.9.3"
|
"typescript": "5.9.3"
|
||||||
|
|||||||
535
pnpm-lock.yaml
generated
535
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
105
scripts/backup.sh
Executable file
105
scripts/backup.sh
Executable 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."
|
||||||
@@ -27,12 +27,23 @@ fi
|
|||||||
if [ -f .env.example ]; then
|
if [ -f .env.example ]; then
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
echo "✅ Arquivo .env criado a partir de .env.example"
|
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
|
else
|
||||||
echo "❌ Erro: .env.example não encontrado!"
|
echo "❌ Erro: .env.example não encontrado!"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
365
setup.mjs
Normal 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
|
||||||
|
`);
|
||||||
Reference in New Issue
Block a user