Compare commits

...

14 Commits

Author SHA1 Message Date
lucasacchi
1dc2f658a0 Update README.md
chores
2026-04-07 11:30:19 +02:00
lucasacchi
a3a641595d Update README.md
added link to 'mockupAWS - Backend Profiler & Cost Estimator'
2026-04-07 11:29:35 +02:00
Luca Sacchi Ricciardi
c2c922c1b7 docs: add demo link to README
Add link to live demo at https://logwhispererai.lab.home.lucasacchi.net/
2026-04-06 12:20:14 +02:00
Luca Sacchi Ricciardi
894073644f fix: disable HMR in production to fix WebSocket error behind reverse proxy
- Disable HMR when NODE_ENV=production to prevent WebSocket binding errors
- HMR was trying to bind to external IP:443 which is not available in Docker
- App now works correctly behind HTTPS reverse proxy
2026-04-03 18:55:21 +02:00
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
Luca Sacchi Ricciardi
92217897ca fix: standardize project name to LogWhispererAI (no space)
- Replace all occurrences of 'LogWhisperer AI' with 'LogWhispererAI'
- Fix 47 instances across 30 files including:
  - Documentation (README, PRD, specs, docs)
  - Frontend components (Footer, Navbar, Hero, etc.)
  - Backend files (Dockerfile, server.js)
  - Workflow files (n8n, bash scripts)
  - Configuration files (AGENTS.md, LICENSE)

Ensures consistent branding across the entire codebase.
2026-04-03 17:07:35 +02:00
Luca Sacchi Ricciardi
8eb7dfb00e 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
2026-04-03 16:57:14 +02:00
Luca Sacchi Ricciardi
eb24b86308 feat: add Footer and OnboardingWizard components - Sprint 3 Complete
Complete Sprint 3 Landing Page development with final 20%:

Footer Component:
- 4-column layout: Brand, Useful Links, Legal, Newsletter
- SVG social icons (GitHub, Twitter, LinkedIn, YouTube)
- Working newsletter form with success feedback
- Dark theme design (slate-900) with proper contrast
- Copyright and attribution footer

OnboardingWizard Component:
- 3-step wizard with progress indicator
- Step 1: Welcome with service explanation
- Step 2: Webhook generation (crypto.randomUUID)
- Step 3: Setup instructions with curl command
- UUID generation and clipboard copy functionality
- Mock implementation (no backend API calls)

Accessibility Features:
- aria-live=polite on wizard container
- aria-current=step for progress indication
- Focus management with useRef/useEffect
- Keyboard navigation support
- Proper ARIA labels on interactive elements

Integration:
- Added to components/index.ts exports
- Integrated in App.tsx after InteractiveDemo
- CTAs scroll to #onboarding section
- Footer placed at page bottom

Build Verification:
- TypeScript compilation: ✓ 0 errors
- CSS bundle: 34KB (gzipped 7KB)
- JS bundle: 238KB (gzipped 72KB)

Sprint 3 Status: 100% Complete 
Landing Page now includes:
- Hero, Problem/Solution, HowItWorks, Demo, Onboarding, Footer

Refs: docs/frontend_landing_plan.md
2026-04-03 16:43:23 +02:00
Luca Sacchi Ricciardi
462a5a9383 docs: update README with real project status
Reflect actual project progress in README:
- Sprint 2: COMPLETED (AI Pipeline + Telegram alerting)
- Sprint 3: IN PROGRESS (~80% - Landing Page development)
- Updated MVP features status with file references
- Changed sprint badge from 1 to 2 completed

Previous README showed Sprint 2/3 as 'in planning' when they
were actually completed/in progress. Now accurately reflects:
- Log Ingestion: DONE (scripts/)
- AI Processing: DONE (workflows/ with OpenRouter)
- Telegram Alerting: DONE (docs/telegram_setup.md)
- Landing Page: IN PROGRESS (frontend/ 80% complete)
2026-04-03 16:34:09 +02:00
Luca Sacchi Ricciardi
c131343321 docs: clarify Interactive Demo uses mock data, not real OpenRouter
Update documentation to reflect demo simulation status:

README.md: Add note explaining demo uses static mock data
CHANGELOG.md: Add Interactive Demo entry marked as Mock
roadmap_ideas.md: Update status to in-evaluation with priority note

Prevents user confusion about AI capabilities in demo section.

Refs: Sprint 3, demo clarification
2026-04-03 16:30:10 +02:00
Luca Sacchi Ricciardi
f3e65ee36f docs: add roadmap ideas document for future development
Create comprehensive living document to track improvement suggestions
and potential new features for LogWhisperer AI roadmap.

Structure includes:
- Status legend (💡🤔📅🚧)
- Categorized by priority and area:
  * Core Features (Backend, AI)
  * UX/UI & Frontend
  * Security & Compliance
  * Integrations (Slack, Discord, etc.)
  * Monitoring & Analytics
  * Developer Experience
  * Internationalization
  * Monetization
- Completed sprints tracking
- Rejected ideas section with rationale
- Notes on performance, costs, scalability
- Contribution guidelines

Serves as central hub for team to propose, evaluate,
and track feature ideas before they enter formal roadmap.

Refs: Sprint 3 planning, team brainstorming
2026-04-03 16:26:45 +02:00
Luca Sacchi Ricciardi
9eb1e0ddf5 fix: implement scroll to demo section on 'Guarda la Demo' button click
The 'Guarda la Demo' (Watch Demo) button in Hero section was not working.
Now it smoothly scrolls to the InteractiveDemo section.

Changes:
- Added id='demo-interattiva' to InteractiveDemo section
- Implemented handleSecondaryCta with document.getElementById
- Used scrollIntoView with smooth behavior for better UX

Refs: User feedback - button was not functional
2026-04-03 16:22:32 +02:00
Luca Sacchi Ricciardi
04db2b30be feat: add InteractiveDemo section with mock AI analysis
Create interactive demo component showcasing AI log analysis:

Layout:
- Two-column responsive grid (side-by-side desktop, stacked mobile)
- Left panel: Terminal-style input with log selection
- Right panel: AI analysis output with results

Terminal Panel (Left):
- Dark terminal design (bg-slate-900) with header
- 3 preset log buttons: PostgreSQL OOM, Nginx 502, Node.js Exception
- Terminal content area with monospace font
- Blinking cursor animation
- Log content display with syntax highlighting

Analysis Panel (Right):
- Initial state with guidance message
- Loading animation: 'Analisi in corso...' with 1.5s delay
- Result display with title, description, command
- Command box with copy button and feedback
- Safety badge and additional notes

React Logic:
- useState for selection, loading, analysis states
- Simulated 1.5s delay with setTimeout
- Static mock data (no API calls)
- Copy to clipboard functionality with visual feedback

Accessibility:
- aria-live=polite on output panel for screen readers
- aria-atomic=true for complete announcements
- aria-pressed on selection buttons
- aria-label on interactive elements
- Icons hidden from screen readers

TypeScript:
- LogAnalysis interface defined
- Proper type safety throughout
- Build passes without errors

Integration:
- Added to components/index.ts exports
- Integrated in App.tsx after HowItWorks section

Refs: Giorno 4 - Sprint 3 Landing Page development
2026-04-03 16:19:23 +02:00
Luca Sacchi Ricciardi
644622f21c fix: remove conflicting CSS legacy styles causing white-on-white text
Critical bug fix (P0): Text appearing white-on-white and unreadable.

