diff --git a/.github/workflows/backend-smoke.yml b/.github/workflows/backend-smoke.yml new file mode 100644 index 00000000..8138fa9c --- /dev/null +++ b/.github/workflows/backend-smoke.yml @@ -0,0 +1,67 @@ +name: Backend Smoke Test + +on: + push: + branches: [ main, master, dev ] + pull_request: + branches: [ main, master, dev ] + +jobs: + smoke-test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:15 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: mysecretpassword + POSTGRES_DB: lira_db + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v3 + + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: Chat/backend/package-lock.json + + - name: Install Dependencies + run: | + cd Chat/backend + npm ci + + - name: Setup Database + env: + DATABASE_URL: postgresql://postgres:mysecretpassword@localhost:5432/lira_db + run: | + cd Chat/backend + npx prisma db push + + - name: Start Backend + run: | + cd Chat/backend + npm run dev > backend.log 2>&1 & + echo "Waiting for backend to start..." + # Wait for port 4000 + timeout 30s bash -c 'until curl -s http://localhost:4000/health > /dev/null; do sleep 1; done' + echo "Backend started!" + env: + DATABASE_URL: postgresql://postgres:mysecretpassword@localhost:5432/lira_db + # Basic env vars needed for server startup + MISTRAL_API_KEY: "dummy_key_for_ci" + PORT: 4000 + + - name: Run Smoke Test + run: | + cd Chat/backend + npm run test:smoke diff --git a/Chat/.env.example b/Chat/.env.example new file mode 100644 index 00000000..cd277c45 --- /dev/null +++ b/Chat/.env.example @@ -0,0 +1,2 @@ +VITE_GEMINI_API_KEY=your_gemini_api_key_here +VITE_API_BASE_URL=http://localhost:4000 diff --git a/Chat/backend/diagnose_app.js b/Chat/backend/diagnose_app.js new file mode 100644 index 00000000..967019c3 --- /dev/null +++ b/Chat/backend/diagnose_app.js @@ -0,0 +1,117 @@ + +const BASE_URL = 'http://localhost:4000'; + +async function runDiagnosis() { + console.log('Starting System Diagnosis...\n'); + const results = { + health: null, + register: null, + login: null, + sessions: null, + memories: null, + apiHealth: null + }; + + // 1. Check Health + try { + const res = await fetch(`${BASE_URL}/health`); + results.health = { status: res.status, ok: res.ok }; + if (res.ok) console.log('✅ /health is UP'); + else console.error('❌ /health failed', res.status); + } catch (e) { + console.error('❌ /health error:', e.message); + results.health = { error: e.message }; + } + + // 2. Register + const testUser = { + email: `diag_${Date.now()}@example.com`, + password: 'password123', + username: 'diaguser' + }; + + try { + const res = await fetch(`${BASE_URL}/api/auth/register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(testUser) + }); + const data = await res.json(); + results.register = { status: res.status, ok: res.ok }; + + if (res.ok) { + console.log('✅ Registration successful'); + } else { + console.error('❌ Registration failed', data); + } + } catch (e) { + console.error('❌ Registration error:', e.message); + results.register = { error: e.message }; + } + + // 3. Login + let token = null; + try { + const res = await fetch(`${BASE_URL}/api/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: testUser.email, password: testUser.password }) + }); + const data = await res.json(); + results.login = { status: res.status, ok: res.ok }; + + if (res.ok && data.token) { + token = data.token; + console.log('✅ Login successful'); + } else { + console.error('❌ Login failed', data); + } + } catch (e) { + console.error('❌ Login error:', e.message); + results.login = { error: e.message }; + } + + if (token) { + const headers = { 'Authorization': `Bearer ${token}` }; + + // 4. Check Sessions + try { + const res = await fetch(`${BASE_URL}/api/chat/sessions`, { headers }); + const data = await res.json(); + results.sessions = { status: res.status, ok: res.ok, count: Array.isArray(data) ? data.length : 'unknown' }; + if (res.ok) console.log('✅ /api/chat/sessions accessed'); + else console.error('❌ /api/chat/sessions failed', res.status); + } catch (e) { + console.error('❌ /api/chat/sessions error:', e.message); + } + + // 5. Check Memories + try { + const res = await fetch(`${BASE_URL}/api/memories`, { headers }); + const data = await res.json(); + results.memories = { status: res.status, ok: res.ok }; + if (res.ok) console.log('✅ /api/memories accessed'); + else console.error('❌ /api/memories failed', res.status); + } catch (e) { + console.error('❌ /api/memories error:', e.message); + } + + // 6. Check /api/health (Protected) + // Note: Previous manual checks showed 404 for authorized /api/health, let's verify programmatically + try { + const res = await fetch(`${BASE_URL}/api/health`, { headers }); + results.apiHealth = { status: res.status, ok: res.ok }; + if (res.ok) console.log('✅ /api/health accessed (Authorized)'); + else console.warn(`⚠️ /api/health returned ${res.status} (Expected if route doesn't exist)`); + } catch (e) { + console.error('❌ /api/health error:', e.message); + } + } else { + console.warn('⚠️ Skipping authenticated checks due to login failure'); + } + + console.log('\nDiagnosis Complete.'); + console.log(JSON.stringify(results, null, 2)); +} + +runDiagnosis(); diff --git a/Chat/backend/package.json b/Chat/backend/package.json index 230e7aac..fee774bc 100644 --- a/Chat/backend/package.json +++ b/Chat/backend/package.json @@ -5,6 +5,7 @@ "scripts": { "dev": "node server.js", "start": "node server.js", + "test:smoke": "node smoke_test.js", "postinstall": "prisma generate" }, "dependencies": { diff --git a/Chat/backend/routes/developer/index.js b/Chat/backend/routes/developer/index.js new file mode 100644 index 00000000..f8eb8915 --- /dev/null +++ b/Chat/backend/routes/developer/index.js @@ -0,0 +1,80 @@ +import express from 'express'; +import { requireAuth } from '../../middlewares/authMiddleware.js'; +import { isAdmin } from '../../authStore.js'; +import prisma from '../../prismaClient.js'; +import { pcController } from '../../services/pcControllerService.js'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import modulesRouter from './modules.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const BACKEND_ROOT = path.resolve(__dirname, '../../'); + +const router = express.Router(); + +// Middleware: Require Admin +router.use(requireAuth); +router.use(async (req, res, next) => { + const isAdm = await isAdmin(req.userId); + if (!isAdm) return res.status(403).json({ error: 'Developer access only' }); + next(); +}); + +// GET /api/developer/stats - Global System Stats +router.get('/stats', async (req, res) => { + try { + const userCount = await prisma.user.count(); + const sessionCount = await prisma.session.count(); + const memoryCount = await prisma.memory.count(); + const activeJobs = await prisma.imageJob.count({ where: { status: 'generating' } }); + + // System Resource Stats + const systemStats = await pcController.getSystemStats(); + + res.json({ + application: { + users: userCount, + sessions: sessionCount, + memories: memoryCount, + active_jobs: activeJobs + }, + system: systemStats, + timestamp: Date.now() + }); + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// GET /api/developer/logs - Recent Application Logs +router.get('/logs', async (req, res) => { + try { + const logPath = path.join(BACKEND_ROOT, 'backend.log'); + if (fs.existsSync(logPath)) { + // Read last 1000 lines or last 50KB roughly + const content = fs.readFileSync(logPath, 'utf-8'); + const lines = content.split('\n').slice(-200); // Last 200 lines + res.json({ logs: lines }); + } else { + res.json({ logs: [], message: 'No log file found' }); + } + } catch (e) { + res.status(500).json({ error: e.message }); + } +}); + +// GET /api/developer/config - View AI Config (Redacted) +router.get('/config', (req, res) => { + res.json({ + mistral_model: process.env.MISTRAL_MODEL || 'mistral-medium', + xiaomi_model: process.env.XIAOMI_MODEL, + voice_provider: process.env.ELEVENLABS_API_KEY ? 'ElevenLabs' : 'XTTS (Local)', + vision_enabled: !!process.env.MISTRAL_PIXTRAL_API_KEY + }); +}); + +router.use('/modules', modulesRouter); + +export default router; diff --git a/Chat/backend/routes/developer/modules.js b/Chat/backend/routes/developer/modules.js new file mode 100644 index 00000000..34f9606f --- /dev/null +++ b/Chat/backend/routes/developer/modules.js @@ -0,0 +1,67 @@ +// Chat/backend/routes/developer/modules.js +import express from 'express'; +const router = express.Router(); + +const modulesData = [ + { + id: "lira-os-chat", + name: "LiraOS Chat", + type: "frontend", + root_path: "Chat/client", + main_files: ["src/App.tsx", "src/main.tsx"], + description: "Interface de Chat da LiraOS", + tags: ["react", "vite", "frontend"], + status: "active", + priority: "high" + }, + { + id: "lira-os-backend", + name: "LiraOS Backend", + type: "backend", + root_path: "Chat/backend", + main_files: ["server.js", "services/ai.js"], + description: "API e Backend Principal", + tags: ["node", "express", "backend"], + status: "active", + priority: "high" + }, + { + id: "lira-gamer", + name: "Lira Gamer", + type: "module", + root_path: "LiraGamer", + main_files: ["main.py", "game_agent.py"], + description: "Módulo de Integração com Jogos", + tags: ["python", "ai", "vision"], + status: "developing", + priority: "medium" + }, + { + id: "dashboard", + name: "Lira Developer Dashboard", + type: "tool", + root_path: "lira-developer-dashboard", + main_files: ["src/App.tsx"], + description: "Dashboard de Desenvolvimento", + tags: ["react", "vite", "dashboard"], + status: "active", + priority: "medium" + } +]; + +// GET /api/developer/modules +router.get('/', (req, res) => { + res.json(modulesData); +}); + +// GET /api/developer/modules/:id +router.get('/:id', (req, res) => { + const mod = modulesData.find(m => m.id === req.params.id); + if (mod) { + res.json(mod); + } else { + res.status(404).json({ error: "Module not found" }); + } +}); + +export default router; diff --git a/Chat/backend/server.js b/Chat/backend/server.js index f682924c..12602e81 100644 --- a/Chat/backend/server.js +++ b/Chat/backend/server.js @@ -31,6 +31,7 @@ import instagramRoutes from './routes/instagram.js'; import imagesRoutes from './routes/images.js'; import todosRoutes from './routes/todos.js'; import googleAuthRoutes from './routes/authGoogle.js'; +import developerRoutes from './routes/developer/index.js'; // Services & Utils import { discordService } from './services/discordService.js'; @@ -73,6 +74,7 @@ app.use((req, res, next) => { // Health Check app.get('/health', (req, res) => res.json({ status: 'ok', timestamp: Date.now() })); +app.get('/api/health', (req, res) => res.json({ status: 'ok', timestamp: Date.now() })); // Routes Mounting console.log('[DEBUG] Mounting routes...'); @@ -114,6 +116,7 @@ app.use('/api/patreon', patreonRoutes); app.use('/api/instagram', instagramRoutes); app.use('/api/todos', todosRoutes); app.use('/api/auth/google', googleAuthRoutes); +app.use('/api/developer', developerRoutes); // Generic fallback (must be last) app.use('/api', chatRoutes); console.log('[DEBUG] All routes mounted successfully'); diff --git a/Chat/backend/smoke_test.js b/Chat/backend/smoke_test.js new file mode 100644 index 00000000..12513a4d --- /dev/null +++ b/Chat/backend/smoke_test.js @@ -0,0 +1,77 @@ + +const BASE_URL = process.env.BASE_URL || 'http://localhost:4000'; + +async function runSmokeTest() { + console.log('🔥 Starting Smoke Test...\n'); + let exitCode = 0; + + try { + // 1. Check Health + console.log('1. Checking /health...'); + const healthRes = await fetch(`${BASE_URL}/health`); + if (!healthRes.ok) throw new Error(`/health failed with status ${healthRes.status}`); + console.log('✅ /health is UP'); + + // 2. Check /api/health (New Requirement) + console.log('2. Checking /api/health...'); + const apiHealthRes = await fetch(`${BASE_URL}/api/health`); + if (!apiHealthRes.ok) throw new Error(`/api/health failed with status ${apiHealthRes.status}`); + console.log('✅ /api/health is UP'); + + // 3. Register + console.log('3. Testing Registration...'); + const testUser = { + email: `smoke_${Date.now()}@example.com`, + password: 'password123', + username: 'smokeuser' + }; + + const regRes = await fetch(`${BASE_URL}/api/auth/register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(testUser) + }); + + if (!regRes.ok) { + const err = await regRes.text(); + throw new Error(`Registration failed: ${regRes.status} - ${err}`); + } + const regData = await regRes.json(); + console.log('✅ Registration successful'); + + // 4. Login + console.log('4. Testing Login...'); + const loginRes = await fetch(`${BASE_URL}/api/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: testUser.email, password: testUser.password }) + }); + + if (!loginRes.ok) { + const err = await loginRes.text(); + throw new Error(`Login failed: ${loginRes.status} - ${err}`); + } + const loginData = await loginRes.json(); + const token = loginData.token; + if (!token) throw new Error('No token received in login'); + console.log('✅ Login successful'); + + // 5. Check Protected Route (Sessions) + console.log('5. Testing Protected API (Sessions)...'); + const sessRes = await fetch(`${BASE_URL}/api/chat/sessions`, { + headers: { 'Authorization': `Bearer ${token}` } + }); + + if (!sessRes.ok) throw new Error(`/api/chat/sessions failed with status ${sessRes.status}`); + console.log('✅ /api/chat/sessions accessed'); + + } catch (error) { + console.error(`\n❌ Smoke Test FAILED: ${error.message}`); + exitCode = 1; + } finally { + console.log('\n🏁 Smoke Test Complete.'); + process.exit(exitCode); + } +} + +runSmokeTest(); diff --git a/Chat/components/Sidebar.tsx b/Chat/components/Sidebar.tsx index 22793452..9af3a11c 100644 --- a/Chat/components/Sidebar.tsx +++ b/Chat/components/Sidebar.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; -import { Plus, MessageSquare, Search, Settings, X, Trash2, Sparkles, Command, LayoutGrid, ShoppingBag, Keyboard, Shield, Video, Crown, Gamepad2, Gift, CheckSquare, Calendar } from 'lucide-react'; +import { Plus, MessageSquare, Search, Settings, X, Trash2, Sparkles, Command, LayoutGrid, ShoppingBag, Keyboard, Shield, Video, Crown, Gamepad2, Gift, CheckSquare, Calendar, Activity } from 'lucide-react'; import { ChatSession } from '../types'; import { LIRA_AVATAR } from '../constants'; import { getCurrentUser } from '../services/userService'; @@ -185,6 +185,11 @@ export const Sidebar: React.FC = ({ >
{t('sidebar.my_items')}
+ +