CardosOS
Portfólio que se alimenta sozinho. Lê os meus repositórios do GitHub e as horas da WakaTime, guarda tudo numa base de dados própria, e o site serve a partir daí. Tem painel de administração e um chatbot que responde sobre o meu percurso.
Spring Boot 3 e PostgreSQL no back-end, Next.js no front-end, tudo em Docker.
Correr
Precisas de Docker Desktop a correr, Java 21 e Node 20.
No Windows é duplo clique em correr.bat: verifica o Docker, sobe a base de dados e o
back-end, espera pelo health check e abre o browser.
À mão:
docker compose up -d --build backend
npm run dev --prefix frontend
Site em http://localhost:3000, API em http://localhost:8080, painel em /admin.
Configuração
Copia o .env.example para .env e preenche. Está comentado variável a variável.
Duas impedem o arranque: JWT_SECRET (gera com openssl rand -base64 32) e
DB_PASSWORD. As outras degradam em vez de rebentar. Sem LLM_API_KEY o chat diz que
está indisponível, sem GITHUB_TOKEN os projetos vêm vazios mas sem erro.
O REVALIDATE_SECRET tem de ter o mesmo valor no .env da raiz e em
frontend/.env.local.
Como está feito
Um só Spring Boot, dividido em módulos com fronteiras verificadas em tempo de teste pelo Spring Modulith: se um módulo importar do interior de outro, o build falha. As setas a cheio são chamadas por interface, as tracejadas são eventos de domínio.
graph TD
portfolio --> shared
integrations --> shared
chat --> shared
chat --> portfolio
contact --> shared
auth --> shared
notification --> shared
integrations -. RepoSyncedEvent .-> portfolio
contact -. ContactSubmittedEvent .-> notification
| Módulo | O que faz |
|---|---|
| integrations | Fala com o GitHub e a WakaTime e regista cada sincronização |
| portfolio | Projetos, percurso e stack |
| chat | Assistente DeepSeek, com o contexto construído da base de dados |
| contact | Formulário, honeypot e limite por IP |
| notification | Email pelo Resend, disparado por eventos |
| auth | Login do administrador, JWT e refresh tokens |
| shared | Envelope de API e rate limiter |
O site nunca chama o GitHub para servir uma página. A sincronização corre de 6 em 6 horas por cron, ou assim que chega um webhook, escreve na base de dados e avisa o Next para revalidar. Por isso o site continua a servir com o GitHub em baixo, e há um teste que prova isso apagando a resposta da API.
Decisões
Monólito modular em vez de microserviços. A modularidade que o requisito pede é verificada pelo Modulith, e cinco JVMs não cabiam na RAM nem nas horas.
JWT próprio em vez de Keycloak. Há um utilizador, o administrador.
Contexto do chatbot no prompt em vez de RAG. O corpus cabe em 5k tokens; RAG só acrescentava pipeline e latência.
Rate limiting em Postgres em vez de Redis. Dois contadores não pagam outro container, e em memória o limite duplicava com duas instâncias.
Watchtower em vez de deploy por SSH, para não guardar uma chave de produção nos segredos do GitHub.
Falta
O deploy. O GitHub Actions corre os testes em cada push e publica as imagens no GHCR a
partir do main, mas não há servidor, e o docker-compose.prod.yml ainda não tem nada a
escutar em 80/443.
Se der problemas
backend/mvnwtem de ter fim de linha LF. Com CRLF o shebang parte dentro da imagem Alpine. O.gitattributesjá força, mas convém confirmar depois de mexer no ficheiro.- Alterações que não aparecem no site: apaga
frontend/.next/dev/cachee reinicia o servidor. A cache do Next 16 continua a servir o valor antigo. - WakaTime a responder
422: falta definir o fuso horário no perfil da conta. npm installa falhar comEALLOWSCRIPTS: é a política do npm nesta máquina. Corre onpm installdiretamente em vez do fluxo docreate-next-app.