From 8eb7dfb00ef38b9c378b8ac6d940bb27ce0d8a89 Mon Sep 17 00:00:00 2001 From: Luca Sacchi Ricciardi Date: Fri, 3 Apr 2026 16:57:14 +0200 Subject: [PATCH] feat: add fake-backend mock API server for frontend development Create mock backend to simulate AI responses for UI development: Backend Implementation: - tools/fake-backend/server.js: Express server with CORS - POST /api/analyze: Accepts log, returns mock AI analysis with 1.5s delay - GET /health: Health check endpoint - Pattern matching for different log types (PostgreSQL, Nginx, Node.js, Disk) - Error handling: 400 for empty payload, 500 for server errors - Mock responses for common errors (OOM, 502, connection refused, disk full) Container Setup: - Dockerfile: Node.js 20 Alpine container - docker-compose.yml: Added fake-backend service on port 3000 - Health checks for both frontend and backend services - Environment variable VITE_API_URL for frontend Frontend Integration: - InteractiveDemo.tsx: Replaced static data with real fetch() calls - API_URL configurable via env var (default: http://localhost:3000) - Error handling with user-friendly messages - Shows backend URL in demo section - Maintains loading states and UI feedback Documentation: - docs/tools_fake_backend.md: Complete usage guide - README.md: Updated with tools/fake-backend structure and usage Development Workflow: 1. docker compose up -d (starts both frontend and backend) 2. Frontend calls http://fake-backend:3000/api/analyze 3. Backend returns realistic mock responses 4. No OpenRouter API costs during development Safety First: - No real API calls during development - Isolated mock logic in dedicated tool - Easy switch to real backend by changing URL - CORS enabled only for development Refs: Sprint 4 preparation, API development workflow --- README.md | 30 + docker-compose.yml | 25 +- docs/tools_fake_backend.md | 192 ++++ .../components/sections/InteractiveDemo.tsx | 168 ++-- tools/fake-backend/.gitignore | 33 + tools/fake-backend/Dockerfile | 31 + tools/fake-backend/package-lock.json | 854 ++++++++++++++++++ tools/fake-backend/package.json | 17 + tools/fake-backend/server.js | 224 +++++ 9 files changed, 1490 insertions(+), 84 deletions(-) create mode 100644 docs/tools_fake_backend.md create mode 100644 tools/fake-backend/.gitignore create mode 100644 tools/fake-backend/Dockerfile create mode 100644 tools/fake-backend/package-lock.json create mode 100644 tools/fake-backend/package.json create mode 100644 tools/fake-backend/server.js diff --git a/README.md b/README.md index b80be4d..9a8d1cf 100644 --- a/README.md +++ b/README.md @@ -237,6 +237,11 @@ LogWhispererAI/ ├── tests/ │ ├── __init__.py │ └── test_logwhisperer.py # Test suite Python +├── tools/ +│ └── fake-backend/ # Mock API server per sviluppo frontend +│ ├── server.js # Server Express mock +│ ├── Dockerfile # Containerizzazione +│ └── README.md # Documentazione tool └── .opencode/ ├── opencode.json # Configurazione MCP servers ├── agents/ # Configurazioni agenti individuali @@ -256,6 +261,31 @@ LogWhispererAI/ └── context7_documentation_retrivial/ ``` +## 🛠️ Tools di Sviluppo + +### Fake Backend (Mock API) + +Per sviluppare e testare il frontend senza dipendere dal backend reale: + +```bash +# Avvia il mock API server +cd tools/fake-backend +npm install +node server.js + +# Oppure con Docker +docker compose up fake-backend -d +``` + +**Endpoint:** `http://localhost:3000/api/analyze` + +Simula le risposte AI con delay di 1.5s. Utile per: +- Sviluppo UI offline +- Testing senza costi API +- Demo senza dipendenze esterne + +Vedi `docs/tools_fake_backend.md` per documentazione completa. + ## ⚖️ Licenza e Note Legali Questo software è **proprietà riservata** di Luca Sacchi Ricciardi. diff --git a/docker-compose.yml b/docker-compose.yml index ee8360d..e7eadb9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,7 @@ # Docker Compose - LogWhisperer AI Development Environment # Usage: docker compose up -d -# Access: http://localhost:5173 +# Access Frontend: http://localhost:5173 +# Access Fake Backend API: http://localhost:3000 services: frontend: @@ -18,8 +19,11 @@ services: environment: - NODE_ENV=development - CHOKIDAR_USEPOLLING=true + - VITE_API_URL=http://fake-backend:3000 # Ensure container restarts on failure restart: unless-stopped + depends_on: + - fake-backend # Health check to verify the service is running healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://localhost:5173"] @@ -28,6 +32,25 @@ services: retries: 3 start_period: 40s + fake-backend: + build: + context: ./tools/fake-backend + dockerfile: Dockerfile + container_name: logwhisperer-fake-backend + ports: + - "3000:3000" + environment: + - PORT=3000 + - DELAY_MS=1500 + - NODE_ENV=production + restart: unless-stopped + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (r) => r.statusCode === 200 ? process.exit(0) : process.exit(1))"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 5s + volumes: node_modules: driver: local \ No newline at end of file diff --git a/docs/tools_fake_backend.md b/docs/tools_fake_backend.md new file mode 100644 index 0000000..dd8968e --- /dev/null +++ b/docs/tools_fake_backend.md @@ -0,0 +1,192 @@ +# Fake Backend Tool + +> **Strumento di sviluppo per simulare risposte API AI** +> +> Scopo: Permettere lo sviluppo e il testing del frontend senza dipendere dal backend reale o dalle API di OpenRouter. + +--- + +## 📋 Descrizione + +Il `fake-backend` è un server Node.js/Express che simula le risposte dell'API di LogWhisperer AI. È progettato per: + +- **Sviluppo UI**: Testare l'interfaccia utente con risposte realistiche +- **Demo offline**: Mostrare il prodotto senza connessione internet +- **Testing**: Validare il flusso frontend senza costi API +- **Onboarding**: Permettere ai nuovi sviluppatori di lavorare subito + +--- + +## 🚀 Endpoint API + +### POST /api/analyze + +Analizza un log e restituisce una risposta AI simulata. + +**Request:** +```http +POST http://localhost:3000/api/analyze +Content-Type: application/json + +{ + "log": "FATAL: database system is out of memory" +} +``` + +**Response (dopo 1.5s delay):** +```json +{ + "success": true, + "analysis": { + "title": "PostgreSQL Out of Memory", + "description": "Il database ha esaurito la memoria disponibile...", + "command": "ps aux | grep postgres | head -5 && free -h", + "isSafe": true, + "note": "Verifica processi Postgres e memoria disponibile." + }, + "timestamp": "2026-04-03T10:30:00.000Z" +} +``` + +**Errori gestiti:** +- `400 Bad Request`: Payload vuoto o malformato +- `500 Internal Error`: Errore server generico + +--- + +## 🛠️ Setup + +### Prerequisiti +- Node.js 18+ +- npm 9+ + +### Installazione + +```bash +# Entra nella directory +cd tools/fake-backend + +# Installa dipendenze +npm install + +# Avvia il server +node server.js +``` + +Il server sarà disponibile su `http://localhost:3000` + +--- + +## 🐳 Docker + +Per avviare il fake backend con Docker: + +```bash +# Dalla root del progetto +docker compose up fake-backend -d +``` + +Il servizio sarà esposto sulla porta 3000. + +--- + +## 🔧 Configurazione + +Variabili ambiente (opzionali): + +```bash +PORT=3000 # Porta server (default: 3000) +DELAY_MS=1500 # Delay simulazione AI (default: 1500ms) +``` + +--- + +## 📁 Struttura + +``` +tools/fake-backend/ +├── server.js # Server Express principale +├── package.json # Dipendenze npm +├── Dockerfile # Containerizzazione +└── README.md # Questo file +``` + +--- + +## 🔒 Sicurezza + +⚠️ **ATTENZIONE**: Questo è uno strumento di sviluppo. NON usarlo in produzione: +- Nessuna autenticazione implementata +- Risposte statiche predefinite +- Nessuna validazione input avanzata +- CORS abilitato per tutte le origini + +--- + +## 🧪 Test Manuale + +```bash +# Test endpoint analyze +curl -X POST http://localhost:3000/api/analyze \ + -H "Content-Type: application/json" \ + -d '{"log": "Error: Connection refused"}' +``` + +--- + +## 📝 Note per Sviluppatori + +### Aggiungere nuove risposte mock + +Modifica l'oggetto `MOCK_RESPONSES` in `server.js`: + +```javascript +const MOCK_RESPONSES = { + 'errore-specifico': { + title: 'Titolo Problema', + description: 'Descrizione...', + command: 'comando-da-eseguire', + isSafe: true, + note: 'Nota aggiuntiva' + } +}; +``` + +### Pattern matching + +Il server cerca keyword nel log e restituisce la risposta appropriata: +- "memory" / "oom" → PostgreSQL OOM +- "connection refused" / "502" → Nginx/Connection error +- Default → Risposta generica + +--- + +## 🤝 Integrazione Frontend + +Per usare il fake backend dal frontend React: + +```typescript +const analyzeLog = async (log: string) => { + const response = await fetch('http://localhost:3000/api/analyze', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ log }) + }); + return response.json(); +}; +``` + +--- + +## 🔄 Passaggio a Backend Reale + +Quando il backend reale è pronto: + +1. Aggiorna l'URL in `InteractiveDemo.tsx` +2. Aggiungi autenticazione (JWT/API Key) +3. Rimuovi CORS permissivo +4. Implementa rate limiting + +--- + +*Strumento creato seguendo il principio "Safety First" del Metodo Sacchi* diff --git a/frontend/src/components/sections/InteractiveDemo.tsx b/frontend/src/components/sections/InteractiveDemo.tsx index 1354418..606d469 100644 --- a/frontend/src/components/sections/InteractiveDemo.tsx +++ b/frontend/src/components/sections/InteractiveDemo.tsx @@ -1,85 +1,34 @@ import React, { useState } from 'react'; -import { Terminal, Copy, Check, Loader2, AlertCircle, Activity } from 'lucide-react'; +import { Terminal, Copy, Check, Loader2, AlertCircle, Activity, Server, Globe, Code } from 'lucide-react'; -// Mock data for demo logs +// Demo log presets - only the content, not the analysis const DEMO_LOGS = [ { id: 'postgres-oom', label: 'PostgreSQL OOM', - icon: , + icon: Server, logContent: `FATAL: database system is out of memory DETAIL: Failed on request of size 8192 HINT: Check memory usage and limits CONTEXT: automatic vacuum of table "public.events"`, - analysis: { - title: 'PostgreSQL Out of Memory', - description: 'Il database ha esaurito la memoria disponibile durante un\'operazione di vacuum automatico su una tabella molto grande.', - command: 'ps aux | grep postgres | head -5 && free -h', - isSafe: true, - note: 'Verifica processi Postgres e memoria disponibile. Se necessario, aumenta work_mem o shared_buffers.', - }, }, { id: 'nginx-502', label: 'Nginx 502 Bad Gateway', - icon: , + icon: Globe, logContent: `2024/01/15 14:32:15 [error] 1234#1234: *56789 connect() failed (111: Connection refused) while connecting to upstream, client: 192.168.1.100, server: api.example.com, upstream: "127.0.0.1:3000"`, - analysis: { - title: 'Nginx 502 - Backend Non Raggiungibile', - description: 'Nginx non riesce a connettersi al backend sulla porta 3000. Probabilmente il servizio è down.', - command: 'sudo systemctl status app-service && netstat -tlnp | grep 3000', - isSafe: true, - note: 'Verifica stato del servizio backend. Se stopped, avvia con: sudo systemctl start app-service', - }, }, { id: 'node-exception', label: 'Node.js Exception', - icon: , + icon: Code, logContent: `Error: connect ECONNREFUSED 127.0.0.1:5432 at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1141:16) at emitErrorNT (internal/streams/destroy.js:92:8) at processTicksAndRejections (internal/process/task_queues.js:80:21)`, - analysis: { - title: 'Node.js - Connessione Database Rifiutata', - description: 'L\'applicazione Node non riesce a connettersi al database PostgreSQL sulla porta 5432.', - command: 'sudo systemctl status postgresql && sudo netstat -tlnp | grep 5432', - isSafe: true, - note: 'Verifica che PostgreSQL sia in esecuzione. Se down, avvia con: sudo systemctl start postgresql', - }, }, ]; -// Icon components -function Database(props: React.SVGProps) { - return ( - - - - - - ); -} - -function Globe(props: React.SVGProps) { - return ( - - - - - - ); -} - -function Code(props: React.SVGProps) { - return ( - - - - - ); -} - interface LogAnalysis { title: string; description: string; @@ -88,26 +37,60 @@ interface LogAnalysis { note: string; } +interface ApiResponse { + success: boolean; + analysis: LogAnalysis & { originalLog?: string }; + meta?: { + processingTime: string; + model: string; + timestamp: string; + }; + error?: string; + message?: string; +} + +const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000'; + export const InteractiveDemo: React.FC = () => { const [selectedLog, setSelectedLog] = useState(null); const [isAnalyzing, setIsAnalyzing] = useState(false); const [analysis, setAnalysis] = useState(null); const [copied, setCopied] = useState(false); + const [error, setError] = useState(null); + + const handleLogSelect = async (logId: string) => { + const logData = DEMO_LOGS.find((l) => l.id === logId); + if (!logData) return; - const handleLogSelect = (logId: string) => { setSelectedLog(logId); setIsAnalyzing(true); setAnalysis(null); setCopied(false); + setError(null); - // Simulate AI analysis delay - setTimeout(() => { - const log = DEMO_LOGS.find((l) => l.id === logId); - if (log) { - setAnalysis(log.analysis); - setIsAnalyzing(false); + try { + // Real API call to fake-backend + const response = await fetch(`${API_URL}/api/analyze`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ log: logData.logContent }), + }); + + const data: ApiResponse = await response.json(); + + if (!response.ok || !data.success) { + throw new Error(data.message || `HTTP error! status: ${response.status}`); } - }, 1500); + + setAnalysis(data.analysis); + } catch (err) { + console.error('API Error:', err); + setError(err instanceof Error ? err.message : 'Errore durante l\'analisi. Verifica che il backend sia attivo.'); + } finally { + setIsAnalyzing(false); + } }; const handleCopyCommand = () => { @@ -119,6 +102,7 @@ export const InteractiveDemo: React.FC = () => { }; const selectedLogData = DEMO_LOGS.find((l) => l.id === selectedLog); + const SelectedIcon = selectedLogData?.icon || Terminal; return (
@@ -131,6 +115,9 @@ export const InteractiveDemo: React.FC = () => {

Seleziona un log di esempio e vedi come l'AI lo trasforma in un comando risolutivo in pochi secondi.

+

+ Backend: {API_URL} +

{/* Two Column Layout */} @@ -154,22 +141,25 @@ export const InteractiveDemo: React.FC = () => {

Seleziona un log di esempio:

- {DEMO_LOGS.map((log) => ( - - ))} + {DEMO_LOGS.map((log) => { + const IconComponent = log.icon; + return ( + + ); + })}
@@ -197,7 +187,7 @@ export const InteractiveDemo: React.FC = () => { aria-live="polite" aria-atomic="true" > - {!selectedLog && !isAnalyzing && !analysis && ( + {!selectedLog && !isAnalyzing && !analysis && !error && (
)} - {analysis && !isAnalyzing && ( + {error && !isAnalyzing && ( +
+
+ )} + + {analysis && !isAnalyzing && !error && (
{/* Analysis Header */}
-

{analysis.title}

diff --git a/tools/fake-backend/.gitignore b/tools/fake-backend/.gitignore new file mode 100644 index 0000000..dc4dc4b --- /dev/null +++ b/tools/fake-backend/.gitignore @@ -0,0 +1,33 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ + +# Logs +logs +*.log + +# Environment variables +.env +.env.local +.env.*.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/tools/fake-backend/Dockerfile b/tools/fake-backend/Dockerfile new file mode 100644 index 0000000..dd56154 --- /dev/null +++ b/tools/fake-backend/Dockerfile @@ -0,0 +1,31 @@ +# Dockerfile for Fake Backend +# Development mock API server for LogWhisperer AI + +FROM node:20-alpine + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci --only=production + +# Copy server code +COPY server.js ./ + +# Expose port 3000 +EXPOSE 3000 + +# Set environment variables +ENV PORT=3000 +ENV DELAY_MS=1500 +ENV NODE_ENV=production + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3000/health', (r) => r.statusCode === 200 ? process.exit(0) : process.exit(1))" + +# Start server +CMD ["node", "server.js"] \ No newline at end of file diff --git a/tools/fake-backend/package-lock.json b/tools/fake-backend/package-lock.json new file mode 100644 index 0000000..d2ac52d --- /dev/null +++ b/tools/fake-backend/package-lock.json @@ -0,0 +1,854 @@ +{ + "name": "fake-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fake-backend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "cors": "^2.8.6", + "express": "^5.2.1" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/tools/fake-backend/package.json b/tools/fake-backend/package.json new file mode 100644 index 0000000..cf09e07 --- /dev/null +++ b/tools/fake-backend/package.json @@ -0,0 +1,17 @@ +{ + "name": "fake-backend", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "cors": "^2.8.6", + "express": "^5.2.1" + } +} diff --git a/tools/fake-backend/server.js b/tools/fake-backend/server.js new file mode 100644 index 0000000..9532cb5 --- /dev/null +++ b/tools/fake-backend/server.js @@ -0,0 +1,224 @@ +/** + * Fake Backend Server for LogWhisperer AI + * + * Simulates AI analysis responses for frontend development + * without requiring real backend or OpenRouter API calls. + * + * @author LogWhisperer AI Team + * @version 1.0.0 + */ + +const express = require('express'); +const cors = require('cors'); + +const app = express(); +const PORT = process.env.PORT || 3000; +const DELAY_MS = parseInt(process.env.DELAY_MS) || 1500; + +// Enable CORS for all origins (development only!) +app.use(cors({ + origin: '*', + methods: ['GET', 'POST'], + allowedHeaders: ['Content-Type'] +})); + +// Parse JSON bodies +app.use(express.json()); + +// Mock responses database +const MOCK_RESPONSES = { + // PostgreSQL errors + 'memory': { + title: 'PostgreSQL Out of Memory', + description: 'Il database ha esaurito la memoria disponibile. Questo è spesso causato da query troppo pesanti o da un numero eccessivo di connessioni.', + command: 'ps aux | grep postgres | head -5 && free -h', + isSafe: true, + note: 'Verifica processi Postgres e memoria disponibile. Se necessario, riavvia il servizio con: sudo systemctl restart postgresql' + }, + 'oom': { + title: 'PostgreSQL Out of Memory', + description: 'Out Of Memory error rilevato. Il sistema ha esaurito la RAM disponibile.', + command: 'free -h && ps aux --sort=-%mem | head -10', + isSafe: true, + note: 'Identifica i processi che consumano più memoria e considera di aumentare la RAM o ottimizzare le query.' + }, + // Nginx/Connection errors + 'connection refused': { + title: 'Connessione al Backend Rifiutata', + description: 'Il reverse proxy (Nginx) non riesce a connettersi al backend. Il servizio potrebbe essere down.', + command: 'sudo systemctl status app-service && netstat -tlnp | grep 3000', + isSafe: true, + note: 'Verifica che il servizio backend sia in esecuzione. Se stopped, avvia con: sudo systemctl start app-service' + }, + '502': { + title: 'Nginx 502 Bad Gateway', + description: 'Nginx riceve errore dal server upstream. Il backend non risponde correttamente.', + command: 'sudo systemctl status backend && tail -n 50 /var/log/backend/error.log', + isSafe: true, + note: 'Controlla i log del backend per errori specifici. Potrebbe essere necessario un riavvio.' + }, + // Node.js errors + 'econnrefused': { + title: 'Node.js - Connessione Database Rifiutata', + description: 'L\'applicazione Node non riesce a connettersi al database PostgreSQL sulla porta 5432.', + command: 'sudo systemctl status postgresql && sudo netstat -tlnp | grep 5432', + isSafe: true, + note: 'Verifica che PostgreSQL sia in esecuzione. Se down, avvia con: sudo systemctl start postgresql' + }, + 'exception': { + title: 'Node.js Exception', + description: 'Eccezione non gestita nell\'applicazione Node.js. Potrebbe essere un errore di connessione o configurazione.', + command: 'pm2 logs app --lines 50', + isSafe: true, + note: 'Controlla i log dell\'applicazione per l\'errore completo. Usa pm2 restart app se necessario.' + }, + // Disk errors + 'disk': { + title: 'Spazio su Disco Esaurito', + description: 'Il filesystem ha raggiunto il 100% di utilizzo. Nessuna scrittura possibile.', + command: 'df -h && du -sh /var/log/* | sort -hr | head -10', + isSafe: true, + note: 'Identifica quali directory occupano più spazio. Pulisci log vecchi con: sudo find /var/log -name "*.log" -mtime +7 -delete' + }, + 'no space': { + title: 'Spazio su Disco Esaurito', + description: 'Spazio insufficiente sul disco. Impossibile scrivere nuovi dati.', + command: 'df -h && du -sh /tmp /var/log /var/cache', + isSafe: true, + note: 'Libera spazio eliminando file temporanei o log vecchi.' + } +}; + +// Default response for unknown errors +const DEFAULT_RESPONSE = { + title: 'Errore di Sistema', + description: 'È stato rilevato un errore nel log. L\'analisi suggerisce di verificare lo stato dei servizi e le risorse di sistema.', + command: 'sudo systemctl status && df -h && free -h', + isSafe: true, + note: 'Esegui il comando sopra per verificare lo stato generale del sistema. Se il problema persiste, controlla i log specifici del servizio.' +}; + +/** + * Find matching mock response based on log content + * @param {string} logContent - The log content to analyze + * @returns {object} - Matching response or default + */ +function findMockResponse(logContent) { + const logLower = logContent.toLowerCase(); + + for (const [keyword, response] of Object.entries(MOCK_RESPONSES)) { + if (logLower.includes(keyword.toLowerCase())) { + return response; + } + } + + return DEFAULT_RESPONSE; +} + +/** + * POST /api/analyze + * Analyze log and return AI-like response + */ +app.post('/api/analyze', (req, res) => { + const { log } = req.body; + + // Validate input + if (!log || typeof log !== 'string' || log.trim().length === 0) { + return res.status(400).json({ + success: false, + error: 'Bad Request', + message: 'Il campo "log" è richiesto e non può essere vuoto', + timestamp: new Date().toISOString() + }); + } + + // Simulate AI processing delay + setTimeout(() => { + try { + const analysis = findMockResponse(log); + + res.json({ + success: true, + analysis: { + ...analysis, + originalLog: log.substring(0, 500) // Include first 500 chars for reference + }, + meta: { + processingTime: `${DELAY_MS}ms`, + model: 'fake-backend-mock-v1', + timestamp: new Date().toISOString() + } + }); + } catch (error) { + console.error('Error processing request:', error); + res.status(500).json({ + success: false, + error: 'Internal Server Error', + message: 'Errore durante l\'analisi del log', + timestamp: new Date().toISOString() + }); + } + }, DELAY_MS); +}); + +/** + * GET /health + * Health check endpoint + */ +app.get('/health', (req, res) => { + res.json({ + status: 'ok', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + version: '1.0.0' + }); +}); + +/** + * GET / + * Root endpoint with API info + */ +app.get('/', (req, res) => { + res.json({ + name: 'LogWhisperer AI - Fake Backend', + version: '1.0.0', + description: 'Mock API server for frontend development', + endpoints: { + 'POST /api/analyze': 'Analyze log and return AI-like response', + 'GET /health': 'Health check endpoint' + }, + documentation: 'See docs/tools_fake_backend.md' + }); +}); + +// Error handling middleware +app.use((err, req, res, next) => { + console.error('Unhandled error:', err); + res.status(500).json({ + success: false, + error: 'Internal Server Error', + message: 'Errore imprevisto del server', + timestamp: new Date().toISOString() + }); +}); + +// Start server +app.listen(PORT, () => { + console.log(` +╔══════════════════════════════════════════════════════════════╗ +║ LogWhisperer AI - Fake Backend Server ║ +║ ║ +║ 🚀 Server running on http://localhost:${PORT} ║ +║ 📖 Documentation: docs/tools_fake_backend.md ║ +║ ⏱️ Simulated delay: ${DELAY_MS}ms ║ +║ ║ +║ Endpoints: ║ +║ POST /api/analyze - Analyze log and get mock AI response ║ +║ GET /health - Health check ║ +║ ║ +║ Press Ctrl+C to stop ║ +╚══════════════════════════════════════════════════════════════╝ + `); +}); + +module.exports = app; // Export for testing \ No newline at end of file