Files
ai_software_factory/ai_software_factory/dashboard_ui.py

272 lines
16 KiB
Python

"""NiceGUI dashboard backed by real database state."""
from __future__ import annotations
from contextlib import closing
from nicegui import ui
try:
from .agents.database_manager import DatabaseManager
from .agents.n8n_setup import N8NSetupAgent
from .config import settings
from .database import get_db_sync, init_db
except ImportError:
from agents.database_manager import DatabaseManager
from agents.n8n_setup import N8NSetupAgent
from config import settings
from database import get_db_sync, init_db
def _resolve_n8n_api_url() -> str:
"""Resolve the configured n8n API base URL."""
if settings.n8n_api_url:
return settings.n8n_api_url
if settings.n8n_webhook_url:
return settings.n8n_webhook_url.split('/webhook', 1)[0].rstrip('/')
return ''
def _load_dashboard_snapshot() -> dict:
"""Load dashboard data from the database."""
db = get_db_sync()
if db is None:
return {'error': 'Database session could not be created'}
with closing(db):
manager = DatabaseManager(db)
try:
return manager.get_dashboard_snapshot(limit=8)
except Exception as exc:
return {'error': f'Database error: {exc}'}
def create_dashboard():
"""Create the main NiceGUI dashboard."""
ui.add_head_html(
"""
<style>
body { background: radial-gradient(circle at top, #f4efe7 0%, #e9e1d4 38%, #d7cec1 100%); }
.factory-shell { max-width: 1240px; margin: 0 auto; }
.factory-panel { background: rgba(255,255,255,0.78); backdrop-filter: blur(18px); border: 1px solid rgba(73,54,40,0.10); border-radius: 24px; box-shadow: 0 24px 60px rgba(84,55,24,0.14); }
.factory-kpi { background: linear-gradient(145deg, rgba(63,94,78,0.94), rgba(29,52,45,0.92)); color: #f8f3eb; border-radius: 18px; padding: 18px; min-height: 128px; }
.factory-muted { color: #745e4c; }
.factory-code { font-family: 'IBM Plex Mono', 'Fira Code', monospace; background: rgba(32,26,20,0.92); color: #f4efe7; border-radius: 14px; padding: 12px; white-space: pre-wrap; }
.factory-chip { background: rgba(173, 129, 82, 0.14); color: #6b4b2e; border-radius: 999px; padding: 4px 10px; font-size: 12px; }
</style>
"""
)
async def setup_n8n_workflow_action() -> None:
api_url = _resolve_n8n_api_url()
if not api_url:
ui.notify('Configure N8N_API_URL or N8N_WEBHOOK_URL first', color='negative')
return
agent = N8NSetupAgent(api_url=api_url, webhook_token=settings.n8n_api_key)
result = await agent.setup(
webhook_path='telegram',
backend_url=f'{settings.backend_public_url}/generate',
force_update=True,
)
db = get_db_sync()
if db is not None:
with closing(db):
DatabaseManager(db).log_system_event(
component='n8n',
level='INFO' if result.get('status') == 'success' else 'ERROR',
message=result.get('message', str(result)),
)
ui.notify(result.get('message', 'n8n setup finished'), color='positive' if result.get('status') == 'success' else 'negative')
dashboard_body.refresh()
def init_db_action() -> None:
result = init_db()
ui.notify(result.get('message', 'Database initialized'), color='positive' if result.get('status') == 'success' else 'negative')
dashboard_body.refresh()
@ui.refreshable
def dashboard_body() -> None:
snapshot = _load_dashboard_snapshot()
if snapshot.get('error'):
with ui.card().classes('factory-panel w-full max-w-4xl mx-auto q-pa-xl'):
ui.label('Dashboard unavailable').style('font-size: 1.5rem; font-weight: 700; color: #5c2d1f;')
ui.label(snapshot['error']).classes('factory-muted')
ui.button('Initialize Database', on_click=init_db_action).props('unelevated')
return
summary = snapshot['summary']
projects = snapshot['projects']
correlations = snapshot['correlations']
system_logs = snapshot['system_logs']
with ui.column().classes('factory-shell w-full gap-4 q-pa-lg'):
with ui.card().classes('factory-panel w-full q-pa-lg'):
with ui.row().classes('items-center justify-between w-full'):
with ui.column().classes('gap-1'):
ui.label('AI Software Factory').style('font-size: 2.3rem; font-weight: 800; color: #302116;')
ui.label('Operational dashboard with project audit, prompt traces, and n8n controls.').classes('factory-muted')
with ui.row().classes('items-center gap-2'):
ui.button('Refresh', on_click=dashboard_body.refresh).props('outline')
ui.button('Initialize DB', on_click=init_db_action).props('unelevated color=dark')
ui.button('Provision n8n Workflow', on_click=setup_n8n_workflow_action).props('unelevated color=accent')
with ui.grid(columns=4).classes('w-full gap-4'):
metrics = [
('Projects', summary['total_projects'], 'Tracked generation requests'),
('Completed', summary['completed_projects'], 'Finished project runs'),
('Prompts', summary['prompt_events'], 'Recorded originating prompts'),
('Code Changes', summary['code_changes'], 'Audited generated file writes'),
]
for title, value, subtitle in metrics:
with ui.card().classes('factory-kpi'):
ui.label(title).style('font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.08em; opacity: 0.8;')
ui.label(str(value)).style('font-size: 2.1rem; font-weight: 800; margin-top: 6px;')
ui.label(subtitle).style('font-size: 0.9rem; opacity: 0.78; margin-top: 8px;')
tabs = ui.tabs().classes('w-full')
overview_tab = ui.tab('Overview')
projects_tab = ui.tab('Projects')
trace_tab = ui.tab('Prompt Trace')
system_tab = ui.tab('System')
with ui.tab_panels(tabs, value=overview_tab).classes('w-full'):
with ui.tab_panel(overview_tab):
with ui.grid(columns=2).classes('w-full gap-4'):
with ui.card().classes('factory-panel q-pa-lg'):
ui.label('Project Pipeline').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;')
if projects:
for project_bundle in projects[:4]:
project = project_bundle['project']
with ui.column().classes('gap-1 q-mt-md'):
with ui.row().classes('justify-between items-center'):
ui.label(project['project_name']).style('font-weight: 700; color: #2f241d;')
ui.label(project['status']).classes('factory-chip')
ui.linear_progress(value=(project['progress'] or 0) / 100, show_value=False).classes('w-full')
ui.label(project['message'] or 'No status message').classes('factory-muted')
else:
ui.label('No projects in the database yet.').classes('factory-muted')
with ui.card().classes('factory-panel q-pa-lg'):
ui.label('n8n and Runtime').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;')
rows = [
('Backend URL', settings.backend_public_url),
('Project Root', str(settings.projects_root)),
('n8n API URL', _resolve_n8n_api_url() or 'Not configured'),
('Running Projects', str(summary['running_projects'])),
('Errored Projects', str(summary['error_projects'])),
]
for label, value in rows:
with ui.row().classes('justify-between w-full q-mt-sm'):
ui.label(label).classes('factory-muted')
ui.label(value).style('font-weight: 600; color: #3a281a;')
with ui.tab_panel(projects_tab):
if not projects:
with ui.card().classes('factory-panel q-pa-lg'):
ui.label('No project data available yet.').classes('factory-muted')
for project_bundle in projects:
project = project_bundle['project']
with ui.expansion(f"{project['project_name']} · {project['status']}", icon='folder').classes('factory-panel w-full q-mb-md'):
with ui.grid(columns=2).classes('w-full gap-4 q-pa-md'):
with ui.card().classes('q-pa-md'):
ui.label('Prompt').style('font-weight: 700; color: #3a281a;')
prompts = project_bundle.get('prompts', [])
if prompts:
prompt = prompts[0]
ui.markdown(f"**Requested features:** {', '.join(prompt['features']) or 'None'}")
ui.markdown(f"**Tech stack:** {', '.join(prompt['tech_stack']) or 'None'}")
ui.label(prompt['prompt_text']).classes('factory-code')
else:
ui.label('No prompt recorded.').classes('factory-muted')
with ui.card().classes('q-pa-md'):
ui.label('Generated Changes').style('font-weight: 700; color: #3a281a;')
changes = project_bundle.get('code_changes', [])
if changes:
for change in changes:
with ui.row().classes('justify-between items-start w-full q-mt-sm'):
ui.label(change['file_path'] or 'unknown file').style('font-weight: 600; color: #2f241d;')
ui.label(change['action_type']).classes('factory-chip')
ui.label(change['diff_summary'] or change['details']).classes('factory-muted')
else:
ui.label('No code changes recorded.').classes('factory-muted')
with ui.grid(columns=2).classes('w-full gap-4 q-pa-md'):
with ui.card().classes('q-pa-md'):
ui.label('Recent Logs').style('font-weight: 700; color: #3a281a;')
logs = project_bundle.get('logs', [])[:6]
if logs:
for log in logs:
ui.markdown(f"- {log['timestamp'] or 'n/a'} · {log['level']} · {log['message']}")
else:
ui.label('No project logs yet.').classes('factory-muted')
with ui.card().classes('q-pa-md'):
ui.label('Audit Trail').style('font-weight: 700; color: #3a281a;')
audits = project_bundle.get('audit_trail', [])[:6]
if audits:
for audit in audits:
ui.markdown(f"- {audit['timestamp'] or 'n/a'} · {audit['action']} · {audit['details']}")
else:
ui.label('No audit events yet.').classes('factory-muted')
with ui.tab_panel(trace_tab):
with ui.card().classes('factory-panel q-pa-lg'):
ui.label('Prompt to Code Correlation').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;')
ui.label('Each prompt entry is linked to the generated files recorded after that prompt for the same project.').classes('factory-muted')
if correlations:
for correlation in correlations:
with ui.card().classes('q-pa-md q-mt-md'):
ui.label(correlation['project_id']).style('font-size: 1rem; font-weight: 700; color: #2f241d;')
ui.label(correlation['prompt_text']).classes('factory-code q-mt-sm')
if correlation['changes']:
for change in correlation['changes']:
ui.markdown(
f"- **{change['file_path'] or 'unknown'}** · {change['change_type']} · {change['diff_summary'] or change['details']}"
)
else:
ui.label('No code changes correlated to this prompt yet.').classes('factory-muted')
else:
ui.label('No prompt traces recorded yet.').classes('factory-muted')
with ui.tab_panel(system_tab):
with ui.grid(columns=2).classes('w-full gap-4'):
with ui.card().classes('factory-panel q-pa-lg'):
ui.label('System Logs').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;')
if system_logs:
for log in system_logs:
ui.markdown(f"- {log['timestamp'] or 'n/a'} · **{log['component']}** · {log['level']} · {log['message']}")
else:
ui.label('No system logs yet.').classes('factory-muted')
with ui.card().classes('factory-panel q-pa-lg'):
ui.label('Important Endpoints').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;')
endpoints = [
'/health',
'/generate',
'/projects',
'/audit/projects',
'/audit/prompts',
'/audit/changes',
'/audit/correlations',
'/n8n/health',
'/n8n/setup',
]
for endpoint in endpoints:
ui.label(endpoint).classes('factory-code q-mt-sm')
dashboard_body()
ui.timer(10.0, dashboard_body.refresh)
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)
if __name__ in {'__main__', '__console__'}:
create_dashboard()
run_app()