Files
LogWhispererAI/frontend/src/components/sections/OnboardingWizard.tsx
Luca Sacchi Ricciardi 26879acba4 feat: add production configuration with environment variables
- Add .env file for production deployment with reverse proxy
- Add docker-compose.prod.yml for production profile
- Add docker-compose.override.yml for local development
- Update docker-compose.yml with all configurable variables
- Update frontend to use VITE_* environment variables
- Update backend to support CORS_ORIGINS and WEBHOOK_BASE_URL
- Add vite.config.ts allowedHosts for reverse proxy
- Add documentation for docker-compose and reverse proxy setup

All URLs are now configurable via environment variables:
- VITE_API_URL: Backend API endpoint
- VITE_WEBHOOK_BASE_URL: Webhook base URL
- VITE_INSTALL_SCRIPT_URL: Install script URL
- VITE_APP_URL: Frontend URL
- CORS_ORIGINS: Allowed CORS origins
- WEBHOOK_BASE_URL: Backend webhook base URL
2026-04-03 18:49:53 +02:00

389 lines
16 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useRef, useEffect } from 'react';
import {
ChevronRight,
ChevronLeft,
Check,
Copy,
Terminal,
Rocket,
Settings,
AlertCircle,
RefreshCw
} from 'lucide-react';
interface OnboardingWizardProps {
onComplete?: () => void;
}
export const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete }) => {
const [currentStep, setCurrentStep] = useState(1);
const [webhookUrl, setWebhookUrl] = useState('');
const [copied, setCopied] = useState(false);
const [isGenerating, setIsGenerating] = useState(false);
const stepRef = useRef<HTMLDivElement>(null);
// Focus management for accessibility - only when step changes, not on initial mount
useEffect(() => {
// Only focus if this is a step change (not initial mount)
// This prevents auto-scroll to the onboarding section on page load
if (stepRef.current && document.activeElement !== document.body) {
stepRef.current.focus({ preventScroll: true });
}
}, [currentStep]);
// Environment configuration
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001';
const WEBHOOK_BASE_URL = import.meta.env.VITE_WEBHOOK_BASE_URL || `${API_URL}/webhook`;
const INSTALL_SCRIPT_URL = import.meta.env.VITE_INSTALL_SCRIPT_URL || `${API_URL}/install.sh`;
const APP_NAME = import.meta.env.VITE_APP_NAME || 'LogWhispererAI';
const generateWebhook = async () => {
setIsGenerating(true);
try {
const response = await fetch(`${API_URL}/api/webhook`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.success && data.uuid) {
// Use configured webhook base URL instead of API response
setWebhookUrl(`${WEBHOOK_BASE_URL}/${data.uuid}`);
} else {
throw new Error(data.message || 'Errore nella generazione del webhook');
}
} catch (error) {
console.error('Error generating webhook:', error);
// Fallback: generate locally if API fails
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
setWebhookUrl(`${WEBHOOK_BASE_URL}/${uuid}`);
} finally {
setIsGenerating(false);
}
};
const copyWebhook = () => {
navigator.clipboard.writeText(webhookUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const copyCurlCommand = () => {
const command = `curl -fsSL ${INSTALL_SCRIPT_URL} | bash -s -- --webhook ${webhookUrl}`;
navigator.clipboard.writeText(command);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const nextStep = () => {
if (currentStep < 3) {
setCurrentStep(currentStep + 1);
} else {
onComplete?.();
}
};
const prevStep = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};
const getInstallCommand = () => {
return webhookUrl
? `curl -fsSL ${INSTALL_SCRIPT_URL} | bash -s -- --webhook ${webhookUrl}`
: `curl -fsSL ${INSTALL_SCRIPT_URL} | bash`;
};
const steps = [
{ number: 1, title: 'Benvenuto', icon: <Rocket className="w-5 h-5" /> },
{ number: 2, title: 'Webhook', icon: <Settings className="w-5 h-5" /> },
{ number: 3, title: 'Setup', icon: <Terminal className="w-5 h-5" /> },
];
return (
<section
id="onboarding"
className="w-full py-24 lg:py-32 bg-gradient-to-b from-indigo-50 to-white"
aria-labelledby="onboarding-heading"
>
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section Header */}
<div className="text-center mb-12">
<h2
id="onboarding-heading"
className="text-3xl sm:text-4xl font-bold text-slate-900 mb-4"
>
Inizia in 3 semplici passi
</h2>
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
Configura LogWhispererAI sul tuo server in meno di 5 minuti.
</p>
</div>
{/* Step Indicator */}
<nav aria-label="Progresso onboarding" className="mb-12">
<ol className="flex items-center justify-center gap-4">
{steps.map((step, index) => (
<li key={step.number} className="flex items-center">
<div
className={`flex items-center gap-2 px-4 py-2 rounded-full font-medium text-sm transition-colors ${
currentStep === step.number
? 'bg-indigo-600 text-white'
: currentStep > step.number
? 'bg-green-100 text-green-700'
: 'bg-slate-200 text-slate-600'
}`}
aria-current={currentStep === step.number ? 'step' : undefined}
>
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-white/20">
{currentStep > step.number ? (
<Check className="w-4 h-4" />
) : (
step.number
)}
</span>
<span className="hidden sm:inline">{step.title}</span>
</div>
{index < steps.length - 1 && (
<ChevronRight className="w-5 h-5 text-slate-300 mx-2" aria-hidden="true" />
)}
</li>
))}
</ol>
</nav>
{/* Wizard Content */}
<div
ref={stepRef}
tabIndex={-1}
className="bg-white rounded-2xl shadow-xl border border-slate-200 p-8 outline-none"
aria-live="polite"
>
{/* Step 1: Welcome */}
{currentStep === 1 && (
<div className="space-y-6">
<div className="text-center">
<div className="w-20 h-20 bg-indigo-100 rounded-full flex items-center justify-center mx-auto mb-6">
<Rocket className="w-10 h-10 text-indigo-600" aria-hidden="true" />
</div>
<h3 className="text-2xl font-bold text-slate-900 mb-4">
Benvenuto su LogWhispererAI
</h3>
<p className="text-slate-600 mb-6 max-w-lg mx-auto">
Il tuo assistente DevOps personale che monitora i log del server
e ti avvisa immediatamente quando qualcosa va storto, suggerendoti
il comando esatto per risolvere il problema.
</p>
</div>
<div className="bg-slate-50 rounded-xl p-6">
<h4 className="font-semibold text-slate-900 mb-4 flex items-center gap-2">
<AlertCircle className="w-5 h-5 text-indigo-600" />
Cosa succederà:
</h4>
<ul className="space-y-3 text-slate-600">
<li className="flex items-start gap-3">
<span className="text-indigo-600 font-bold">1.</span>
<span>Genereremo un webhook URL univoco per il tuo account</span>
</li>
<li className="flex items-start gap-3">
<span className="text-indigo-600 font-bold">2.</span>
<span>Installerai uno script leggero sul tuo server</span>
</li>
<li className="flex items-start gap-3">
<span className="text-indigo-600 font-bold">3.</span>
<span>Inizierai a ricevere notifiche intelligenti su Telegram</span>
</li>
</ul>
</div>
<div className="text-center text-sm text-slate-500">
<p> Tempo stimato: 5 minuti</p>
<p>💰 Nessuna carta di credito richiesta</p>
</div>
</div>
)}
{/* Step 2: Webhook Generation */}
{currentStep === 2 && (
<div className="space-y-6">
<div className="text-center">
<div className="w-20 h-20 bg-indigo-100 rounded-full flex items-center justify-center mx-auto mb-6">
<Settings className="w-10 h-10 text-indigo-600" aria-hidden="true" />
</div>
<h3 className="text-2xl font-bold text-slate-900 mb-4">
Genera il tuo Webhook
</h3>
<p className="text-slate-600 mb-6 max-w-lg mx-auto">
Clicca il pulsante qui sotto per generare il tuo webhook URL univoco.
Questo URL riceverà i log dal tuo server.
</p>
</div>
{!webhookUrl ? (
<div className="text-center">
<button
onClick={generateWebhook}
disabled={isGenerating}
className="inline-flex items-center gap-2 px-8 py-4 bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-400 text-white font-bold rounded-xl transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
{isGenerating ? (
<>
<RefreshCw className="w-5 h-5 animate-spin" />
Generazione...
</>
) : (
<>
<Rocket className="w-5 h-5" />
Genera Webhook URL
</>
)}
</button>
</div>
) : (
<div className="space-y-4">
<div className="bg-slate-900 rounded-xl p-4">
<div className="flex items-center justify-between mb-2">
<span className="text-slate-400 text-sm">Il tuo webhook URL:</span>
<button
onClick={copyWebhook}
className="flex items-center gap-1 text-indigo-400 hover:text-indigo-300 text-sm"
>
{copied ? (
<><Check className="w-4 h-4" /> Copiato!</>
) : (
<><Copy className="w-4 h-4" /> Copia</>
)}
</button>
</div>
<code className="text-green-400 font-mono text-sm break-all">
{webhookUrl}
</code>
</div>
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<p className="text-green-800 text-sm flex items-center gap-2">
<Check className="w-5 h-5" />
Webhook generato con successo! Procedi allo step successivo per installare lo script.
</p>
</div>
</div>
)}
</div>
)}
{/* Step 3: Setup Instructions */}
{currentStep === 3 && (
<div className="space-y-6">
<div className="text-center">
<div className="w-20 h-20 bg-indigo-100 rounded-full flex items-center justify-center mx-auto mb-6">
<Terminal className="w-10 h-10 text-indigo-600" aria-hidden="true" />
</div>
<h3 className="text-2xl font-bold text-slate-900 mb-4">
Installa sul tuo Server
</h3>
<p className="text-slate-600 mb-6 max-w-lg mx-auto">
Esegui questo comando sul tuo server per installare LogWhisperer.
Lo script configurerà automaticamente il monitoraggio dei log.
</p>
</div>
<div className="bg-slate-900 rounded-xl p-6">
<div className="flex items-center justify-between mb-3">
<span className="text-slate-400 text-sm">Comando di installazione:</span>
<button
onClick={copyCurlCommand}
className="flex items-center gap-1 text-indigo-400 hover:text-indigo-300 text-sm"
>
{copied ? (
<><Check className="w-4 h-4" /> Copiato!</>
) : (
<><Copy className="w-4 h-4" /> Copia comando</>
)}
</button>
</div>
<code className="text-green-400 font-mono text-sm block break-all">
{getInstallCommand()}
</code>
</div>
<div className="space-y-4">
<h4 className="font-semibold text-slate-900">Istruzioni:</h4>
<ol className="space-y-3 text-slate-600">
<li className="flex items-start gap-3">
<span className="w-6 h-6 bg-indigo-100 text-indigo-600 rounded-full flex items-center justify-center text-sm font-bold flex-shrink-0">1</span>
<span>Connettiti al tuo server via SSH</span>
</li>
<li className="flex items-start gap-3">
<span className="w-6 h-6 bg-indigo-100 text-indigo-600 rounded-full flex items-center justify-center text-sm font-bold flex-shrink-0">2</span>
<span>Esegui il comando copiato sopra</span>
</li>
<li className="flex items-start gap-3">
<span className="w-6 h-6 bg-indigo-100 text-indigo-600 rounded-full flex items-center justify-center text-sm font-bold flex-shrink-0">3</span>
<span>Segui la configurazione guidata</span>
</li>
<li className="flex items-start gap-3">
<span className="w-6 h-6 bg-indigo-100 text-indigo-600 rounded-full flex items-center justify-center text-sm font-bold flex-shrink-0">4</span>
<span>Controlla Telegram per il messaggio di conferma</span>
</li>
</ol>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<p className="text-blue-800 text-sm">
<strong>Nota:</strong> Lo script richiede privilegi sudo per installare il servizio di sistema.
Nessun dato sensibile viene inviato durante l'installazione.
</p>
</div>
</div>
)}
{/* Navigation Buttons */}
<div className="flex items-center justify-between mt-8 pt-8 border-t border-slate-200">
<button
onClick={prevStep}
disabled={currentStep === 1}
className="flex items-center gap-2 px-6 py-3 text-slate-600 hover:text-slate-900 disabled:opacity-50 disabled:cursor-not-allowed font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 rounded-lg"
>
<ChevronLeft className="w-5 h-5" />
Indietro
</button>
<button
onClick={nextStep}
disabled={currentStep === 2 && !webhookUrl}
className="flex items-center gap-2 px-8 py-3 bg-indigo-600 hover:bg-indigo-700 disabled:bg-slate-300 text-white font-bold rounded-xl transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
{currentStep === 3 ? (
<>
<Check className="w-5 h-5" />
Completa
</>
) : (
<>
Avanti
<ChevronRight className="w-5 h-5" />
</>
)}
</button>
</div>
</div>
</div>
</section>
);
};
export default OnboardingWizard;