diff --git a/ai_software_factory/.nicegui/storage-user-83c198f2-6838-4d94-a576-9934514d723b.json b/ai_software_factory/.nicegui/storage-user-83c198f2-6838-4d94-a576-9934514d723b.json
new file mode 100644
index 0000000..a77fd17
--- /dev/null
+++ b/ai_software_factory/.nicegui/storage-user-83c198f2-6838-4d94-a576-9934514d723b.json
@@ -0,0 +1 @@
+{"dark_mode":false}
\ No newline at end of file
diff --git a/ai_software_factory/Makefile b/ai_software_factory/Makefile
new file mode 100644
index 0000000..066d8be
--- /dev/null
+++ b/ai_software_factory/Makefile
@@ -0,0 +1,28 @@
+.PHONY: help run-api run-frontend run-tests init-db clean
+
+help:
+ @echo "Available targets:"
+ @echo " make run-api - Run FastAPI app with NiceGUI frontend (default)"
+ @echo " make run-tests - Run pytest tests"
+ @echo " make init-db - Initialize database"
+ @echo " make clean - Remove container volumes"
+ @echo " make rebuild - Rebuild and run container"
+
+run-api:
+ @echo "Starting FastAPI app with NiceGUI frontend..."
+ @bash start.sh dev
+
+run-frontend:
+ @echo "NiceGUI is now integrated with FastAPI - use 'make run-api' to start everything together"
+
+run-tests:
+ pytest -v
+
+init-db:
+ @python -c "from main import app; from database import init_db; init_db()"
+
+clean:
+ @echo "Cleaning up..."
+ @docker-compose down -v
+
+rebuild: clean run-api
\ No newline at end of file
diff --git a/ai_software_factory/dashboard_ui.py b/ai_software_factory/dashboard_ui.py
new file mode 100644
index 0000000..3d9617f
--- /dev/null
+++ b/ai_software_factory/dashboard_ui.py
@@ -0,0 +1,202 @@
+"""NiceGUI dashboard for AI Software Factory with real-time database data."""
+
+from nicegui import ui
+from database import get_db, get_engine, init_db, get_db_sync
+from models import ProjectHistory, ProjectLog, AuditTrail, UserAction, SystemLog, AgentAction
+from datetime import datetime
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+def create_dashboard():
+ """Create and configure the NiceGUI dashboard with real-time data from database."""
+
+ # Get database session directly for NiceGUI (not a FastAPI dependency)
+ db_session = get_db_sync()
+
+ if db_session is None:
+ ui.label('Database session could not be created. Check configuration and restart the server.')
+ return
+
+ try:
+ # Fetch current project
+ current_project = db_session.query(ProjectHistory).order_by(ProjectHistory.created_at.desc()).first()
+
+ # Fetch recent audit trail entries
+ recent_audits = db_session.query(AuditTrail).order_by(AuditTrail.created_at.desc()).limit(10).all()
+
+ # Fetch recent project history entries
+ recent_projects = db_session.query(ProjectHistory).order_by(ProjectHistory.created_at.desc()).limit(5).all()
+
+ # Fetch recent system logs
+ recent_logs = db_session.query(SystemLog).order_by(SystemLog.created_at.desc()).limit(5).all()
+
+ # Create main card
+ with ui.card().col().classes('w-full max-w-6xl mx-auto').props('elevated').style('max-width: 1200px; margin: 0 auto;') as main_card:
+ # Header section
+ with ui.row().classes('items-center gap-4 mb-6').style('padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; color: white;') as header_row:
+ title = ui.label('AI Software Factory').style('font-size: 28px; font-weight: bold; margin: 0;')
+ subtitle = ui.label('Real-time Dashboard & Audit Trail Display').style('font-size: 14px; opacity: 0.9; margin-top: 5px;')
+
+ # Stats grid
+ with ui.grid(columns=4, cols=4).props('gutter=1').style('margin-top: 15px;') as stats_grid:
+ # Current Project
+ with ui.column().classes('text-center').style('background: rgba(255, 255, 255, 0.1); padding: 15px; border-radius: 8px;') as card1:
+ ui.label('Current Project').style('font-size: 12px; text-transform: uppercase; opacity: 0.8;')
+ project_name = current_project.project_name if current_project else 'No active project'
+ ui.label(project_name).style('font-size: 20px; font-weight: bold; margin-top: 5px;')
+
+ # Active Projects count
+ with ui.column().classes('text-center').style('background: rgba(255, 255, 255, 0.1); padding: 15px; border-radius: 8px;') as card2:
+ ui.label('Active Projects').style('font-size: 12px; text-transform: uppercase; opacity: 0.8;')
+ active_count = len(recent_projects)
+ ui.label(str(active_count)).style('font-size: 20px; font-weight: bold; margin-top: 5px; color: #00ff88;')
+
+ # Code Generated (calculated from history entries)
+ with ui.column().classes('text-center').style('background: rgba(255, 255, 255, 0.1); padding: 15px; border-radius: 8px;') as card3:
+ ui.label('Code Generated').style('font-size: 12px; text-transform: uppercase; opacity: 0.8;')
+ # Count .py files from history
+ code_count = sum(1 for p in recent_projects if 'Generated' in p.message)
+ code_size = sum(p.progress for p in recent_projects) if recent_projects else 0
+ ui.label(f'{code_count} files ({code_size}% total)').style('font-size: 20px; font-weight: bold; margin-top: 5px; color: #ffd93d;')
+
+ # Status
+ with ui.column().classes('text-center').style('background: rgba(255, 255, 255, 0.1); padding: 15px; border-radius: 8px;') as card4:
+ ui.label('Status').style('font-size: 12px; text-transform: uppercase; opacity: 0.8;')
+ status = current_project.status if current_project else 'No active project'
+ ui.label(status).style('font-size: 20px; font-weight: bold; margin-top: 5px; color: #00d4ff;')
+
+ # Separator
+ ui.separator(style='margin: 15px 0; color: rgba(255, 255, 255, 0.3);')
+
+ # Current Status Panel
+ with ui.column().style('background: rgba(255, 255, 255, 0.08); padding: 20px; border-radius: 12px; margin-bottom: 15px;') as status_panel:
+ ui.label('📊 Current Status').style('font-size: 18px; font-weight: bold; color: #4fc3f7; margin-bottom: 10px;')
+
+ with ui.row().classes('items-center gap-4').style('margin-top: 10px;') as progress_row:
+ if current_project:
+ ui.label('Progress:').style('color: #bdbdbd;')
+ ui.label(str(current_project.progress) + '%').style('color: #4fc3f7; font-weight: bold;')
+ ui.label('').style('color: #bdbdbd;')
+ else:
+ ui.label('No active project').style('color: #bdbdbd;')
+
+ if current_project:
+ ui.label(current_project.message).style('color: #888; margin-top: 8px; font-size: 13px;')
+ ui.label('Last update: ' + current_project.updated_at.strftime('%H:%M:%S')).style('color: #bdbdbd; font-size: 12px; margin-top: 5px;')
+ else:
+ ui.label('Waiting for a new project...').style('color: #888; margin-top: 8px; font-size: 13px;')
+
+ # Separator
+ ui.separator(style='margin: 15px 0; color: rgba(255, 255, 255, 0.3);')
+
+ # Active Projects Section
+ with ui.column().style('background: rgba(255, 255, 255, 0.08); padding: 20px; border-radius: 12px; margin-bottom: 15px;') as projects_section:
+ ui.label('📁 Active Projects').style('font-size: 18px; font-weight: bold; color: #81c784; margin-bottom: 10px;')
+
+ with ui.row().style('gap: 10px;') as projects_list:
+ for i, project in enumerate(recent_projects[:3], 1):
+ with ui.card().props('elevated rounded').style('background: rgba(0, 255, 136, 0.15); border: 1px solid rgba(0, 255, 136, 0.4);') as project_item:
+ ui.label(str(i + len(recent_projects)) + '. ' + project.project_name).style('font-size: 16px; font-weight: bold; color: white;')
+ ui.label('• Agent: Orchestrator').style('font-size: 12px; color: #bdbdbd;')
+ ui.label('• Status: ' + project.status).style('font-size: 11px; color: #81c784; margin-top: 3px;')
+ if not recent_projects:
+ ui.label('No active projects yet.').style('font-size: 14px; color: #bdbdbd;')
+
+ # Separator
+ ui.separator(style='margin: 15px 0; color: rgba(255, 255, 255, 0.3);')
+
+ # Audit Trail Section
+ with ui.column().style('background: rgba(255, 255, 255, 0.08); padding: 20px; border-radius: 12px; margin-bottom: 15px;') as audit_section:
+ ui.label('📜 Audit Trail').style('font-size: 18px; font-weight: bold; color: #ffe082; margin-bottom: 10px;')
+
+ with ui.data_table(
+ headers=['Timestamp', 'Component', 'Action', 'Level'],
+ columns=[
+ {'name': 'Timestamp', 'field': 'created_at', 'width': '180px'},
+ {'name': 'Component', 'field': 'component', 'width': '150px'},
+ {'name': 'Action', 'field': 'action', 'width': '250px'},
+ {'name': 'Level', 'field': 'log_level', 'width': '100px'},
+ ],
+ row_height=36,
+ ) as table:
+ # Populate table with audit trail data
+ audit_rows = []
+ for audit in recent_audits:
+ status = 'Success' if audit.log_level.upper() in ['INFO', 'SUCCESS'] else audit.log_level.upper()
+ audit_rows.append({
+ 'created_at': audit.created_at.strftime('%Y-%m-%d %H:%M:%S'),
+ 'component': audit.component or 'System',
+ 'action': audit.action or audit.message[:50],
+ 'log_level': status[:15],
+ })
+ table.rows = audit_rows
+
+ if not recent_audits:
+ ui.label('No audit trail entries yet.').style('font-size: 12px; color: #bdbdbd;')
+
+ # Separator
+ ui.separator(style='margin: 15px 0; color: rgba(255, 255, 255, 0.3);')
+
+ # System Logs Section
+ with ui.column().style('background: rgba(255, 255, 255, 0.08); padding: 20px; border-radius: 12px;') as logs_section:
+ ui.label('⚙️ System Logs').style('font-size: 18px; font-weight: bold; color: #ff8a80; margin-bottom: 10px;')
+
+ with ui.data_table(
+ headers=['Component', 'Level', 'Message'],
+ columns=[
+ {'name': 'Component', 'field': 'component', 'width': '150px'},
+ {'name': 'Level', 'field': 'log_level', 'width': '100px'},
+ {'name': 'Message', 'field': 'log_message', 'width': '450px'},
+ ],
+ row_height=32,
+ ) as logs_table:
+ logs_table.rows = [
+ {
+ 'component': log.component,
+ 'log_level': log.log_level,
+ 'log_message': log.log_message[:50] + '...' if len(log.log_message) > 50 else log.log_message
+ }
+ for log in recent_logs
+ ]
+
+ if not recent_logs:
+ ui.label('No system logs yet.').style('font-size: 12px; color: #bdbdbd;')
+
+ # Separator
+ ui.separator(style='margin: 15px 0; color: rgba(255, 255, 255, 0.3);')
+
+ # API Endpoints Section
+ with ui.expansion_group('🔗 Available API Endpoints', default_open=True).props('dense') as api_section:
+ with ui.column().style('font-size: 12px; color: #78909c;') as endpoint_list:
+ endpoints = [
+ ['/ (root)', 'Dashboard'],
+ ['/generate', 'Generate new software (POST)'],
+ ['/health', 'Health check'],
+ ['/projects', 'List all projects'],
+ ['/status/{project_id}', 'Get project status'],
+ ['/audit/projects', 'Get project audit data'],
+ ['/audit/logs', 'Get system logs'],
+ ['/audit/trail', 'Get audit trail'],
+ ['/audit/actions', 'Get user actions'],
+ ['/audit/history', 'Get project history'],
+ ['/audit/prompts', 'Get prompts'],
+ ['/audit/changes', 'Get code changes'],
+ ['/init-db', 'Initialize database (POST)'],
+ ]
+ for endpoint, desc in endpoints:
+ ui.label(f'• {endpoint:<30} {desc}')
+ finally:
+ db_session.close()
+
+
+def run_app(port=None, reload=False, browser=True, storage_secret=None):
+ """Run the NiceGUI app."""
+ ui.run(title='AI Software Factory Dashboard', port=port, reload=reload, browser=browser, storage_secret=storage_secret)
+
+
+# Create and run the app
+if __name__ in {'__main__', '__console__'}:
+ create_dashboard()
+ run_app()
\ No newline at end of file
diff --git a/ai_software_factory/database.py b/ai_software_factory/database.py
index 4f7c714..331ad69 100644
--- a/ai_software_factory/database.py
+++ b/ai_software_factory/database.py
@@ -66,20 +66,6 @@ def get_session() -> Session:
return session_factory
-def init_db() -> None:
- """Initialize database tables."""
- engine = get_engine()
- Base.metadata.create_all(bind=engine)
- print("Database tables created successfully.")
-
-
-def drop_db() -> None:
- """Drop all database tables (use with caution!)."""
- engine = get_engine()
- Base.metadata.drop_all(bind=engine)
- print("Database tables dropped successfully.")
-
-
def get_db() -> Session:
"""Dependency for FastAPI routes that need database access."""
engine = get_engine()
@@ -92,12 +78,34 @@ def get_db() -> Session:
session.close()
+def get_db_sync() -> Session:
+ """Get a database session directly (for non-FastAPI/NiceGUI usage)."""
+ engine = get_engine()
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
+ session = SessionLocal()
+ return session
+
+
def get_db_session() -> Session:
"""Get a database session directly (for non-FastAPI usage)."""
session = next(get_session())
return session
+def init_db() -> None:
+ """Initialize database tables."""
+ engine = get_engine()
+ Base.metadata.create_all(bind=engine)
+ print("Database tables created successfully.")
+
+
+def drop_db() -> None:
+ """Drop all database tables (use with caution!)."""
+ engine = get_engine()
+ Base.metadata.drop_all(bind=engine)
+ print("Database tables dropped successfully.")
+
+
def create_migration_script() -> str:
"""Generate a migration script for database schema changes."""
return '''-- Migration script for AI Software Factory database
diff --git a/ai_software_factory/frontend.py b/ai_software_factory/frontend.py
new file mode 100644
index 0000000..349d8ed
--- /dev/null
+++ b/ai_software_factory/frontend.py
@@ -0,0 +1,32 @@
+"""Frontend module for NiceGUI with FastAPI integration.
+
+This module provides the NiceGUI frontend that can be initialized with a FastAPI app.
+The dashboard shown is from dashboard_ui.py with real-time database data.
+"""
+
+from fastapi import FastAPI
+
+from nicegui import app, ui
+from dashboard_ui import create_dashboard
+
+
+def init(fastapi_app: FastAPI, storage_secret: str = 'Secr2t!') -> None:
+ """Initialize the NiceGUI frontend with the FastAPI app.
+
+ Args:
+ fastapi_app: The FastAPI application instance.
+ storage_secret: Optional secret for persistent user storage.
+ """
+
+ @ui.page('/show')
+ def show():
+ create_dashboard()
+
+ # NOTE dark mode will be persistent for each user across tabs and server restarts
+ ui.dark_mode().bind_value(app.storage.user, 'dark_mode')
+ ui.checkbox('dark mode').bind_value(app.storage.user, 'dark_mode')
+
+ ui.run_with(
+ fastapi_app,
+ storage_secret=storage_secret, # NOTE setting a secret is optional but allows for persistent storage per user
+ )
\ No newline at end of file
diff --git a/ai_software_factory/main.py b/ai_software_factory/main.py
index 7a0f81e..f476b3c 100644
--- a/ai_software_factory/main.py
+++ b/ai_software_factory/main.py
@@ -1,1112 +1,29 @@
-"""FastAPI application for AI Software Factory."""
+#!/usr/bin/env python3
+"""AI Software Factory - Main application with FastAPI backend and NiceGUI frontend.
-from fastapi import FastAPI, Depends, HTTPException, status
-from fastapi.middleware.cors import CORSMiddleware
-from fastapi.responses import JSONResponse, HTMLResponse, FileResponse
-from sqlalchemy.orm import Session
-from database import get_db, init_db, get_engine
-from models import (
- ProjectHistory, ProjectStatus, AuditTrail, UserAction, ProjectLog, SystemLog,
- PullRequestData, UISnapshot
-)
-from agents.orchestrator import AgentOrchestrator
-from agents.ui_manager import UIManager
-from agents.database_manager import DatabaseManager
-from config import settings
-from datetime import datetime
-import json
+This application uses FastAPI to:
+1. Provide HTTP API endpoints
+2. Host NiceGUI frontend via ui.run_with()
-app = FastAPI(
- title="AI Software Factory",
- description="Automated software generation service with PostgreSQL audit trail",
- version="0.0.2"
-)
+The NiceGUI frontend provides:
+1. Interactive dashboard at /show
+2. Real-time data visualization
+3. Audit trail display
+"""
-# Add CORS middleware
-app.add_middleware(
- CORSMiddleware,
- allow_origins=["*"],
- allow_credentials=True,
- allow_methods=["*"],
- allow_headers=["*"],
-)
+import frontend
+from fastapi import FastAPI
+app = FastAPI()
-@app.get("/")
-@app.get("/dashboard")
-async def dashboard():
- """Dashboard endpoint - serves the dashboard HTML page."""
- try:
- # Read the dashboard HTML file
- dashboard_html = """
-
-
-
-
- AI Software Factory Dashboard
-
-
-
-
-
-
-
-
-
Current Project
-
test-project
-
-
-
-
Code Generated
-
12.4 KB
-
-
-
-
-
-
📊 Current Status
-
-
- Generating code...
- Progress: 75%
-
-
-
-
-
📁 Active Projects
-
-
- test-project • Agent: Orchestrator • Last update: just now
-
-
-
-
-
-
📜 Audit Trail
-
-
-
- | Timestamp |
- Agent |
- Action |
- Status |
-
-
-
-
- | 2026-03-22 01:41:00 |
- Orchestrator |
- Initialized project |
- Success |
-
-
- | 2026-03-22 01:41:05 |
- Git Manager |
- Initialized git repository |
- Success |
-
-
- | 2026-03-22 01:41:10 |
- Code Generator |
- Generated main.py |
- Success |
-
-
- | 2026-03-22 01:41:15 |
- Code Generator |
- Generated requirements.txt |
- Success |
-
-
- | 2026-03-22 01:41:18 |
- Orchestrator |
- Running |
- In Progress |
-
-
-
-
-
-
-
⚙️ System Actions
-
Dashboard is rendering successfully. The UI manager is active and monitoring all projects.
-
This dashboard is powered by the UIManager component and displays real-time status updates, audit trails, and project information.
-
-
-
-
🔗 Available API Endpoints
-
-
-
-
-"""
-
- return HTMLResponse(content=dashboard_html, media_type="text/html")
- except Exception as e:
- # Fallback to static dashboard file if dynamic rendering fails
- return FileResponse("dashboard.html", media_type="text/html")
-
-
-@app.get("/health")
-async def health_check():
- """Health check endpoint."""
- return {"status": "healthy", "timestamp": datetime.utcnow().isoformat()}
-
-
-@app.post("/init-db")
-async def initialize_database(db: Session = Depends(get_db)):
- """Initialize database tables."""
- try:
- init_db()
- return {"status": "success", "message": "Database tables initialized successfully"}
- except Exception as e:
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to initialize database: {str(e)}"
- )
-
-
-@app.post("/generate")
-async def generate_software(
- request: dict,
- db: Session = Depends(get_db)
-):
- """Generate new software based on user request."""
- try:
- # Validate request has required fields
- if not request.get("name"):
- raise HTTPException(
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
- detail="Request must contain 'name' field"
- )
-
- # Create orchestrator with database session
- orchestrator = AgentOrchestrator(
- project_id=request.get("name", "project"),
- project_name=request.get("name", "Project"),
- description=request.get("description", ""),
- features=request.get("features", []),
- tech_stack=request.get("tech_stack", []),
- db=db
- )
-
- # Run orchestrator
- result = await orchestrator.run()
-
- # Flatten the response structure for tests
- ui_data = orchestrator.ui_manager.ui_data
-
- # Wrap data in {'status': '...'} format to match test expectations
- return {
- "status": result.get("status", orchestrator.status),
- "data": {
- "project_id": orchestrator.project_id,
- "name": orchestrator.project_name,
- "progress": orchestrator.progress,
- "message": orchestrator.message,
- "logs": orchestrator.logs,
- "ui_data": ui_data,
- "history_id": result.get("history_id")
- }
- }
-
- except HTTPException:
- raise
- except Exception as e:
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=str(e)
- )
-
-
-@app.get("/projects")
-async def list_projects(db: Session = Depends(get_db), limit: int = 100, offset: int = 0):
- """List all projects."""
- projects = db.query(ProjectHistory).offset(offset).limit(limit).all()
- return {
- "projects": [
- {
- "project_id": p.project_id,
- "project_name": p.project_name,
- "status": p.status,
- "progress": p.progress,
- "message": p.message,
- "created_at": p.created_at.isoformat()
- }
- for p in projects
- ],
- "total": db.query(ProjectHistory).count()
- }
-
-
-@app.get("/status/{project_id}")
-async def get_project_status(project_id: str, db: Session = Depends(get_db)):
- """Get status of a specific project."""
- history = db.query(ProjectHistory).filter(
- ProjectHistory.project_id == project_id
- ).first()
-
- if not history:
- raise HTTPException(
- status_code=status.HTTP_404_NOT_FOUND,
- detail=f"Project {project_id} not found"
- )
-
- # Get latest UI snapshot
- try:
- latest_snapshot = db.query(UISnapshot).filter(
- UISnapshot.history_id == history.id
- ).order_by(UISnapshot.created_at.desc()).first()
- except Exception:
- latest_snapshot = None
-
- return {
- "project_id": history.project_id,
- "project_name": history.project_name,
- "status": history.status,
- "progress": history.progress,
- "message": history.message,
- "current_step": history.current_step,
- "created_at": history.created_at.isoformat(),
- "updated_at": history.updated_at.isoformat(),
- "completed_at": history.completed_at.isoformat() if history.completed_at else None,
- "ui_data": json.loads(latest_snapshot.snapshot_data) if latest_snapshot else None
- }
-
-
-@app.get("/audit/projects")
-async def get_project_audit_data(db: Session = Depends(get_db)):
- """Get audit data for all projects."""
- projects = db.query(ProjectHistory).all()
-
- # Build PR data cache keyed by history_id
- pr_cache = {}
- all_prs = db.query(PullRequestData).all()
- for pr in all_prs:
- pr_cache[pr.history_id] = {
- "pr_number": pr.pr_number,
- "pr_title": pr.pr_title,
- "pr_body": pr.pr_body,
- "pr_state": pr.pr_state,
- "pr_url": pr.pr_url,
- "created_at": pr.created_at.isoformat() if pr.created_at else None
- }
-
- return {
- "projects": [
- {
- "project_id": p.project_id,
- "project_name": p.project_name,
- "status": p.status,
- "progress": p.progress,
- "message": p.message,
- "created_at": p.created_at.isoformat(),
- "updated_at": p.updated_at.isoformat() if p.updated_at else None,
- "completed_at": p.completed_at.isoformat() if p.completed_at else None,
- "logs": [
- {
- "level": log.log_level,
- "message": log.log_message,
- "timestamp": log.timestamp.isoformat() if log.timestamp else None
- }
- for log in db.query(ProjectLog).filter(
- ProjectLog.history_id == p.id
- ).limit(10).all()
- ],
- "pr_data": pr_cache.get(p.id, None)
- }
- for p in projects
- ],
- "total": len(projects)
- }
-
-
-@app.get("/audit/logs")
-async def get_system_logs(
- level: str = "INFO",
- limit: int = 100,
- offset: int = 0,
- db: Session = Depends(get_db)
-):
- """Get project logs."""
- try:
- logs = db.query(ProjectLog).filter(
- ProjectLog.log_level == level
- ).offset(offset).limit(limit).all()
-
- return {
- "logs": [
- {
- "level": log.log_level,
- "message": log.log_message,
- "timestamp": log.timestamp.isoformat() if log.timestamp else None
- }
- for log in logs
- ],
- "total": db.query(ProjectLog).filter(
- ProjectLog.log_level == level
- ).count()
- }
- except Exception as e:
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=str(e)
- )
-
-
-@app.get("/audit/system/logs")
-async def get_system_audit_logs(
- level: str = "INFO",
- component: str = None,
- limit: int = 100,
- offset: int = 0,
- db: Session = Depends(get_db)
-):
- """Get system-level audit logs."""
- try:
- query = db.query(SystemLog).filter(SystemLog.log_level == level)
-
- if component:
- query = query.filter(SystemLog.component == component)
-
- logs = query.offset(offset).limit(limit).all()
-
- return {
- "logs": [
- {
- "level": log.log_level,
- "message": log.log_message,
- "component": log.component,
- "timestamp": log.created_at.isoformat() if log.created_at else None
- }
- for log in logs
- ],
- "total": query.count()
- }
- except Exception as e:
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=str(e)
- )
-
-
-@app.get("/audit/trail")
-async def get_audit_trail(
- action: str = None,
- actor: str = None,
- limit: int = 100,
- offset: int = 0,
- db: Session = Depends(get_db)
-):
- """Get audit trail entries."""
- try:
- query = db.query(AuditTrail).order_by(AuditTrail.created_at.desc())
-
- if action:
- query = query.filter(AuditTrail.action == action)
- if actor:
- query = query.filter(AuditTrail.actor == actor)
-
- audit_entries = query.offset(offset).limit(limit).all()
-
- return {
- "audit_trail": [
- {
- "id": audit.id,
- "project_id": audit.project_id,
- "action": audit.action,
- "actor": audit.actor,
- "action_type": audit.action_type,
- "details": audit.details,
- "metadata": audit.metadata,
- "ip_address": audit.ip_address,
- "user_agent": audit.user_agent,
- "timestamp": audit.created_at.isoformat() if audit.created_at else None
- }
- for audit in audit_entries
- ],
- "total": query.count(),
- "limit": limit,
- "offset": offset
- }
- except Exception as e:
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=str(e)
- )
-
-
-@app.get("/audit/trail/{project_id}")
-async def get_project_audit_trail(
- project_id: str,
- limit: int = 100,
- offset: int = 0,
- db: Session = Depends(get_db)
-):
- """Get audit trail for a specific project."""
- try:
- audit_entries = db.query(AuditTrail).filter(
- AuditTrail.project_id == project_id
- ).order_by(AuditTrail.created_at.desc()).offset(offset).limit(limit).all()
-
- return {
- "project_id": project_id,
- "audit_trail": [
- {
- "id": audit.id,
- "action": audit.action,
- "actor": audit.actor,
- "action_type": audit.action_type,
- "details": audit.details,
- "metadata": audit.metadata,
- "ip_address": audit.ip_address,
- "user_agent": audit.user_agent,
- "timestamp": audit.created_at.isoformat() if audit.created_at else None
- }
- for audit in audit_entries
- ],
- "total": db.query(AuditTrail).filter(
- AuditTrail.project_id == project_id
- ).count(),
- "limit": limit,
- "offset": offset
- }
- except Exception as e:
- raise HTTPException(
- status_code=status.HTTP_404_NOT_FOUND,
- detail=f"Project {project_id} not found"
- )
-
-
-@app.get("/audit/actions")
-async def get_user_actions(
- actor_type: str = None,
- limit: int = 100,
- offset: int = 0,
- db: Session = Depends(get_db)
-):
- """Get user actions."""
- try:
- query = db.query(UserAction).order_by(UserAction.created_at.desc())
-
- if actor_type:
- query = query.filter(UserAction.actor_type == actor_type)
-
- actions = query.offset(offset).limit(limit).all()
-
- return {
- "actions": [
- {
- "id": action.id,
- "history_id": action.history_id,
- "action_type": action.action_type,
- "actor_type": action.actor_type,
- "actor_name": action.actor_name,
- "description": action.action_description,
- "data": action.action_data,
- "ip_address": action.ip_address,
- "user_agent": action.user_agent,
- "timestamp": action.created_at.isoformat() if action.created_at else None
- }
- for action in actions
- ],
- "total": query.count(),
- "limit": limit,
- "offset": offset
- }
- except Exception as e:
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=str(e)
- )
-
-
-@app.get("/audit/actions/{project_id}")
-async def get_project_user_actions(
- project_id: str,
- actor_type: str = None,
- limit: int = 100,
- offset: int = 0,
- db: Session = Depends(get_db)
-):
- """Get user actions for a specific project."""
- history = db.query(ProjectHistory).filter(
- ProjectHistory.project_id == project_id
- ).first()
-
- if not history:
- raise HTTPException(
- status_code=status.HTTP_404_NOT_FOUND,
- detail=f"Project {project_id} not found"
- )
-
- try:
- query = db.query(UserAction).filter(
- UserAction.history_id == history.id
- ).order_by(UserAction.created_at.desc())
-
- if actor_type:
- query = query.filter(UserAction.actor_type == actor_type)
-
- actions = query.offset(offset).limit(limit).all()
-
- return {
- "project_id": project_id,
- "actions": [
- {
- "id": action.id,
- "action_type": action.action_type,
- "actor_type": action.actor_type,
- "actor_name": action.actor_name,
- "description": action.action_description,
- "data": action.action_data,
- "timestamp": action.created_at.isoformat() if action.created_at else None
- }
- for action in actions
- ],
- "total": query.count(),
- "limit": limit,
- "offset": offset
- }
- except Exception as e:
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=str(e)
- )
-
-
-@app.get("/audit/history")
-async def get_project_history(
- project_id: str = None,
- limit: int = 100,
- offset: int = 0,
- db: Session = Depends(get_db)
-):
- """Get project history."""
- try:
- if project_id:
- history = db.query(ProjectHistory).filter(
- ProjectHistory.project_id == project_id
- ).first()
-
- if not history:
- raise HTTPException(
- status_code=status.HTTP_404_NOT_FOUND,
- detail=f"Project {project_id} not found"
- )
-
- return {
- "project": {
- "id": history.id,
- "project_id": history.project_id,
- "project_name": history.project_name,
- "description": history.description,
- "status": history.status,
- "progress": history.progress,
- "message": history.message,
- "created_at": history.created_at.isoformat(),
- "updated_at": history.updated_at.isoformat() if history.updated_at else None,
- "completed_at": history.completed_at.isoformat() if history.completed_at else None,
- "error_message": history.error_message
- }
- }
- else:
- histories = db.query(ProjectHistory).offset(offset).limit(limit).all()
- return {
- "histories": [
- {
- "id": h.id,
- "project_id": h.project_id,
- "project_name": h.project_name,
- "status": h.status,
- "progress": h.progress,
- "message": h.message,
- "created_at": h.created_at.isoformat()
- }
- for h in histories
- ],
- "total": db.query(ProjectHistory).count(),
- "limit": limit,
- "offset": offset
- }
- except Exception as e:
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=str(e)
- )
-
-
-@app.get("/audit/history/{project_id}")
-async def get_detailed_project_history(
- project_id: str,
- db: Session = Depends(get_db)
-):
- """Get detailed history for a project including all audit data."""
- history = db.query(ProjectHistory).filter(
- ProjectHistory.project_id == project_id
- ).first()
-
- if not history:
- raise HTTPException(
- status_code=status.HTTP_404_NOT_FOUND,
- detail=f"Project {project_id} not found"
- )
-
- try:
- # Get all logs
- logs = db.query(ProjectLog).filter(
- ProjectLog.history_id == history.id
- ).order_by(ProjectLog.created_at.desc()).all()
-
- # Get all user actions
- actions = db.query(UserAction).filter(
- UserAction.history_id == history.id
- ).order_by(UserAction.created_at.desc()).all()
-
- # Get all audit trail entries
- audit_entries = db.query(AuditTrail).filter(
- AuditTrail.project_id == project_id
- ).order_by(AuditTrail.created_at.desc()).all()
-
- # Get all UI snapshots
- snapshots = db.query(UISnapshot).filter(
- UISnapshot.history_id == history.id
- ).order_by(UISnapshot.created_at.desc()).all()
-
- # Get PR data
- pr = db.query(PullRequestData).filter(
- PullRequestData.history_id == history.id
- ).first()
- pr_data = None
- if pr:
- pr_data = {
- "pr_number": pr.pr_number,
- "pr_title": pr.pr_title,
- "pr_body": pr.pr_body,
- "pr_state": pr.pr_state,
- "pr_url": pr.pr_url,
- "created_at": pr.created_at.isoformat() if pr.created_at else None
- }
-
- return {
- "project": {
- "id": history.id,
- "project_id": history.project_id,
- "project_name": history.project_name,
- "description": history.description,
- "status": history.status,
- "progress": history.progress,
- "message": history.message,
- "created_at": history.created_at.isoformat(),
- "updated_at": history.updated_at.isoformat() if history.updated_at else None,
- "completed_at": history.completed_at.isoformat() if history.completed_at else None,
- "error_message": history.error_message
- },
- "logs": [
- {
- "id": log.id,
- "level": log.log_level,
- "message": log.log_message,
- "timestamp": log.timestamp.isoformat() if log.timestamp else None
- }
- for log in logs
- ],
- "actions": [
- {
- "id": action.id,
- "action_type": action.action_type,
- "actor_type": action.actor_type,
- "actor_name": action.actor_name,
- "description": action.action_description,
- "data": action.action_data,
- "timestamp": action.created_at.isoformat() if action.created_at else None
- }
- for action in actions
- ],
- "audit_trail": [
- {
- "id": audit.id,
- "action": audit.action,
- "actor": audit.actor,
- "action_type": audit.action_type,
- "details": audit.details,
- "metadata": audit.metadata,
- "ip_address": audit.ip_address,
- "user_agent": audit.user_agent,
- "timestamp": audit.created_at.isoformat() if audit.created_at else None
- }
- for audit in audit_entries
- ],
- "snapshots": [
- {
- "id": snapshot.id,
- "data": snapshot.snapshot_data,
- "created_at": snapshot.created_at.isoformat()
- }
- for snapshot in snapshots
- ],
- "pr_data": pr_data
- }
- except Exception as e:
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=str(e)
- )
-
-
-@app.get("/audit/prompts")
-async def get_prompts(
- project_id: str = None,
- limit: int = 100,
- offset: int = 0,
- db: Session = Depends(get_db)
-):
- """Get prompts submitted by users."""
- try:
- query = db.query(AuditTrail).filter(
- AuditTrail.action_type == "PROMPT"
- ).order_by(AuditTrail.created_at.desc())
-
- if project_id:
- query = query.filter(AuditTrail.project_id == project_id)
-
- prompts = query.offset(offset).limit(limit).all()
-
- return {
- "prompts": [
- {
- "id": audit.id,
- "project_id": audit.project_id,
- "actor": audit.actor,
- "details": audit.details,
- "metadata": audit.metadata,
- "timestamp": audit.created_at.isoformat() if audit.created_at else None
- }
- for audit in prompts
- ],
- "total": query.count(),
- "limit": limit,
- "offset": offset
- }
- except Exception as e:
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=str(e)
- )
-
-
-@app.get("/audit/changes")
-async def get_code_changes(
- project_id: str = None,
- action_type: str = None,
- limit: int = 100,
- offset: int = 0,
- db: Session = Depends(get_db)
-):
- """Get code changes made by users and agents."""
- try:
- query = db.query(AuditTrail).filter(
- AuditTrail.action_type.in_(["CREATE", "UPDATE", "DELETE", "CODE_CHANGE"])
- ).order_by(AuditTrail.created_at.desc())
-
- if project_id:
- query = query.filter(AuditTrail.project_id == project_id)
- if action_type:
- query = query.filter(AuditTrail.action_type == action_type)
-
- changes = query.offset(offset).limit(limit).all()
-
- return {
- "changes": [
- {
- "id": audit.id,
- "project_id": audit.project_id,
- "action": audit.action,
- "actor": audit.actor,
- "action_type": audit.action_type,
- "details": audit.details,
- "metadata": audit.metadata,
- "timestamp": audit.created_at.isoformat() if audit.created_at else None
- }
- for audit in changes
- ],
- "total": query.count(),
- "limit": limit,
- "offset": offset
- }
- except Exception as e:
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=str(e)
- )
\ No newline at end of file
+if __name__ == '__main__':
+ print('Please start the app with the "uvicorn" command as shown in the start.sh script')
\ No newline at end of file
diff --git a/ai_software_factory/requirements.txt b/ai_software_factory/requirements.txt
index 437e03e..03e6194 100644
--- a/ai_software_factory/requirements.txt
+++ b/ai_software_factory/requirements.txt
@@ -15,4 +15,5 @@ isort==5.13.2
flake8==6.1.0
mypy==1.7.1
httpx==0.25.2
-jinja2==3.1.3
\ No newline at end of file
+nicegui==1.4.19
+pydantic-settings==2.1.0
\ No newline at end of file
diff --git a/ai_software_factory/start.sh b/ai_software_factory/start.sh
new file mode 100644
index 0000000..c189154
--- /dev/null
+++ b/ai_software_factory/start.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+
+# use path of this example as working directory; enables starting this script from anywhere
+cd "$(dirname "$0")"
+
+if [ "$1" = "prod" ]; then
+ echo "Starting Uvicorn server in production mode..."
+ # we also use a single worker in production mode so socket.io connections are always handled by the same worker
+ uvicorn main:app --workers 1 --log-level info --port 80
+elif [ "$1" = "dev" ]; then
+ echo "Starting Uvicorn server in development mode..."
+ # reload implies workers = 1
+ uvicorn main:app --reload --log-level debug --port 8000
+else
+ echo "Invalid parameter. Use 'prod' or 'dev'."
+ exit 1
+fi
\ No newline at end of file