272 lines
16 KiB
Python
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() |