diff --git a/.env.example b/.env.example index 57421e9..82c8084 100644 --- a/.env.example +++ b/.env.example @@ -6,3 +6,7 @@ APPWRITE_FUNCTIONS_API_KEY= VITE_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1 VITE_APPWRITE_PROJECT_ID= VITE_APPWRITE_DATABASE_ID= +VITE_APPWRITE_COLLECTION_SERVERS_ID= +VITE_APPWRITE_COLLECTION_GITHUB_REPOS_ID= +VITE_APPWRITE_COLLECTION_AUDIT_LOGS_ID= +VITE_APPWRITE_COLLECTION_CLOUDFLARE_ACCOUNTS_ID= diff --git a/README.md b/README.md index 877313c..d19d1f1 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,85 @@ # Core Platform Monorepo -Ambiente unificado com três camadas principais: +Ambiente monorepo com três camadas principais: **Landing (Fresh + Deno)**, **Dashboard (React + Vite)** e **Appwrite Cloud** como backend. Este guia prioriza a configuração do Appwrite Cloud antes de rodar qualquer código local. -- **Appwrite** para autenticação, base de dados e funções (sync-github e check-cloudflare-status). -- **Landing (Fresh + Deno)** para a página pública em `landing/`. -- **Dashboard (React + Vite)** para o painel operacional em `dashboard/`. - -## Pré-requisitos -- Node.js 18+ e npm -- Deno 1.39+ (para o app Fresh) -- Docker e Docker Compose (para Appwrite local) ou um projeto Appwrite Cloud -- Variáveis de ambiente configuradas a partir de [.env.example](.env.example) +## Passo a passo (Appwrite Cloud) +1. Acesse https://cloud.appwrite.io e crie/abra um projeto. +2. No menu **API Keys**, gere uma chave com permissão de Admin para criar bancos, coleções e funções. +3. Registre os valores a seguir para preencher o `.env`: + - **API Endpoint** (ex.: `https://cloud.appwrite.io/v1`) + - **Project ID** + - **Database ID** (criado no passo seguinte) + - Opcional: endpoints/keys das Functions se você usar domínios dedicados. +4. Crie um Database chamado **DevOpsPlatform** e anote seu ID. +5. Dentro do banco, crie as coleções (IDs sugeridos entre parênteses): + - **servers** (`servers`): + - `name` (string) + - `ip` (string) + - `status` (enum: `online`, `offline`) + - `region` (string) + - **github_repos** (`github_repos`): + - `repo_name` (string) + - `url` (url) + - `last_commit` (string) + - `status` (string) + - **audit_logs** (`audit_logs`): + - `event` (string) + - `user_id` (string) + - `timestamp` (datetime) + - **cloud_accounts** (`cloud_accounts`) para integrações: + - `provider` (string) + - `apiKey` (string) + - `label` (string) +6. Ative o provedor **Email/Password** em *Authentication* e crie um usuário de teste. +7. Implante as Functions `sync-github` e `check-cloudflare-status` (fontes em `appwrite-functions/`). ## Variáveis de ambiente -- **Backend/Appwrite:** use `APPWRITE_ENDPOINT`, `APPWRITE_PROJECT_ID`, `APPWRITE_API_KEY`, `APPWRITE_DATABASE_ID` e credenciais de funções conforme necessário. -- **Dashboard (Vite):** use `VITE_APPWRITE_ENDPOINT`, `VITE_APPWRITE_PROJECT_ID` e `VITE_APPWRITE_DATABASE_ID`. O prefixo `VITE_` garante a leitura pelo Vite em tempo de build. -- **Landing (Deno Fresh):** o arquivo `landing/main.ts` carrega automaticamente variáveis do `.env` via `$std/dotenv/load.ts`, então os mesmos valores de Appwrite podem ser reutilizados. - -Sugestão rápida: +Copie o arquivo de exemplo e preencha com os IDs anotados: ```bash cp .env.example .env -# Preencha IDs/projetos/chaves de acordo com seu ambiente Appwrite ``` +Campos principais: +- `VITE_APPWRITE_ENDPOINT`, `VITE_APPWRITE_PROJECT_ID`, `VITE_APPWRITE_DATABASE_ID` +- `VITE_APPWRITE_COLLECTION_SERVERS_ID`, `VITE_APPWRITE_COLLECTION_GITHUB_REPOS_ID`, `VITE_APPWRITE_COLLECTION_AUDIT_LOGS_ID`, `VITE_APPWRITE_COLLECTION_CLOUDFLARE_ACCOUNTS_ID` +- Para scripts server-side, use também `APPWRITE_ENDPOINT`, `APPWRITE_PROJECT_ID`, `APPWRITE_API_KEY`. -## Executando os ambientes -### 1) Appwrite local -1. Tenha Docker em execução. -2. Inicie o stack Appwrite (CLI ou imagem oficial), por exemplo: - ```bash - docker run -it --rm \ - -p 80:80 -p 443:443 \ - -v "$(pwd)/appwrite:/var/lib/appwrite" \ - appwrite/appwrite:latest install - ``` -3. Após provisionar o projeto, configure as variáveis (`APPWRITE_*` e `VITE_APPWRITE_*`) e implante as funções em `appwrite-functions/` conforme necessário. +## Estrutura de pastas +- `landing/`: app Fresh (Deno) para a landing page. +- `dashboard/`: painel React + Vite com integrações Appwrite (auth, Functions, Database, Realtime). +- `appwrite-functions/`: funções `sync-github` e `check-cloudflare-status`. -### 2) Landing (Fresh + Deno) +## Rodando os ambientes +1) **Appwrite local (opcional)** +```bash +docker run -it --rm \ + -p 80:80 -p 443:443 \ + -v "$(pwd)/appwrite:/var/lib/appwrite" \ + appwrite/appwrite:latest install +``` +Preencha o `.env` com os IDs gerados localmente e replique o schema acima no console. + +2) **Landing (Fresh + Deno)** ```bash npm run dev:landing # ou: cd landing && deno task start ``` -- O servidor roda em `http://localhost:8000` por padrão. -- As variáveis `.env` são lidas automaticamente no bootstrap do Deno. +Roda em `http://localhost:8000`. O Deno carrega variáveis do `.env` automaticamente. -### 3) Dashboard (React + Vite) +3) **Dashboard (React + Vite)** ```bash cd dashboard npm install npm run dev ``` -- Usa os valores `VITE_APPWRITE_*` para client, database e funções. -- Disponível em `http://localhost:5173`. +Disponível em `http://localhost:5173`. O Vite lê apenas variáveis prefixadas com `VITE_`. -### 4) Landing + Dashboard em paralelo -No diretório raiz: +4) **Landing + Dashboard em paralelo** ```bash npm run dev:web ``` -O script usa `npm-run-all` para iniciar `landing` (Deno) e `dashboard` (Vite) lado a lado. +Usa `npm-run-all` para iniciar ambos os frontends. -## Estrutura de pastas -- `appwrite-functions/`: fontes das funções `sync-github` e `check-cloudflare-status`. -- `landing/`: app Fresh (Deno) para a camada pública. -- `dashboard/`: painel autenticado com React + Vite, Tailwind e integrações Appwrite. - -## Notas finais -- O dashboard valida a sessão Appwrite no carregamento inicial e protege rotas privadas. -- O Appwrite Database precisa do ID configurado (`APPWRITE_DATABASE_ID`/`VITE_APPWRITE_DATABASE_ID`) para consultas de projetos, cloud_accounts e audit_logs. -- O canal Realtime da coleção `audit_logs` é usado para o terminal de logs em tempo real. +## Notas rápidas +- Rotas do dashboard são protegidas pelo Appwrite (Email/Password) e usam `account.createEmailPasswordSession`. +- O widget de overview consulta as coleções de repositórios, servidores e audit logs para mostrar métricas reais. +- O terminal inferior assina o canal Realtime da coleção `audit_logs` usando o ID configurado no `.env`. diff --git a/dashboard/src/components/TerminalLogs.tsx b/dashboard/src/components/TerminalLogs.tsx index e28b275..b8dfb79 100644 --- a/dashboard/src/components/TerminalLogs.tsx +++ b/dashboard/src/components/TerminalLogs.tsx @@ -1,9 +1,5 @@ import { useEffect, useMemo, useState } from 'react' -import { client, appwriteDatabaseId } from '../lib/appwrite' - -const channel = appwriteDatabaseId - ? `databases.${appwriteDatabaseId}.collections.audit_logs.documents` - : undefined +import { client, appwriteCollectionAuditLogsId, appwriteDatabaseId } from '../lib/appwrite' type TerminalLog = { id: string @@ -25,13 +21,14 @@ export function TerminalLogs() { const logTitle = useMemo(() => (open ? 'Ocultar Terminal' : 'Mostrar Terminal'), [open]) useEffect(() => { - if (!channel) return undefined + if (!appwriteDatabaseId || !appwriteCollectionAuditLogsId) return undefined + const channel = `databases.${appwriteDatabaseId}.collections.${appwriteCollectionAuditLogsId}.documents` const unsubscribe = client.subscribe(channel, (response) => { const payload = (response as { payload?: Record }).payload || {} - const action = (payload.action as string) || 'evento' + const action = (payload.event as string) || (payload.action as string) || 'evento' const timestamp = (payload.timestamp as string) || new Date().toISOString() - const userId = (payload.userId as string) || undefined + const userId = (payload.user_id as string) || (payload.userId as string) || undefined setLogs((prev) => [ { @@ -47,7 +44,7 @@ export function TerminalLogs() { return () => { unsubscribe() } - }, [channel]) + }, []) return (
diff --git a/dashboard/src/contexts/Auth.tsx b/dashboard/src/contexts/Auth.tsx index f97d48e..35d433c 100644 --- a/dashboard/src/contexts/Auth.tsx +++ b/dashboard/src/contexts/Auth.tsx @@ -1,5 +1,5 @@ import { AppwriteException, Models } from 'appwrite' -import React, { createContext, useContext, useEffect, useMemo, useState } from 'react' +import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' import { Navigate, Outlet, useLocation } from 'react-router-dom' import { account } from '../lib/appwrite' @@ -17,7 +17,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children const [user, setUser] = useState | null>(null) const [loading, setLoading] = useState(true) - const fetchUser = async () => { + const fetchUser = useCallback(async () => { try { setLoading(true) const current = await account.get() @@ -32,21 +32,24 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children } finally { setLoading(false) } - } + }, []) useEffect(() => { fetchUser() - }, []) + }, [fetchUser]) - const login = async (email: string, password: string) => { - await account.createEmailPasswordSession(email, password) - await fetchUser() - } + const login = useCallback( + async (email: string, password: string) => { + await account.createEmailPasswordSession(email, password) + await fetchUser() + }, + [fetchUser], + ) - const logout = async () => { + const logout = useCallback(async () => { await account.deleteSession('current') setUser(null) - } + }, []) const value = useMemo( () => ({ @@ -56,12 +59,13 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children logout, refresh: fetchUser, }), - [loading, user], + [fetchUser, loading, login, logout, user], ) return {children} } +// eslint-disable-next-line react-refresh/only-export-components export const useAuth = () => { const context = useContext(AuthContext) if (!context) throw new Error('useAuth deve ser usado dentro de AuthProvider') diff --git a/dashboard/src/layouts/DashboardLayout.tsx b/dashboard/src/layouts/DashboardLayout.tsx index fb41c4f..5753a10 100644 --- a/dashboard/src/layouts/DashboardLayout.tsx +++ b/dashboard/src/layouts/DashboardLayout.tsx @@ -1,4 +1,4 @@ -import { Cloud, Github, Home, LogOut, Settings } from 'lucide-react' +import { Cloud, Github, Home, LogOut, Settings, Terminal } from 'lucide-react' import { NavLink, Outlet, useNavigate } from 'react-router-dom' import { TerminalLogs } from '../components/TerminalLogs' import { useAuth } from '../contexts/Auth' @@ -11,9 +11,9 @@ const navItems = [ ] const activeClass = - 'flex items-center gap-2 rounded-lg bg-slate-800/60 px-3 py-2 text-slate-50 shadow-inner shadow-slate-900' + 'flex items-center gap-2 rounded-md bg-slate-800/80 px-3 py-2 text-slate-50 shadow-inner shadow-slate-950 border border-slate-700' const baseClass = - 'flex items-center gap-2 rounded-lg px-3 py-2 text-slate-300 hover:bg-slate-800/40 transition-colors duration-150' + 'flex items-center gap-2 rounded-md px-3 py-2 text-slate-300 hover:bg-slate-800/50 transition-colors duration-150 border border-transparent' export default function DashboardLayout() { const { user, logout } = useAuth() @@ -26,14 +26,14 @@ export default function DashboardLayout() { return (
-