chore(infra): atualizar build, docker e tooling

This commit is contained in:
Felipe Coutinho
2026-04-03 18:10:16 +00:00
parent d01bc8a669
commit ba369e8a83
11 changed files with 1146 additions and 425 deletions

View File

@@ -1,58 +0,0 @@
# AI Coding Assistant Instructions for OpenMonetis
## Project Overview
OpenMonetis is a self-hosted personal finance management application built with Next.js 16, TypeScript, PostgreSQL, and Drizzle ORM. It provides manual transaction tracking, account management, budgeting, and financial insights with a Portuguese interface.
## Architecture
- **Frontend**: Next.js App Router with React 19, shadcn/ui components, Tailwind CSS
- **Backend**: Server actions in Next.js, API routes for auth/health
- **Database**: PostgreSQL with Drizzle ORM, schema in `db/schema.ts`
- **Auth**: Better Auth (OAuth + email magic links)
- **Deployment**: Docker multi-stage build, health checks
## Key Patterns
- **Server Actions**: Use `"use server"` for mutations, validate with Zod schemas, handle errors with `handleActionError`
- **Database Queries**: Use Drizzle's query API with relations, e.g., `db.query.lancamentos.findMany({ with: { categoria: true } })`
- **Authentication**: Import from `lib/auth/server`, redirect on failure
- **Revalidation**: Call `revalidateForEntity("lancamentos")` after mutations
- **Portuguese Naming**: DB fields like `nome`, `tipo_conta`, `pagador` (payer), `lancamento` (transaction)
- **Component Structure**: Feature-based folders in `components/`, shared UI in `components/ui/`
## Development Workflow
- **Start Dev**: `pnpm dev` (Turbopack), `docker compose up db -d` for DB
- **Database**: `pnpm db:push` to sync schema, `pnpm db:studio` for visual editor
- **Build**: `pnpm build`, `pnpm start` for production
- **Docker**: `pnpm docker:up` for full stack, `pnpm docker:logs` for monitoring
## Common Tasks
- **Add Transaction**: Create server action in `app/(dashboard)/lancamentos/actions.ts`, validate with Zod, insert via Drizzle
- **New Entity**: Add to `db/schema.ts`, define relations, create CRUD actions in `lib/[entity]/actions.ts`
- **UI Component**: Use shadcn/ui, place in `components/[feature]/`, export from `components/ui/`
- **API Route**: Add to `app/api/`, use `getUserSession()` for auth
## Conventions
- **Imports**: Absolute paths with `@/`, group by external/internal
- **Error Handling**: Return `{ success: false, error: string }` from actions
- **Currency**: Store as decimal strings (e.g., "123.45"), convert to cents for calculations
- **Periods**: Format as "YYYY-MM", use `parsePeriodParam()` for URL params
- **Notifications**: Send emails via `sendPagadorAutoEmails()` for payer updates
## External Integrations
- **Better Auth**: Config in `lib/auth/config.ts`, session handling
- **Drizzle**: Migrations in `drizzle/`, studio at `pnpm db:studio`
- **AI Features**: Use `@ai-sdk/*` for insights, configured in environment
- **Email**: Resend for notifications, configured via `RESEND_API_KEY`
## File Examples
- Schema: `db/schema.ts` (relations, indexes)
- Actions: `app/(dashboard)/lancamentos/actions.ts` (CRUD with validation)
- Components: `components/lancamentos/page/lancamentos-page.tsx` (client component)
- Utils: `lib/lancamentos/page-helpers.ts` (data transformation)

View File

@@ -15,8 +15,32 @@ env:
DOCKER_IMAGE_NAME: openmonetis DOCKER_IMAGE_NAME: openmonetis
jobs: jobs:
quality:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.33.0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm run lint
build-and-push: build-and-push:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: quality
permissions: permissions:
contents: read contents: read
packages: write packages: write

View File

