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
This commit is contained in:
@@ -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: <Database className="w-4 h-4" />,
|
||||
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: <Globe className="w-4 h-4" />,
|
||||
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: <Code className="w-4 h-4" />,
|
||||
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<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<ellipse cx="12" cy="5" rx="9" ry="3" />
|
||||
<path d="M3 5V19A9 3 0 0 0 21 19V5" />
|
||||
<path d="M3 12A9 3 0 0 0 21 12" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function Globe(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="2" y1="12" x2="22" y2="12" />
|
||||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function Code(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="16 18 22 12 16 6" />
|
||||
<polyline points="8 6 2 12 8 18" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
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<string | null>(null);
|
||||
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
||||
const [analysis, setAnalysis] = useState<LogAnalysis | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<section id="demo-interattiva" className="w-full py-24 lg:py-32 bg-white" aria-labelledby="demo-heading">
|
||||
@@ -131,6 +115,9 @@ export const InteractiveDemo: React.FC = () => {
|
||||
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
|
||||
Seleziona un log di esempio e vedi come l'AI lo trasforma in un comando risolutivo in pochi secondi.
|
||||
</p>
|
||||
<p className="text-sm text-slate-500 mt-2">
|
||||
Backend: <code className="bg-slate-100 px-2 py-1 rounded">{API_URL}</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Two Column Layout */}
|
||||
@@ -154,22 +141,25 @@ export const InteractiveDemo: React.FC = () => {
|
||||
<div className="p-4 border-b border-slate-700">
|
||||
<p className="text-slate-400 text-sm mb-3">Seleziona un log di esempio:</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{DEMO_LOGS.map((log) => (
|
||||
<button
|
||||
key={log.id}
|
||||
onClick={() => handleLogSelect(log.id)}
|
||||
disabled={isAnalyzing}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
|
||||
selectedLog === log.id
|
||||
? 'bg-indigo-600 text-white'
|
||||
: 'bg-slate-700 text-slate-300 hover:bg-slate-600'
|
||||
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
aria-pressed={selectedLog === log.id}
|
||||
>
|
||||
{log.icon}
|
||||
{log.label}
|
||||
</button>
|
||||
))}
|
||||
{DEMO_LOGS.map((log) => {
|
||||
const IconComponent = log.icon;
|
||||
return (
|
||||
<button
|
||||
key={log.id}
|
||||
onClick={() => handleLogSelect(log.id)}
|
||||
disabled={isAnalyzing}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
|
||||
selectedLog === log.id
|
||||
? 'bg-indigo-600 text-white'
|
||||
: 'bg-slate-700 text-slate-300 hover:bg-slate-600'
|
||||
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
aria-pressed={selectedLog === log.id}
|
||||
>
|
||||
<IconComponent className="w-4 h-4" />
|
||||
{log.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -197,7 +187,7 @@ export const InteractiveDemo: React.FC = () => {
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>
|
||||
{!selectedLog && !isAnalyzing && !analysis && (
|
||||
{!selectedLog && !isAnalyzing && !analysis && !error && (
|
||||
<div className="h-full flex flex-col items-center justify-center text-center text-slate-400">
|
||||
<Activity className="w-16 h-16 mb-4 opacity-50" aria-hidden="true" />
|
||||
<p className="text-lg">L'output dell'analisi apparirà qui</p>
|
||||
@@ -218,12 +208,24 @@ export const InteractiveDemo: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{analysis && !isAnalyzing && (
|
||||
{error && !isAnalyzing && (
|
||||
<div className="h-full flex flex-col items-center justify-center text-center">
|
||||
<AlertCircle className="w-16 h-16 mb-4 text-red-500" aria-hidden="true" />
|
||||
<p className="text-lg font-medium text-red-700 mb-2">Errore</p>
|
||||
<p className="text-sm text-slate-600 mb-4">{error}</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
Assicurati che il fake-backend sia in esecuzione:<br />
|
||||
<code className="bg-slate-200 px-2 py-1 rounded">docker compose up fake-backend -d</code>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{analysis && !isAnalyzing && !error && (
|
||||
<div className="space-y-6">
|
||||
{/* Analysis Header */}
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 bg-indigo-100 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<AlertCircle className="w-5 h-5 text-indigo-600" aria-hidden="true" />
|
||||
<SelectedIcon className="w-5 h-5 text-indigo-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-slate-900">{analysis.title}</h3>
|
||||
|
||||
Reference in New Issue
Block a user