Root Cause:
- CSS legacy styles in index.css were setting global colors via :root variables
- color-scheme: light dark was interfering with Tailwind classes
- Global color: var(--text) and background: var(--bg) were overriding Tailwind
- Dark mode media query was applying even when Tailwind expected light mode
- Variables like --text-h were referenced but not properly initialized

Fix:
- Removed all legacy CSS variable definitions
- Removed color-scheme declaration that interfered with Tailwind
- Removed global color/background declarations
- Simplified index.css to only import Tailwind and set minimal base styles
- Let Tailwind utility classes handle all colors completely

Result:
- Text now renders with correct Tailwind colors (text-slate-900, text-indigo-700, etc.)
- Badge with 'Sprint 2 Completato' now visible with indigo background
- Headline 'Il DevOps tascabile...' now renders in dark slate
- Build successful: 28KB CSS bundle with all utilities

Safety First:
- Verified build passes
- No breaking changes to component structure
- Tailwind classes now have full control over styling
- Minimal base styles preserved for font-smoothing

Refs: Tailwind v4 CSS-first configuration
2026-04-03 16:12:07 +02:00
45 changed files with 3262 additions and 169 deletions

41
.env Normal file
View File

@@ -0,0 +1,41 @@
# LogWhispererAI - Production Environment Configuration
# Generated for deployment with reverse proxy
# Date: 2026-04-03
# ============================================
# FRONTEND CONFIGURATION (VITE_* variables)
# These are exposed to the browser
# ============================================
# Backend API URL - HTTPS endpoint via reverse proxy
VITE_API_URL=https://srv-logwhispererai.lab.home.lucasacchi.net
# Webhook base URL - public HTTPS endpoint for webhooks
VITE_WEBHOOK_BASE_URL=https://logwhispererai.lab.home.lucasacchi.net/webhook
# Install script URL - public HTTPS endpoint for install script
VITE_INSTALL_SCRIPT_URL=https://logwhispererai.lab.home.lucasacchi.net/install.sh
# Application identification
VITE_APP_NAME=LogWhispererAI
# Application public URL - main frontend domain
VITE_APP_URL=https://logwhispererai.lab.home.lucasacchi.net
# ============================================
# BACKEND CONFIGURATION
# ============================================
# CORS Origins - restrict to frontend domain for security
# Only requests from this origin will be accepted by the backend
CORS_ORIGINS=https://logwhispererai.lab.home.lucasacchi.net
# Webhook base URL - used for generating webhook URLs
# This should match VITE_WEBHOOK_BASE_URL
WEBHOOK_BASE_URL=https://logwhispererai.lab.home.lucasacchi.net/webhook
# API delay simulation (milliseconds) - simulates AI processing time
DELAY_MS=1500
# Node environment
NODE_ENV=production

View File

@@ -1,22 +1,64 @@
# LogWhispererAI - Environment Variables
# ============================================
# FRONTEND CONFIGURATION (VITE_* variables)
# These are exposed to the browser
# ============================================
# Backend API URL - where the frontend will make API calls
# Example: https://srv-logwhispererai.lab.home.lucasacchi.net
VITE_API_URL=http://localhost:3001
# Webhook base URL - used for displaying webhook URLs to users
# Example: https://logwhispererai.lab.home.lucasacchi.net/webhook
VITE_WEBHOOK_BASE_URL=http://localhost:3001/webhook
# Install script URL - the curl command shown to users
# Example: https://logwhispererai.lab.home.lucasacchi.net/install.sh
VITE_INSTALL_SCRIPT_URL=http://localhost:3001/install.sh
# Application identification
VITE_APP_NAME=LogWhispererAI
VITE_APP_URL=http://localhost:5173
# ============================================
# BACKEND CONFIGURATION
# ============================================
# CORS Origins - comma-separated list of allowed frontend origins
# Use '*' for development, set specific domains for production
# Example: https://logwhispererai.lab.home.lucasacchi.net
CORS_ORIGINS=*
# Webhook base URL - used for generating webhook URLs
# Example: https://logwhispererai.lab.home.lucasacchi.net/webhook
WEBHOOK_BASE_URL=https://logwhisperer.ai/webhook
# API delay simulation (milliseconds)
DELAY_MS=1500
# Node environment
NODE_ENV=development
# ============================================
# OPTIONAL: Third-party Services
# ============================================
# Telegram Bot Configuration
# Ottieni questi valori seguendo le istruzioni in docs/telegram_setup.md
TELEGRAM_BOT_TOKEN=your_bot_token_here
TELEGRAM_CHAT_ID=your_chat_id_here
# TELEGRAM_BOT_TOKEN=your_bot_token_here
# TELEGRAM_CHAT_ID=your_chat_id_here
# n8n Configuration
N8N_WEBHOOK_URL=https://your-n8n-instance.com/webhook/logwhisperer
# N8N_WEBHOOK_URL=https://your-n8n-instance.com/webhook/logwhisperer
# AI Provider Configuration
OPENAI_API_KEY=your_openai_api_key_here
# oppure
ANTHROPIC_API_KEY=your_anthropic_api_key_here
# OPENAI_API_KEY=your_openai_api_key_here
# ANTHROPIC_API_KEY=your_anthropic_api_key_here
# Supabase Configuration (per autenticazione e database)
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your_anon_key_here
# SUPABASE_URL=https://your-project.supabase.co
# SUPABASE_ANON_KEY=your_anon_key_here
# Stripe Configuration (per pagamenti)
STRIPE_SECRET_KEY=sk_test_your_key_here
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret_here
# STRIPE_SECRET_KEY=sk_test_your_key_here
# STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret_here

View File

@@ -17,6 +17,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add Tailwind directives to src/index.css
- Ready for component development and styling
- feat: Add Interactive Demo section to Landing Page (Mock)
- Two-column layout: Terminal input + AI analysis output
- 3 preset log examples (PostgreSQL OOM, Nginx 502, Node.js Exception)
- Simulated 1.5s AI analysis delay with loading animation
- Copy-to-clipboard functionality for suggested commands
- Fully accessible with aria-live and screen reader support
- Note: Uses static mock data, NOT real OpenRouter API calls
- Real AI integration planned for future (see docs/roadmap_ideas.md)
- feat: Create n8n workflow `LogWhisperer_Ingest` for secure log ingestion
- Webhook trigger on POST `/webhook/logwhisperer/ingest`
- HMAC-SHA256 signature validation with anti-replay protection

View File