@@ -5,8 +5,7 @@
# ============================================ # ============================================
FROM node:22-alpine AS deps FROM node:22-alpine AS deps
# Instalar pnpm globalmente RUN corepack enable && corepack prepare pnpm@10.33.0 --activate
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app WORKDIR /app
@@ -24,8 +23,7 @@ RUN pnpm install --frozen-lockfile
# ============================================ # ============================================
FROM node:22-alpine AS builder FROM node:22-alpine AS builder
# Instalar pnpm globalmente RUN corepack enable && corepack prepare pnpm@10.33.0 --activate
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app WORKDIR /app
@@ -35,13 +33,14 @@ COPY --from=deps /app/node_modules ./node_modules
# Copiar todo o código fonte # Copiar todo o código fonte
COPY . . COPY . .
# Garantir que o pdf.worker vem da versão instalada no stage 1, não do host
COPY --from=deps /app/public/pdf.worker.min.mjs ./public/pdf.worker.min.mjs
# Variáveis de ambiente necessárias para o build # Variáveis de ambiente necessárias para o build
# DATABASE_URL será fornecida em runtime, mas precisa estar definida para validação
ENV NEXT_TELEMETRY_DISABLED=1 \ ENV NEXT_TELEMETRY_DISABLED=1 \
NODE_ENV=production NODE_ENV=production
# Build da aplicação Next.js # Build da aplicação Next.js
# Nota: Se houver erros de tipo, ajuste typescript.ignoreBuildErrors no next.config.ts
RUN pnpm build RUN pnpm build
# ============================================ # ============================================
@@ -49,8 +48,7 @@ RUN pnpm build
# ============================================ # ============================================
FROM node:22-alpine AS runner FROM node:22-alpine AS runner
# Instalar pnpm globalmente RUN corepack enable && corepack prepare pnpm@10.33.0 --activate
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app WORKDIR /app
@@ -60,8 +58,6 @@ RUN addgroup --system --gid 1001 nodejs && \
# Copiar apenas arquivos necessários para produção # Copiar apenas arquivos necessários para produção
COPY --from=builder --chown=nextjs:nodejs /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json
COPY --from=builder --chown=nextjs:nodejs /app/pnpm-lock.yaml ./pnpm-lock.yaml
# Copiar arquivos de build do Next.js # Copiar arquivos de build do Next.js
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
@@ -72,8 +68,25 @@ COPY --from=builder --chown=nextjs:nodejs /app/drizzle ./drizzle
COPY --from=builder --chown=nextjs:nodejs /app/drizzle.config.ts ./drizzle.config.ts COPY --from=builder --chown=nextjs:nodejs /app/drizzle.config.ts ./drizzle.config.ts
COPY --from=builder --chown=nextjs:nodejs /app/src/db ./src/db COPY --from=builder --chown=nextjs:nodejs /app/src/db ./src/db
# Copiar node_modules para ter drizzle-kit disponível para migrations # Instalar apenas as deps necessárias para drizzle-kit migrate
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules # Gera package.json mínimo a partir do original para evitar version drift
COPY --from=builder /app/package.json /tmp/pkg.json
RUN node -e "\
const p=JSON.parse(require('fs').readFileSync('/tmp/pkg.json','utf8'));\
require('fs').writeFileSync('package.json',JSON.stringify({\
name:'openmonetis',version:p.version,\
dependencies:{\
'drizzle-orm':p.dependencies['drizzle-orm'],\
'pg':p.dependencies['pg']\
},\
devDependencies:{'drizzle-kit':p.devDependencies['drizzle-kit']}\
}));" && \
pnpm install --no-frozen-lockfile --ignore-scripts && \
chown nextjs:nodejs package.json
# Copiar entrypoint de migrations
COPY docker-entrypoint.sh ./
RUN chmod +x /app/docker-entrypoint.sh && chown nextjs:nodejs /app/docker-entrypoint.sh
# Definir variáveis de ambiente de produção # Definir variáveis de ambiente de produção
ENV NODE_ENV=production \ ENV NODE_ENV=production \
@@ -89,8 +102,8 @@ USER nextjs
# Health check # Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" || exit 1 CMD wget --quiet --tries=1 --spider http://localhost:3000/api/health || exit 1
# Comando de inicialização # Entrypoint: roda migrations e depois executa o CMD
# Nota: Em produção com standalone build, o servidor é iniciado pelo arquivo server.js ENTRYPOINT ["/app/docker-entrypoint.sh"]
CMD ["node", "server.js"] CMD ["node", "server.js"]

View File

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

View File

