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:
Luca Sacchi Ricciardi
2026-04-03 16:57:14 +02:00
parent eb24b86308
commit 8eb7dfb00e
9 changed files with 1490 additions and 84 deletions

View File

@@ -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>