@@ -2,9 +2,13 @@
> **UVP:** Il DevOps tascabile che traduce i crash del tuo server e ti dice l'esatto comando per risolverli in sicurezza, senza farti perdere ore su StackOverflow.
🌐 **Demo Online:** [https://logwhispererai.lab.home.lucasacchi.net/](https://logwhispererai.lab.home.lucasacchi.net/)
**mockupAWS - Backend Profiler & Cost Estimator** [https://gitea.lab.home.lucasacchi.net/lucasacchi/mockupAWS](https://gitea.lab.home.lucasacchi.net/lucasacchi/mockupAWS)
[![Tests](https://img.shields.io/badge/tests-12%2F12%20passing-brightgreen)]()
[![Version](https://img.shields.io/badge/version-0.1.0-blue)]()
[![Sprint](https://img.shields.io/badge/sprint-1%20completed-success)]()
[![Sprint](https://img.shields.io/badge/sprint-2%20completed-success)]()
[![Status](https://img.shields.io/badge/status-active%20development-orange)]()
@@ -17,17 +21,19 @@ LogWhisperer AI trasforma i log di sistema e database spesso incomprensibili in
| Feature | Stato | Sprint |
|---------|-------|--------|
| ✅ Log Ingestion Script | **Completato** | Sprint 1 |
| 🔄 AI Processing Pipeline | *In pianificazione* | Sprint 2 |
| 🔄 Alerting (Telegram/Slack) | *In pianificazione* | Sprint 2-3 |
| 🔄 Landing Page & Onboarding | *In pianificazione* | Sprint 3 |
| AI Processing Pipeline | **Completato** | Sprint 2 |
| Alerting (Telegram) | **Completato** | Sprint 2 |
| 🚧 Landing Page & Onboarding | *In corso (~80%)* | Sprint 3 |
### Funzionalità MVP
- **Log Ingestion** ✅: Script Bash leggero per il monitoraggio (tail -f) di log critici (syslog, nginx, postgres). Invia payload JSON via HTTP POST a webhook.
- **Log Ingestion** ✅: Script Bash leggero per il monitoraggio (tail -f) di log critici (syslog, nginx, postgres). Invia payload JSON via HTTP POST a webhook. Script disponibili in `scripts/`.
- **AI Processing Pipeline** 🔄: Workflow su n8n che analizza i log tramite LLM (OpenAI/Anthropic) applicando il Metodo Sacchi (Safety first, little often, double check).
- **AI Processing Pipeline** : Workflow n8n completo che riceve log via webhook, valida HMAC, analizza con OpenRouter (GPT-4o-mini) e restituisce JSON con analisi e comando risolutivo. Workflow in `workflows/logwhisperer_ingest.json`.
- **Alerting Umano** 🔄: Notifiche su Telegram/Slack con sintesi del problema, severità e comando esatto per la mitigazione.
- **Alerting Umano** : Notifiche su Telegram con sintesi del problema, severità e comando esatto. Configurazione in `docs/telegram_setup.md`.
- **Landing Page** 🚧: Interfaccia web React + Vite + Tailwind CSS con Hero, Problem/Solution, How It Works, e Demo Interattiva. In sviluppo in `frontend/`.
🛠️ Stack Tecnologico
Core Logic: Python 3.12+ (in venv)
@@ -141,6 +147,9 @@ npm run preview
La landing page sarà disponibile su `http://localhost:5173` durante lo sviluppo.
**Nota sulla Demo Interattiva:**
La sezione "Demo Interattiva" nella landing page utilizza **dati mock statici** per scopi dimostrativi. Non effettua chiamate API reali a OpenRouter o al backend. I log di esempio e le relative analisi sono predefiniti nel codice frontend. Per una demo funzionante con AI reale, è necessario implementare un backend sicuro (vedi `docs/roadmap_ideas.md`).
#### Sviluppo con Docker (Consigliato)
Per un ambiente di sviluppo isolato e consistente, utilizza Docker Compose:
@@ -232,6 +241,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
@@ -251,6 +265,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.

View File

@@ -0,0 +1,23 @@
# Docker Compose Override - Local Development Configuration
# This file is automatically loaded by Docker Compose and overrides docker-compose.yml
# Use this for local development settings without modifying the main docker-compose.yml
#
# Usage: docker compose up -d (automatically loads this file)
#
# To use production configuration, create a .env file and run:
# docker compose -f docker-compose.yml up -d
services:
frontend:
environment:
# Development defaults - override these in .env file for production
- VITE_API_URL=http://192.168.254.79:3001
- VITE_WEBHOOK_BASE_URL=http://192.168.254.79:3001/webhook
- VITE_INSTALL_SCRIPT_URL=http://192.168.254.79:3001/install.sh
- VITE_APP_URL=http://192.168.254.79:5173
fake-backend:
environment:
# Development defaults
- CORS_ORIGINS=*
- WEBHOOK_BASE_URL=http://192.168.254.79:3001/webhook

31
docker-compose.prod.yml Normal file
View File

@@ -0,0 +1,31 @@
# Docker Compose - Production Configuration
# Usage with reverse proxy:
# docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
#
# Required: Create a .env file with production values before running
services:
frontend:
# In production, don't mount source volumes (use built image)
volumes:
- node_modules:/app/node_modules
environment:
# Production API URLs - these must be set in .env file
- VITE_API_URL=${VITE_API_URL}
- VITE_WEBHOOK_BASE_URL=${VITE_WEBHOOK_BASE_URL}
- VITE_INSTALL_SCRIPT_URL=${VITE_INSTALL_SCRIPT_URL}
- VITE_APP_URL=${VITE_APP_URL}
- NODE_ENV=production
fake-backend:
environment:
# Production CORS - restrict to frontend domain
- CORS_ORIGINS=${CORS_ORIGINS}
# Production webhook URL
- WEBHOOK_BASE_URL=${WEBHOOK_BASE_URL}
# Production settings
- NODE_ENV=production
- DELAY_MS=${DELAY_MS:-1500}

View File

@@ -1,6 +1,19 @@
# Docker Compose - LogWhispererAI Development Environment
# Usage: docker compose up -d
# Access: http://localhost:5173
# Access Frontend: http://localhost:5173
# Access Fake Backend API: http://localhost:3001
#
# For production deployment with reverse proxy:
# 1. Copy .env.example to .env and customize values
# 2. Run: docker compose up -d
#
# Required environment variables for production:
# VITE_API_URL=https://srv-logwhispererai.lab.home.lucasacchi.net
# VITE_WEBHOOK_BASE_URL=https://logwhispererai.lab.home.lucasacchi.net/webhook
# VITE_INSTALL_SCRIPT_URL=https://logwhispererai.lab.home.lucasacchi.net/install.sh
# VITE_APP_URL=https://logwhispererai.lab.home.lucasacchi.net
# CORS_ORIGINS=https://logwhispererai.lab.home.lucasacchi.net
# WEBHOOK_BASE_URL=https://logwhispererai.lab.home.lucasacchi.net/webhook
services:
frontend:
@@ -16,10 +29,42 @@ services:
# Use named volume for node_modules to avoid conflicts with host
- node_modules:/app/node_modules
environment:
- NODE_ENV=development
# Node environment
- NODE_ENV=${NODE_ENV:-development}
- CHOKIDAR_USEPOLLING=true
# ============================================
# FRONTEND CONFIGURATION (VITE_* variables)
# These are exposed to the browser
# ============================================
# Backend API URL - where the frontend will make API calls
# Default: http://fake-backend:3000 (internal Docker network)
# Production: https://srv-logwhispererai.lab.home.lucasacchi.net
- VITE_API_URL=${VITE_API_URL:-http://fake-backend:3000}
# Webhook base URL - used for displaying webhook URLs to users
# Default: http://localhost:3001/webhook
# Production: https://logwhispererai.lab.home.lucasacchi.net/webhook
- VITE_WEBHOOK_BASE_URL=${VITE_WEBHOOK_BASE_URL:-http://localhost:3001/webhook}
# Install script URL - the curl command shown to users
# Default: http://localhost:3001/install.sh
# Production: https://logwhispererai.lab.home.lucasacchi.net/install.sh
- VITE_INSTALL_SCRIPT_URL=${VITE_INSTALL_SCRIPT_URL:-http://localhost:3001/install.sh}
# Application identification
- VITE_APP_NAME=${VITE_APP_NAME:-LogWhispererAI}
# Application public URL
# Default: http://localhost:5173
# Production: https://logwhispererai.lab.home.lucasacchi.net
- VITE_APP_URL=${VITE_APP_URL:-http://localhost:5173}
# 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 +73,47 @@ services:
retries: 3
start_period: 40s
fake-backend:
build:
context: ./tools/fake-backend
dockerfile: Dockerfile
container_name: logwhisperer-fake-backend
ports:
- "3001:3000"
environment:
# Server port (internal)
- PORT=3000
# API delay simulation (milliseconds)
- DELAY_MS=${DELAY_MS:-1500}
# Node environment
- NODE_ENV=${NODE_ENV:-production}
# ============================================
# BACKEND CONFIGURATION
# ============================================
# CORS origins - comma-separated list of allowed frontend origins
# Use '*' for development (allows all origins)
# Production: set to your frontend domain for security
# Example: https://logwhispererai.lab.home.lucasacchi.net
- CORS_ORIGINS=${CORS_ORIGINS:-*}
# Webhook base URL - used for generating webhook URLs
# This should be the public URL that users see
# Default: https://logwhisperer.ai/webhook
# Production: https://logwhispererai.lab.home.lucasacchi.net/webhook
- WEBHOOK_BASE_URL=${WEBHOOK_BASE_URL:-https://logwhisperer.ai/webhook}
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

131
docs/docker-compose.md Normal file
View File

@@ -0,0 +1,131 @@
# Docker Compose Configuration
Questa directory contiene diverse configurazioni Docker Compose per vari ambienti.
## File Disponibili
| File | Scopo | Uso |
|------|-------|-----|
| `docker-compose.yml` | Configurazione base con variabili d'ambiente | Sviluppo e produzione |
| `docker-compose.override.yml` | Override per sviluppo locale | Caricato automaticamente |
| `docker-compose.prod.yml` | Configurazione produzione | Da usare con `-f` |
## Variabili d'Ambiente
Tutte le variabili sono configurabili tramite file `.env` o variabili di sistema.
### Frontend (VITE_*)
| Variabile | Default | Descrizione |
|-----------|---------|-------------|
| `VITE_API_URL` | `http://fake-backend:3000` | URL del backend API |
| `VITE_WEBHOOK_BASE_URL` | `http://localhost:3001/webhook` | Base URL webhook |
| `VITE_INSTALL_SCRIPT_URL` | `http://localhost:3001/install.sh` | URL script installazione |
| `VITE_APP_NAME` | `LogWhispererAI` | Nome applicazione |
| `VITE_APP_URL` | `http://localhost:5173` | URL pubblico app |
### Backend
| Variabile | Default | Descrizione |
|-----------|---------|-------------|
| `PORT` | `3000` | Porta server (interna) |
| `DELAY_MS` | `1500` | Delay simulazione API |
| `NODE_ENV` | `production` | Ambiente Node |
| `CORS_ORIGINS` | `*` | Origini CORS consentite |
| `WEBHOOK_BASE_URL` | `https://logwhisperer.ai/webhook` | Base URL webhook |
## Utilizzo
### Sviluppo Locale (default)
```bash
# Le variabili di docker-compose.override.yml vengono usate automaticamente
docker compose up -d
```
### Sviluppo con configurazione personalizzata
```bash
# Crea un file .env nella root del progetto
cat > .env << 'EOF'
VITE_API_URL=http://192.168.254.79:3001
VITE_WEBHOOK_BASE_URL=http://192.168.254.79:3001/webhook
CORS_ORIGINS=*
EOF
# Avvia con le variabili dal file .env
docker compose up -d
```
### Produzione con Reverse Proxy
```bash
# 1. Crea il file .env con i valori di produzione
cat > .env << 'EOF'
VITE_API_URL=https://srv-logwhispererai.lab.home.lucasacchi.net
VITE_WEBHOOK_BASE_URL=https://logwhispererai.lab.home.lucasacchi.net/webhook
VITE_INSTALL_SCRIPT_URL=https://logwhispererai.lab.home.lucasacchi.net/install.sh
VITE_APP_URL=https://logwhispererai.lab.home.lucasacchi.net
CORS_ORIGINS=https://logwhispererai.lab.home.lucasacchi.net
WEBHOOK_BASE_URL=https://logwhispererai.lab.home.lucasacchi.net/webhook
NODE_ENV=production
EOF
# 2. Avvia con la configurazione di produzione
# (ignora docker-compose.override.yml)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
```
### Solo Backend
```bash
docker compose up fake-backend -d
```
### Solo Frontend
```bash
docker compose up frontend -d
```
## Verifica Configurazione
```bash
# Controlla le variabili caricate
docker compose config
# Verifica il backend
curl https://srv-logwhispererai.lab.home.lucasacchi.net/health
# Verifica la generazione webhook
curl -X POST https://srv-logwhispererai.lab.home.lucasacchi.net/api/webhook
```
## Troubleshooting
### Cambiare le variabili
Se modifichi il file `.env`, devi ricreare i container:
```bash
docker compose down
docker compose up -d
```
### Vedere le variabili in uso
```bash
# Frontend
docker exec logwhisperer-frontend-dev env | grep VITE
# Backend
docker exec logwhisperer-fake-backend env | grep -E '(CORS|WEBHOOK)'
```
### Reset alla configurazione default
```bash
docker compose down
rm .env # Rimuovi configurazione personalizzata
docker compose up -d # Userà i valori default
```

129
docs/reverse_proxy_setup.md Normal file
View File

@@ -0,0 +1,129 @@
# Configurazione con Reverse Proxy
Questa guida spiega come configurare LogWhispererAI con un reverse proxy SSL.
## Scenario
Hai configurato:
- **Frontend**: `https://logwhispererai.lab.home.lucasacchi.net``http://192.168.254.79:5173`
- **Backend**: `https://srv-logwhispererai.lab.home.lucasacchi.net``http://192.168.254.79:3001`
## Configurazione
### 1. Crea il file di configurazione
Crea un file `.env` nella root del progetto:
```bash
cp .env.example .env
```
Modifica il file `.env` con i tuoi valori:
```env
# Frontend Configuration
VITE_API_URL=https://srv-logwhispererai.lab.home.lucasacchi.net
VITE_WEBHOOK_BASE_URL=https://logwhispererai.lab.home.lucasacchi.net/webhook
VITE_INSTALL_SCRIPT_URL=https://logwhispererai.lab.home.lucasacchi.net/install.sh
VITE_APP_NAME=LogWhispererAI
VITE_APP_URL=https://logwhispererai.lab.home.lucasacchi.net
# Backend Configuration
CORS_ORIGINS=https://logwhispererai.lab.home.lucasacchi.net
WEBHOOK_BASE_URL=https://logwhispererai.lab.home.lucasacchi.net/webhook
DELAY_MS=1500
NODE_ENV=production
```
### 2. Avvia i servizi
```bash
# Ferma eventuali container esistenti
docker compose down
# Avvia con le nuove variabili
docker compose up -d
```
### 3. Verifica la configurazione
Testa i vari endpoint:
```bash
# Health check backend
curl https://srv-logwhispererai.lab.home.lucasacchi.net/health
# Genera un webhook
curl -X POST https://srv-logwhispererai.lab.home.lucasacchi.net/api/webhook
# Analizza un log
curl -X POST https://srv-logwhispererai.lab.home.lucasacchi.net/api/analyze \
-H "Content-Type: application/json" \
-d '{"log": "FATAL: out of memory"}'
```
### 4. Verifica il frontend
Apri `https://logwhispererai.lab.home.lucasacchi.net` nel browser e verifica che:
1. La demo interattiva funzioni (chiama il backend corretto)
2. Lo step 2 dell'onboarding generi webhook con l'URL corretto
## Variabili d'Ambiente
### Frontend (VITE_*)
| Variabile | Descrizione | Esempio |
|-----------|-------------|---------|
| `VITE_API_URL` | URL del backend API | `https://srv-logwhispererai.lab.home.lucasacchi.net` |
| `VITE_WEBHOOK_BASE_URL` | Base URL per i webhook | `https://logwhispererai.lab.home.lucasacchi.net/webhook` |
| `VITE_INSTALL_SCRIPT_URL` | URL dello script di installazione | `https://logwhispererai.lab.home.lucasacchi.net/install.sh` |
| `VITE_APP_NAME` | Nome dell'applicazione | `LogWhispererAI` |
| `VITE_APP_URL` | URL pubblico dell'app | `https://logwhispererai.lab.home.lucasacchi.net` |
### Backend
| Variabile | Descrizione | Esempio |
|-----------|-------------|---------|
| `CORS_ORIGINS` | Origini CORS consentite (comma-separated) | `https://logwhispererai.lab.home.lucasacchi.net` |
| `WEBHOOK_BASE_URL` | Base URL per i webhook generati | `https://logwhispererai.lab.home.lucasacchi.net/webhook` |
| `DELAY_MS` | Delay simulato API (ms) | `1500` |
| `NODE_ENV` | Ambiente Node | `production` |
## File Configurati
- `frontend/.env` - Configurazione frontend per produzione
- `frontend/.env.development` - Configurazione per sviluppo locale
- `frontend/vite.config.ts` - Allow hosts per Vite
- `tools/fake-backend/server.js` - Supporto CORS dinamico e webhook URL configurabili
- `docker-compose.yml` - Passaggio variabili ai container
## Troubleshooting
### Errore "Blocked request" di Vite
Se vedi questo errore:
```
Blocked request. This host ("logwhispererai.lab.home.lucasacchi.net") is not allowed.
```
Aggiungi il dominio a `frontend/vite.config.ts`:
```typescript
server: {
allowedHosts: [
'logwhispererai.lab.home.lucasacchi.net',
],
}
```
### Errore CORS
Se il browser blocca le richieste API:
1. Verifica che `CORS_ORIGINS` includa il dominio del frontend
2. Ricostruisci il backend: `docker compose up fake-backend --build -d`
### Webhook URL errati
Se i webhook generati hanno URL sbagliati:
1. Verifica `WEBHOOK_BASE_URL` nel backend
2. Verifica `VITE_WEBHOOK_BASE_URL` nel frontend
3. Ricostruisci entrambi i servizi

292
docs/roadmap_ideas.md Normal file
View File

@@ -0,0 +1,292 @@
# Roadmap & Suggerimenti di Sviluppo
> **Documento Living** - Questo file raccoglie idee, suggerimenti e potenziali nuove funzionalità per LogWhispererAI.
>
> Ultimo aggiornamento: 2026-04-03
---
## 📋 Come Usare Questo Documento
- Aggiungi nuove idee in fondo alla sezione appropriata
- Usa le emoji di stato per indicare lo stato dell'idea
- Discuti le idee prioritarie nel team prima di implementarle
- Sposta le idee completate nella sezione "Completate"
**Legenda Stati:**
- 💡 = Idea proposta
- 🤔 = In valutazione
- 📅 = Pianificata per prossimo sprint
- 🚧 = In sviluppo
- ✅ = Completata
- ❌ = Scartata (con motivazione)
---
## 🎯 Funzionalità Core (Priorità Alta)
### Backend & API
- [ ] 🤔 **Integrazione OpenRouter reale nella demo**
- Attualmente la demo usa dati mock (implementato in Sprint 3)
- Implementare chiamata API a backend sicuro
- Gestire rate limiting e errori gracefully
- **Nota:** Richiede mini-backend per non esporre API key nel frontend
- **Priorità:** Media (demo mock è sufficiente per MVP)
- [ ] 💡 **Dashboard utente con storico log**
- Pagina per visualizzare tutti i log ricevuti
- Filtri per data, severità, tipo
- Esportazione CSV/JSON
- [ ] 💡 **Webhook personalizzabili**
- Possibilità di configurare URL webhook diversi per clienti
- Supporto per autenticazione (Bearer token, HMAC)
- Retry automatico in caso di fallimento
### AI & Analisi
- [ ] 💡 **Supporto multi-modello**
- Scegliere tra GPT-4o-mini, Claude 3.5 Sonnet, ecc.
- Fallback automatico se un modello non risponde
- Confronto prestazioni tra modelli
- [ ] 💡 **Training personalizzato**
- Apprendimento dai feedback utente (👍/👎 sulle risposte)
- Adattamento ai pattern specifici dell'infrastruttura utente
- Suggerimenti migliorati nel tempo
- [ ] 💡 **Analisi predittiva**
- Rilevare pattern prima che diventino errori critici
- Alert preventivi basati su trend
- Metriche di health del sistema
---
## 🎨 UX/UI & Frontend
### Landing Page
- [ ] 💡 **Sezione Testimonianze**
- Carousel con feedback utenti reali
- Case study dettagliati
- Metriche di successo (tempo risparmiato, ecc.)
- [ ] 💡 **Pricing Page**
- Tabella comparativa piani (Free, Pro, Enterprise)
- Calcolatore costi basato su volume log
- FAQ specifiche sul pricing
- [ ] 💡 **Blog/Documentazione integrata**
- Articoli su best practices DevOps
- Guide troubleshooting comuni
- Changelog pubblico
### Dashboard
- [ ] 💡 **Dark mode completa**
- Toggle tema chiaro/scuro
- Persistenza preferenza utente
- Design system coerente
- [ ] 💡 **Notifiche real-time**
- WebSocket per alert istantanei
- Suoni opzionali per errori critici
- Browser notifications
- [ ] 💡 **Mobile app (PWA)**
- Installabile su smartphone
- Push notification nativi
- Interfaccia ottimizzata mobile
---
## 🔒 Sicurezza & Compliance
- [ ] 💡 **Crittografia end-to-end**
- Log criptati in transito e a riposo
- Zero-knowledge architecture
- Certificazioni compliance (GDPR, SOC2)
- [ ] 💡 **RBAC (Role-Based Access Control)**
- Ruoli diversi (Admin, Viewer, Sviluppatore)
- Permessi granulari
- Audit log delle azioni
- [ ] 💡 **Data retention policies**
- Configurazione automatica retention log
- Archiviazione cold storage
- Cancellazione programmatica
---
## 🔌 Integrazioni
### Canali di Notifica
- [ ] 💡 **Slack**
- Bot dedicato per canali
- Thread per discussioni
- Comandi slash (/logwhisperer)
- [ ] 💡 **Discord**
- Bot con embed rich
- Supporto webhook
- Ruoli e permessi
- [ ] 💡 **Email**
- Report giornalieri/settimanali
- Alert digest
- Template personalizzabili
- [ ] 💡 **PagerDuty/Opsgenie**
- Integrazione incident management
- Escalation automatica
- On-call scheduling
### Log Sources
- [ ] 💡 **Agent Docker**
- Container ufficiale LogWhisperer
- Sidecar per Kubernetes
- Configurazione via env vars
- [ ] 💡 **Cloud providers nativi**
- AWS CloudWatch Logs
- Google Cloud Logging
- Azure Monitor Logs
- DigitalOcean Spaces
- [ ] 💡 **Log aggregators**
- Fluentd plugin
- Logstash input
- Vector sink
---
## 📊 Monitoring & Analytics
- [ ] 💡 **Metrics dashboard**
- Volume log processati
- Tempo medio risposta AI
- Tasso di errori risolti
- Costi API
- [ ] 💡 **Health checks**
- Stato sistema in tempo reale
- Latenza webhook
- Disponibilità servizi
- [ ] 💡 **Alerting sullo stato del servizio**
- Notifica se LogWhisperer stesso ha problemi
- Monitoraggio self-hosted
---
## 🛠️ Developer Experience
- [ ] 💡 **CLI Tool**
- `logwhisperer init` - Setup rapido
- `logwhisperer logs` - Visualizza log in tempo reale
- `logwhisperer config` - Gestione configurazione
- [ ] 💡 **API pubblica documentata**
- OpenAPI/Swagger spec
- SDK ufficiali (Python, Node.js, Go)
- Rate limiting chiaro
- [ ] 💡 **Plugin system**
- Estensioni personalizzate
- Custom parsers per log
- Actions custom post-analisi
---
## 🌍 Internazionalizzazione
- [ ] 💡 **Multi-lingua**
- Supporto italiano (già presente)
- English
- Spanish
- French
- German
- [ ] 💡 **Timezone-aware**
- Visualizzazione log in timezone utente
- Scheduling basato su timezone
---
## 💰 Monetizzazione
- [ ] 💡 **Marketplace di comandi**
- Comunità condivide comandi utili
- Rating e recensioni
- Premium commands
- [ ] 💡 **White-label solution**
- Rebranding per MSP/agenzie
- API dedicated
- Supporto prioritario
---
## ✅ Completate
### Sprint 1 (Completato)
- ✅ Script bash log ingestion
- ✅ Pattern matching errori
- ✅ Payload JSON via webhook
### Sprint 2 (Completato)
- ✅ Workflow n8n con HMAC
- ✅ Integrazione OpenRouter
- ✅ Notifiche Telegram
### Sprint 3 (In Progress)
- ✅ Landing page base
- ✅ Demo interattiva mock
- ✅ Docker dev environment
---
## ❌ Scartate
_Nessuna idea scartata al momento. Documentare qui le idee scartate con motivazione per referenza futura._
---
## 📝 Note & Considerazioni
### Performance
- Valutare caching delle risposte AI per log simili
- Implementare batch processing per alto volume
- Considerare edge functions per ridurre latenza
### Costi
- Monitorare costi OpenRouter/API
- Implementare budget alerts
- Ottimizzare token usage (prompt engineering)
### Scalabilità
- Architettura serverless per handling picchi
- Queue system (Redis/RabbitMQ) per log ad alto volume
- Sharding database per storico log
---
## 🤝 Come Contribuire
Hai un'idea? Aggiungila a questo documento seguendo il formato:
```markdown
- [ ] 💡 **Titolo idea breve**
- Descrizione dettagliata
- Benefici attesi
- Eventuali dipendenze tecniche
- Priorità suggerita (Alta/Media/Bassa)
```
---
*Documento mantenuto dal team LogWhispererAI*

192
docs/tools_fake_backend.md Normal file
View File

@@ -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 LogWhispererAI. È 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*

15
frontend/.env.development Normal file
View File

@@ -0,0 +1,15 @@
# LogWhispererAI - Frontend Environment Variables (Development)
# These variables are exposed to the browser (must start with VITE_)
# Backend API URL
VITE_API_URL=http://192.168.254.79:3001
# Base URL for webhook endpoints
VITE_WEBHOOK_BASE_URL=http://192.168.254.79:3001/webhook
# Install script URL
VITE_INSTALL_SCRIPT_URL=http://192.168.254.79:3001/install.sh
# Application identification
VITE_APP_NAME=LogWhispererAI
VITE_APP_URL=http://192.168.254.79:5173

View File

@@ -1,20 +1,31 @@
import { Navbar, Hero, ProblemSolution, HowItWorks } from './components';
import { Navbar, Footer, Hero, ProblemSolution, HowItWorks, InteractiveDemo, OnboardingWizard } from './components';
import './App.css';
function App() {
const handlePrimaryCta = () => {
console.log('Primary CTA clicked: Ottieni Webhook URL');
// TODO: Implementare logica per generazione webhook
const onboardingSection = document.getElementById('onboarding');
if (onboardingSection) {
onboardingSection.scrollIntoView({ behavior: 'smooth' });
}
};
const handleSecondaryCta = () => {
console.log('Secondary CTA clicked: Guarda Demo');
// TODO: Implementare logica per apertura demo
const demoSection = document.getElementById('demo-interattiva');
if (demoSection) {
demoSection.scrollIntoView({ behavior: 'smooth' });
}
};
const handleNavbarCta = () => {
console.log('Navbar CTA clicked: Inizia Gratis');
// TODO: Scroll to form o apertura modal
const onboardingSection = document.getElementById('onboarding');
if (onboardingSection) {
onboardingSection.scrollIntoView({ behavior: 'smooth' });
}
};
const handleOnboardingComplete = () => {
console.log('Onboarding completato!');
// TODO: Redirect to dashboard o mostra messaggio success
};
return (
@@ -27,7 +38,10 @@ function App() {
/>
<ProblemSolution />
<HowItWorks />
<InteractiveDemo />
<OnboardingWizard onComplete={handleOnboardingComplete} />
</main>
<Footer />
</div>
);
}

View File

@@ -1,4 +1,7 @@
export { Navbar } from './layout/Navbar';
export { Footer } from './layout/Footer';
export { Hero } from './sections/Hero';
export { ProblemSolution } from './sections/ProblemSolution';
export { HowItWorks } from './sections/HowItWorks';
export { InteractiveDemo } from './sections/InteractiveDemo';
export { OnboardingWizard } from './sections/OnboardingWizard';

View File

@@ -0,0 +1,241 @@
import React, { useState } from 'react';
import { Mail, Send } from 'lucide-react';
// SVG Icons for social media
const GithubIcon = () => (
<svg viewBox="0 0 24 24" fill="currentColor" className="w-5 h-5">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
);
const TwitterIcon = () => (
<svg viewBox="0 0 24 24" fill="currentColor" className="w-5 h-5">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
);
const LinkedinIcon = () => (
<svg viewBox="0 0 24 24" fill="currentColor" className="w-5 h-5">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
</svg>
);
const YoutubeIcon = () => (
<svg viewBox="0 0 24 24" fill="currentColor" className="w-5 h-5">
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/>
</svg>
);
export const Footer: React.FC = () => {
const [email, setEmail] = useState('');
const [subscribed, setSubscribed] = useState(false);
const handleSubscribe = (e: React.FormEvent) => {
e.preventDefault();
if (email) {
setSubscribed(true);
setTimeout(() => {
setSubscribed(false);
setEmail('');
}, 3000);
}
};
const currentYear = new Date().getFullYear();
return (
<footer className="w-full bg-slate-900 text-slate-300">
{/* Main Footer Content */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12">
{/* Brand Column */}
<div className="lg:col-span-1">
<div className="flex items-center gap-2 mb-4">
<span className="text-2xl">🌌</span>
<span className="text-xl font-bold text-white tracking-tight">
LogWhispererAI
</span>
</div>
<p className="text-slate-400 text-sm leading-relaxed mb-6">
Il DevOps tascabile che traduce i crash del tuo server e ti dice
l'esatto comando per risolverli.
</p>
{/* Social Links */}
<div className="flex items-center gap-4">
<a
href="https://github.com/LucaSacchiNet/LogWhispererAI"
target="_blank"
rel="noopener noreferrer"
className="w-10 h-10 bg-slate-800 rounded-lg flex items-center justify-center text-slate-400 hover:bg-slate-700 hover:text-white transition-colors"
aria-label="GitHub"
>
<GithubIcon />
</a>
<a
href="https://twitter.com/vite_js"
target="_blank"
rel="noopener noreferrer"
className="w-10 h-10 bg-slate-800 rounded-lg flex items-center justify-center text-slate-400 hover:bg-slate-700 hover:text-white transition-colors"
aria-label="Twitter"
>
<TwitterIcon />
</a>
<a
href="https://linkedin.com/in/lucasacchi"
target="_blank"
rel="noopener noreferrer"
className="w-10 h-10 bg-slate-800 rounded-lg flex items-center justify-center text-slate-400 hover:bg-slate-700 hover:text-white transition-colors"
aria-label="LinkedIn"
>
<LinkedinIcon />
</a>
<a
href="https://youtube.com/@lucasacchinet"
target="_blank"
rel="noopener noreferrer"
className="w-10 h-10 bg-slate-800 rounded-lg flex items-center justify-center text-slate-400 hover:bg-slate-700 hover:text-white transition-colors"
aria-label="YouTube"
>
<YoutubeIcon />
</a>
</div>
</div>
{/* Quick Links */}
<div>
<h3 className="text-white font-semibold mb-4">Link Utili</h3>
<ul className="space-y-3">
<li>
<a
href="#"
className="text-slate-400 hover:text-white transition-colors text-sm"
>
Documentazione
</a>
</li>
<li>
<a
href="https://github.com/LucaSacchiNet/LogWhispererAI"
target="_blank"
rel="noopener noreferrer"
className="text-slate-400 hover:text-white transition-colors text-sm"
>
GitHub Repository
</a>
</li>
<li>
<a
href="#"
className="text-slate-400 hover:text-white transition-colors text-sm"
>
Roadmap
</a>
</li>
<li>
<a
href="#demo-interattiva"
className="text-slate-400 hover:text-white transition-colors text-sm"
>
Prova la Demo
</a>
</li>
</ul>
</div>
{/* Legal */}
<div>
<h3 className="text-white font-semibold mb-4">Legale</h3>
<ul className="space-y-3">
<li>
<a
href="#"
className="text-slate-400 hover:text-white transition-colors text-sm"
>
Privacy Policy
</a>
</li>
<li>
<a
href="#"
className="text-slate-400 hover:text-white transition-colors text-sm"
>
Termini di Servizio
</a>
</li>
<li>
<a
href="#"
className="text-slate-400 hover:text-white transition-colors text-sm"
>
Cookie Policy
</a>
</li>
<li>
<a
href="#"
className="text-slate-400 hover:text-white transition-colors text-sm"
>
Licenza
</a>
</li>
</ul>
</div>
{/* Newsletter */}
<div>
<h3 className="text-white font-semibold mb-4">Newsletter</h3>
<p className="text-slate-400 text-sm mb-4">
Ricevi aggiornamenti su nuove funzionalità e best practices DevOps.
</p>
<form onSubmit={handleSubscribe} className="space-y-3">
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-500" />
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="la tua@email.com"
className="w-full pl-10 pr-4 py-3 bg-slate-800 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-sm"
required
aria-label="Indirizzo email per newsletter"
/>
</div>
<button
type="submit"
disabled={subscribed}
className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-indigo-600 hover:bg-indigo-700 disabled:bg-green-600 text-white font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 text-sm"
>
{subscribed ? (
<>
<span>Iscritto!</span>
</>
) : (
<>
<Send className="w-4 h-4" />
<span>Iscriviti</span>
</>
)}
</button>
</form>
</div>
</div>
</div>
{/* Bottom Bar */}
<div className="border-t border-slate-800">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
<p className="text-slate-500 text-sm">
© {currentYear} LogWhispererAI. Tutti i diritti riservati.
</p>
<p className="text-slate-600 text-xs">
Made with by Luca Sacchi Ricciardi
</p>
</div>
</div>
</div>
</footer>
);
};
export default Footer;

View File

@@ -0,0 +1,285 @@
import React, { useState } from 'react';
import { Terminal, Copy, Check, Loader2, AlertCircle, Activity, Server, Globe, Code } from 'lucide-react';
// Demo log presets - only the content, not the analysis
const DEMO_LOGS = [
{
id: 'postgres-oom',
label: 'PostgreSQL OOM',
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"`,
},
{
id: 'nginx-502',
label: 'Nginx 502 Bad Gateway',
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"`,
},
{
id: 'node-exception',
label: 'Node.js Exception',
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)`,
},
];
interface LogAnalysis {
title: string;
description: string;
command: string;
isSafe: boolean;
note: string;
}
interface ApiResponse {
success: boolean;
analysis: LogAnalysis & { originalLog?: string };
meta?: {
processingTime: string;
model: string;
timestamp: string;
};
error?: string;
message?: string;
}
// Environment configuration
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001';
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;
setSelectedLog(logId);
setIsAnalyzing(true);
setAnalysis(null);
setCopied(false);
setError(null);
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}`);
}
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 = () => {
if (analysis?.command) {
navigator.clipboard.writeText(analysis.command);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
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">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section Header */}
<div className="text-center mb-16">
<h2 id="demo-heading" className="text-3xl sm:text-4xl font-bold text-slate-900 mb-4">
Prova la Demo Interattiva
</h2>
<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 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Left Panel - Terminal Input */}
<div className="bg-slate-900 rounded-2xl overflow-hidden shadow-2xl">
{/* Terminal Header */}
<div className="bg-slate-800 px-4 py-3 flex items-center justify-between border-b border-slate-700">
<div className="flex items-center gap-2">
<Terminal className="w-5 h-5 text-slate-400" aria-hidden="true" />
<span className="text-slate-300 text-sm font-mono">terminal</span>
</div>
<div className="flex gap-1.5">
<div className="w-3 h-3 rounded-full bg-red-500" aria-hidden="true" />
<div className="w-3 h-3 rounded-full bg-yellow-500" aria-hidden="true" />
<div className="w-3 h-3 rounded-full bg-green-500" aria-hidden="true" />
</div>
</div>
{/* Log Selection Buttons */}
<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) => {
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>
{/* Terminal Content */}
<div className="p-6 font-mono text-sm min-h-[300px] max-h-[400px] overflow-auto">
{selectedLogData ? (
<div className="space-y-2">
<div className="text-slate-500">$ tail -f /var/log/syslog</div>
<div className="text-red-400 whitespace-pre-wrap">
{selectedLogData.logContent}
</div>
<div className="text-slate-500 mt-4 animate-pulse"> _</div>
</div>
) : (
<div className="text-slate-500 flex items-center justify-center h-full">
<span>Seleziona un log dall'alto per iniziare...</span>
</div>
)}
</div>
</div>
{/* Right Panel - AI Analysis Output */}
<div
className="bg-slate-50 rounded-2xl p-8 border border-slate-200 shadow-lg"
aria-live="polite"
aria-atomic="true"
>
{!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>
<p className="text-sm mt-2">Seleziona un log a sinistra per iniziare</p>
</div>
)}
{isAnalyzing && (
<div className="h-full flex flex-col items-center justify-center text-center">
<Loader2 className="w-12 h-12 text-indigo-600 animate-spin mb-4" aria-hidden="true" />
<p className="text-lg font-medium text-slate-700">Analisi in corso...</p>
<p className="text-sm text-slate-500 mt-2">L'AI sta analizzando il log</p>
<div className="mt-4 flex gap-1">
<span className="w-2 h-2 bg-indigo-600 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
<span className="w-2 h-2 bg-indigo-600 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
<span className="w-2 h-2 bg-indigo-600 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
</div>
</div>
)}
{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">
<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>
<p className="text-slate-600 mt-1">{analysis.description}</p>
</div>
</div>
{/* Command Box */}
<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 font-medium">Comando suggerito:</span>
{analysis.isSafe && (
<span className="flex items-center gap-1 text-green-400 text-xs bg-green-400/10 px-2 py-1 rounded-full">
<Check className="w-3 h-3" aria-hidden="true" />
Sicuro
</span>
)}
</div>
<code className="block text-green-400 font-mono text-sm break-all">
{analysis.command}
</code>
<button
onClick={handleCopyCommand}
className="mt-4 flex items-center gap-2 px-4 py-2 bg-slate-800 hover:bg-slate-700 text-white text-sm font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
aria-label={copied ? 'Comando copiato' : 'Copia comando negli appunti'}
>
{copied ? (
<>
<Check className="w-4 h-4" aria-hidden="true" />
Copiato!
</>
) : (
<>
<Copy className="w-4 h-4" aria-hidden="true" />
Copia Comando
</>
)}
</button>
</div>
{/* Additional Notes */}
<div className="bg-blue-50 border border-blue-100 rounded-lg p-4">
<p className="text-sm text-blue-800">
<span className="font-semibold">Nota:</span> {analysis.note}
</p>
</div>
</div>
)}
</div>
</div>
</div>
</section>
);
};
export default InteractiveDemo;

View File

@@ -0,0 +1,389 @@
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;

View File

@@ -1,113 +1,13 @@
@import "tailwindcss";
:root {
--text: #6b6375;
--text-h: #08060d;
--bg: #fff;
--border: #e5e4e7;
--code-bg: #f4f3ec;
--accent: #aa3bff;
--accent-bg: rgba(170, 59, 255, 0.1);
--accent-border: rgba(170, 59, 255, 0.5);
--social-bg: rgba(244, 243, 236, 0.5);
--shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, Consolas, monospace;
font: 18px/145% var(--sans);
letter-spacing: 0.18px;
color-scheme: light dark;
color: var(--text);
background: var(--bg);
font-synthesis: none;
text-rendering: optimizeLegibility;
/* Base styles - let Tailwind handle everything */
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@media (max-width: 1024px) {
font-size: 16px;
}
}
@media (prefers-color-scheme: dark) {
:root {
--text: #9ca3af;
--text-h: #f3f4f6;
--bg: #16171d;
--border: #2e303a;
--code-bg: #1f2028;
--accent: #c084fc;
--accent-bg: rgba(192, 132, 252, 0.15);
--accent-border: rgba(192, 132, 252, 0.5);
--social-bg: rgba(47, 48, 58, 0.5);
--shadow:
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
}
#social .button-icon {
filter: invert(1) brightness(2);
}
margin: 0;
}
#root {
width: 1126px;
max-width: 100%;
margin: 0 auto;
text-align: center;
border-inline: 1px solid var(--border);
min-height: 100svh;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
body {
margin: 0;
}
h1,
h2 {
font-family: var(--heading);
font-weight: 500;
color: var(--text-h);
}
h1 {
font-size: 56px;
letter-spacing: -1.68px;
margin: 32px 0;
@media (max-width: 1024px) {
font-size: 36px;
margin: 20px 0;
}
}
h2 {
font-size: 24px;
line-height: 118%;
letter-spacing: -0.24px;
margin: 0 0 8px;
@media (max-width: 1024px) {
font-size: 20px;
}
}
p {
margin: 0;
}
code,
.counter {
font-family: var(--mono);
display: inline-flex;
border-radius: 4px;
color: var(--text-h);
}
code {
font-size: 15px;
line-height: 135%;
padding: 4px 8px;
background: var(--code-bg);
min-height: 100vh;
}

View File

@@ -10,5 +10,28 @@ export default defineConfig({
watch: {
usePolling: true,
},
// Allow requests from reverse proxy host
// Add your production domain here
allowedHosts: [
'localhost',
'127.0.0.1',
'.lab.home.lucasacchi.net', // Allow all subdomains
'logwhispererai.lab.home.lucasacchi.net',
],
// HMR configuration - disabled when running behind reverse proxy
// HMR causes issues with WebSocket through HTTPS reverse proxy
hmr: process.env.NODE_ENV === 'production' ? false : {
// Use polling instead of WebSocket for HMR (more compatible with reverse proxy)
protocol: 'wss',
host: 'logwhispererai.lab.home.lucasacchi.net',
port: 443,
clientPort: 443,
},
// CORS configuration
cors: {
origin: '*',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
},
},
})

33
tools/fake-backend/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,31 @@
# Dockerfile for Fake Backend
# Development mock API server for LogWhispererAI
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"]

854
tools/fake-backend/package-lock.json generated Normal file
View File

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

View File

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

View File

@@ -0,0 +1,273 @@
/**
* Fake Backend Server for LogWhispererAI
*
* Simulates AI analysis responses for frontend development
* without requiring real backend or OpenRouter API calls.
*
* @author LogWhispererAI 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;
// CORS configuration - supports multiple origins via env var
// CORS_ORIGINS can be comma-separated list or '*' for all
const corsOrigins = process.env.CORS_ORIGINS || '*';
const corsOptions = corsOrigins === '*'
? { origin: '*', methods: ['GET', 'POST'], allowedHeaders: ['Content-Type'] }
: {
origin: corsOrigins.split(',').map(o => o.trim()),
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type'],
credentials: true
};
app.use(cors(corsOptions));
// 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);
});
// Environment configuration for webhook URLs
const WEBHOOK_BASE_URL = process.env.WEBHOOK_BASE_URL || 'https://logwhisperer.ai/webhook';
/**
* POST /api/webhook
* Generate a fake webhook URL for onboarding
*/
app.post('/api/webhook', (req, res) => {
// Simulate generation delay (500ms - 1s)
const delay = Math.floor(Math.random() * 500) + 500;
setTimeout(() => {
try {
// Generate fake UUID v4
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);
});
res.json({
success: true,
webhookUrl: `${WEBHOOK_BASE_URL}/${uuid}`,
uuid: uuid,
meta: {
generatedAt: new Date().toISOString(),
expiresIn: '30 days'
}
});
} catch (error) {
console.error('Error generating webhook:', error);
res.status(500).json({
success: false,
error: 'Internal Server Error',
message: 'Errore durante la generazione del webhook',
timestamp: new Date().toISOString()
});
}
}, delay);
});
/**
* 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: 'LogWhispererAI - 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 - listen on all interfaces (0.0.0.0) to allow external connections
app.listen(PORT, '0.0.0.0', () => {
console.log(`
╔══════════════════════════════════════════════════════════════╗
║ LogWhispererAI - 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 ║
║ POST /api/webhook - Generate fake webhook URL ║
║ GET /health - Health check ║
║ ║
║ Press Ctrl+C to stop ║
╚══════════════════════════════════════════════════════════════╝
`);
});
module.exports = app; // Export for testing