@@ -4,23 +4,28 @@ name: openmonetis
# MODOS DE USO: # MODOS DE USO:
# 1. Banco LOCAL (PostgreSQL em container): # 1. Banco LOCAL (PostgreSQL em container):
# - Configure DATABASE_URL com host "db" no .env # - Configure DATABASE_URL com host "db" no .env
# - Execute: docker compose up # - Execute: docker compose --profile local up
# #
# 2. Banco REMOTO (ex: Supabase, Neon, etc): # 2. Banco REMOTO (ex: Supabase, Neon, etc):
# - Configure DATABASE_URL com a URL do banco remoto no .env # - Configure DATABASE_URL com a URL do banco remoto no .env
# - Execute: docker compose up app (apenas o serviço app) # - Execute: docker compose up
# #
# 3. Para parar todos os serviços: # 3. Build local (desenvolvimento):
# - Execute: docker compose --profile local up --build
#
# 4. Para parar todos os serviços:
# - Execute: docker compose down # - Execute: docker compose down
# #
# 4. Para remover volumes (CUIDADO: apaga dados do banco local): # 5. Para remover volumes (CUIDADO: apaga dados do banco local):
# - Execute: docker compose down -v # - Execute: docker compose down -v
services: services:
# ============================================ # ============================================
# Serviço: PostgreSQL (Banco de dados local) # Serviço: PostgreSQL (Banco de dados local)
# Ativado apenas com: --profile local
# ============================================ # ============================================
db: db:
profiles: ["local"]
image: postgres:18-alpine image: postgres:18-alpine
container_name: openmonetis_postgres container_name: openmonetis_postgres
restart: unless-stopped restart: unless-stopped
@@ -63,6 +68,7 @@ services:
# Serviço: Aplicação Next.js # Serviço: Aplicação Next.js
# ============================================ # ============================================
app: app:
build: .
image: felipegcoutinho/openmonetis:latest image: felipegcoutinho/openmonetis:latest
container_name: openmonetis_app container_name: openmonetis_app
@@ -80,6 +86,13 @@ services:
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET} BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
BETTER_AUTH_URL: ${BETTER_AUTH_URL:-http://localhost:3000} BETTER_AUTH_URL: ${BETTER_AUTH_URL:-http://localhost:3000}
# S3 (opcional)
S3_ENDPOINT: ${S3_ENDPOINT:-}
S3_REGION: ${S3_REGION:-}
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:-}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-}
S3_BUCKET: ${S3_BUCKET:-}
# Email (opcional) # Email (opcional)
RESEND_API_KEY: ${RESEND_API_KEY:-} RESEND_API_KEY: ${RESEND_API_KEY:-}
RESEND_FROM_EMAIL: ${RESEND_FROM_EMAIL:-} RESEND_FROM_EMAIL: ${RESEND_FROM_EMAIL:-}
@@ -96,24 +109,11 @@ services:
GOOGLE_GENERATIVE_AI_API_KEY: ${GOOGLE_GENERATIVE_AI_API_KEY:-} GOOGLE_GENERATIVE_AI_API_KEY: ${GOOGLE_GENERATIVE_AI_API_KEY:-}
OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-} OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-}
# Só depende do 'db' se estiver usando banco local # required: false permite subir sem banco local (banco remoto via DATABASE_URL)
# Para banco remoto, comente as linhas abaixo
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
required: false
# Script de inicialização: roda migrations antes de iniciar o app
entrypoint: ["/bin/sh", "-c"]
command:
- |
echo "Aguardando banco de dados..."
sleep 5
echo "Rodando migrations..."
pnpm db:push || echo "Migrations falharam ou já estão atualizadas"
echo "Iniciando aplicação Next.js..."
node server.js
healthcheck: healthcheck:
test: test:

8
docker-entrypoint.sh Normal file
View File

@@ -0,0 +1,8 @@
#!/bin/sh
set -e
echo "Rodando migrations do banco de dados..."
./node_modules/.bin/drizzle-kit push
echo "Migrations concluídas."
exec "$@"

22
knip.jsonc Normal file
View File

@@ -0,0 +1,22 @@
{
"$schema": "https://unpkg.com/knip@6/schema-jsonc.json",
// Exclude shared UI primitives from dead code reporting while we focus the
// cleanup on feature and domain code first.
"ignore": [
"src/shared/components/ui/**"
],
// Runtime asset referenced by string in the PDF viewer.
"ignoreFiles": [
"public/pdf.worker.min.mjs",
"setup.mjs"
],
// PostCSS is inferred from the config file, but the project only depends on
// the Tailwind PostCSS plugin directly.
"ignoreDependencies": [
"postcss"
],
"next": true,
"postcss": true,
"biome": true,
"drizzle": true
}

View File

@@ -8,12 +8,18 @@ const nextConfig: NextConfig = {
output: "standalone", output: "standalone",
cacheComponents: true, cacheComponents: true,
reactCompiler: true, reactCompiler: true,
images: { images: {
remotePatterns: [new URL("https://lh3.googleusercontent.com/**")], remotePatterns: [new URL("https://lh3.googleusercontent.com/**")],
}, },
devIndicators: { devIndicators: {
position: "bottom-right", position: "bottom-right",
}, },
experimental: {
prefetchInlining: true,
turbopackFileSystemCacheForDev: true,
},
// Headers for Safari compatibility // Headers for Safari compatibility
async headers() { async headers() {
return [ return [

View File

@@ -1,13 +1,15 @@
{ {
"name": "openmonetis", "name": "openmonetis",
"version": "2.2.1", "version": "2.3.0",
"private": true, "private": true,
"packageManager": "pnpm@10.33.0",
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev --turbopack",
"db:seed": "tsx scripts/mock-data.ts", "db:seed": "tsx scripts/mock-data.ts",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "biome check .", "lint": "biome check .",
"lint:deadcode": "knip --reporter compact",
"lint:fix": "biome check --write .", "lint:fix": "biome check --write .",
"env:setup": "bash scripts/setup-env.sh", "env:setup": "bash scripts/setup-env.sh",
"db:generate": "drizzle-kit generate", "db:generate": "drizzle-kit generate",
@@ -29,11 +31,11 @@
"backup": "bash scripts/backup.sh" "backup": "bash scripts/backup.sh"
}, },
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "^3.0.64", "@ai-sdk/anthropic": "^3.0.65",
"@ai-sdk/google": "^3.0.53", "@ai-sdk/google": "^3.0.55",
"@ai-sdk/openai": "^3.0.48", "@ai-sdk/openai": "^3.0.49",
"@aws-sdk/client-s3": "^3.1019.0", "@aws-sdk/client-s3": "^3.1022.0",
"@aws-sdk/s3-request-presigner": "^3.1019.0", "@aws-sdk/s3-request-presigner": "^3.1022.0",
"@better-auth/passkey": "^1.5.6", "@better-auth/passkey": "^1.5.6",
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
@@ -59,9 +61,10 @@
"@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toggle-group": "1.1.11",
"@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-tooltip": "1.2.8",
"@remixicon/react": "4.9.0", "@remixicon/react": "4.9.0",
"@tanstack/react-query": "^5.96.2",
"@tanstack/react-table": "8.21.3", "@tanstack/react-table": "8.21.3",
"@tanstack/react-virtual": "^3.13.23", "@tanstack/react-virtual": "^3.13.23",
"ai": "^6.0.141", "ai": "^6.0.143",
"better-auth": "1.5.6", "better-auth": "1.5.6",
"canvas-confetti": "^1.9.4", "canvas-confetti": "^1.9.4",
"class-variance-authority": "0.7.1", "class-variance-authority": "0.7.1",
@@ -71,7 +74,7 @@
"drizzle-orm": "0.45.2", "drizzle-orm": "0.45.2",
"jspdf": "^4.2.1", "jspdf": "^4.2.1",
"jspdf-autotable": "^5.0.7", "jspdf-autotable": "^5.0.7",
"next": "16.1.7", "next": "16.2.2",
"next-themes": "0.4.6", "next-themes": "0.4.6",
"pdfjs-dist": "^5.6.205", "pdfjs-dist": "^5.6.205",
"pg": "8.20.0", "pg": "8.20.0",
@@ -80,7 +83,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.1", "recharts": "3.8.1",
"resend": "^6.9.4", "resend": "^6.10.0",
"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",
@@ -88,15 +91,16 @@
"zod": "4.3.6" "zod": "4.3.6"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.4.9", "@biomejs/biome": "2.4.10",
"@tailwindcss/postcss": "4.2.2", "@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.20.0", "@types/pg": "^8.20.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.4.0",
"drizzle-kit": "0.31.10", "drizzle-kit": "0.31.10",
"knip": "^6.3.0",
"tailwindcss": "4.2.2", "tailwindcss": "4.2.2",
"tsx": "4.21.0", "tsx": "4.21.0",
"typescript": "6.0.2" "typescript": "6.0.2"

1342
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,5 +16,3 @@ export const america = localFont({
display: "swap", display: "swap",
variable: "--font-america", variable: "--font-america",
}); });
export const americaFontVariable = america.variable;