From 3d77ac3104de59667885deefe05f048cf2c5a146 Mon Sep 17 00:00:00 2001 From: Simon Diesenreiter Date: Sat, 11 Apr 2026 10:30:56 +0200 Subject: [PATCH] feat: better dashboard reloading mechanism, refs NOISSUE --- ai_software_factory/dashboard_ui.py | 1080 +++++++++++++++------------ 1 file changed, 582 insertions(+), 498 deletions(-) diff --git a/ai_software_factory/dashboard_ui.py b/ai_software_factory/dashboard_ui.py index 553e662..22313ac 100644 --- a/ai_software_factory/dashboard_ui.py +++ b/ai_software_factory/dashboard_ui.py @@ -627,15 +627,15 @@ def create_dashboard(): def _store_llm_stage(event) -> None: app.storage.user[llm_stage_filter_key] = event.value or '' - dashboard_body.refresh() + _refresh_llm_filtered_sections() def _store_llm_model(event) -> None: app.storage.user[llm_model_filter_key] = event.value or '' - dashboard_body.refresh() + _refresh_llm_filtered_sections() def _store_llm_search(event) -> None: app.storage.user[llm_search_filter_key] = event.value or '' - dashboard_body.refresh() + _refresh_llm_filtered_sections() def _selected_commit_lookup() -> str: return app.storage.user.get(commit_lookup_key, '') @@ -648,7 +648,7 @@ def create_dashboard(): def _store_branch_scope(event) -> None: app.storage.user[branch_scope_filter_key] = event.value or '' - dashboard_body.refresh() + _refresh_timeline_sections() def _selected_repo_owner() -> str: return app.storage.user.get(repo_owner_key, settings.gitea_owner or '') @@ -698,7 +698,7 @@ def create_dashboard(): ) _set_discovered_repositories(resolved) ui.notify(f'Discovered {len(resolved)} repositories in {owner}', color='positive') - dashboard_body.refresh() + _refresh_system_sections() async def onboard_repository_action(owner: str, repo_name: str) -> None: if not settings.gitea_url or not settings.gitea_token: @@ -728,7 +728,7 @@ def create_dashboard(): ) await discover_gitea_repositories_action() ui.notify(f'Onboarded {owner}/{repo_name}', color='positive') - dashboard_body.refresh() + _refresh_all_dashboard_sections() def sync_project_repository_action(project_id: str) -> None: if not settings.gitea_url or not settings.gitea_token: @@ -760,7 +760,7 @@ def create_dashboard(): ) manager.sync_repository_issues(project_id=project_id, gitea_api=gitea_api, state='open') ui.notify(result.get('message', 'Repository sync finished'), color='positive' if result.get('status') == 'success' else 'negative') - dashboard_body.refresh() + _refresh_all_dashboard_sections() async def setup_n8n_workflow_action() -> None: api_url = _resolve_n8n_api_url() @@ -787,7 +787,7 @@ def create_dashboard(): if result.get('status') == 'error': _render_n8n_error_dialog(result) ui.notify(result.get('message', 'n8n setup finished'), color='positive' if result.get('status') == 'success' else 'negative') - dashboard_body.refresh() + _refresh_all_dashboard_sections() async def send_telegram_prompt_guide_action() -> None: if not settings.telegram_bot_token: @@ -815,12 +815,12 @@ def create_dashboard(): ) ui.notify(result.get('message', 'Telegram message sent'), color='positive' if result.get('status') == 'success' else 'negative') - dashboard_body.refresh() + _refresh_health_sections() 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() + _refresh_all_dashboard_sections() async def undo_prompt_action(project_id: str, prompt_id: int) -> None: db = get_db_sync() @@ -830,7 +830,7 @@ def create_dashboard(): with closing(db): result = await PromptWorkflowManager(db).undo_prompt(project_id=project_id, prompt_id=prompt_id) ui.notify(result.get('message', 'Prompt reverted') if result.get('status') != 'success' else 'Prompt changes reverted', color='positive' if result.get('status') == 'success' else 'negative') - dashboard_body.refresh() + _refresh_all_dashboard_sections() def archive_project_action(project_id: str) -> None: db = get_db_sync() @@ -840,7 +840,7 @@ def create_dashboard(): with closing(db): result = DatabaseManager(db).archive_project(project_id) ui.notify(result.get('message', 'Project archived'), color='positive' if result.get('status') == 'success' else 'negative') - dashboard_body.refresh() + _refresh_all_dashboard_sections() def unarchive_project_action(project_id: str) -> None: db = get_db_sync() @@ -850,7 +850,7 @@ def create_dashboard(): with closing(db): result = DatabaseManager(db).unarchive_project(project_id) ui.notify(result.get('message', 'Project restored'), color='positive' if result.get('status') == 'success' else 'negative') - dashboard_body.refresh() + _refresh_all_dashboard_sections() def delete_project_action(project_id: str) -> None: db = get_db_sync() @@ -876,527 +876,611 @@ def create_dashboard(): if remote_delete and not remote_delete.get('error'): message = f"{message}; remote repository deleted" ui.notify(message, color='positive' if result.get('status') == 'success' else 'negative') - dashboard_body.refresh() + _refresh_all_dashboard_sections() - @ui.refreshable - def dashboard_body() -> None: + dashboard_state: dict = {} + + def _load_dashboard_view_model() -> dict: 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'] - archived_projects = snapshot.get('archived_projects', []) - correlations = snapshot['correlations'] - system_logs = snapshot['system_logs'] llm_runtime = LLMServiceClient().get_runtime_configuration() llm_stage_filter = _selected_llm_stage() llm_model_filter = _selected_llm_model() llm_search_filter = _selected_llm_search() branch_scope_filter = _selected_branch_scope() commit_lookup_query = _selected_commit_lookup() - commit_context = _load_commit_context(commit_lookup_query, branch_scope_filter) if commit_lookup_query else None discovered_repositories = _get_discovered_repositories() - all_llm_traces = [trace for project_bundle in projects for trace in project_bundle.get('llm_traces', [])] - llm_stage_options = [''] + sorted({trace.get('stage') for trace in all_llm_traces if trace.get('stage')}) - llm_model_options = [''] + sorted({trace.get('model') for trace in all_llm_traces if trace.get('model')}) - project_repository_map = { - project_bundle['project']['project_id']: { - 'project_name': project_bundle['project']['project_name'], - 'repository': project_bundle.get('repository') or project_bundle['project'].get('repository'), - 'repository_sync': project_bundle.get('repository_sync') or project_bundle['project'].get('repository_sync'), - 'pull_request': next((pr for pr in project_bundle.get('pull_requests', []) if pr.get('pr_state') == 'open' and not pr.get('merged')), None), + if snapshot.get('error'): + return { + 'error': snapshot['error'], + 'llm_runtime': llm_runtime, + 'llm_stage_filter': llm_stage_filter, + 'llm_model_filter': llm_model_filter, + 'llm_search_filter': llm_search_filter, + 'branch_scope_filter': branch_scope_filter, + 'commit_lookup_query': commit_lookup_query, + 'discovered_repositories': discovered_repositories, } - for project_bundle in projects - if project_bundle.get('project') + projects = snapshot['projects'] + all_llm_traces = [trace for project_bundle in projects for trace in project_bundle.get('llm_traces', [])] + return { + 'snapshot': snapshot, + 'summary': snapshot['summary'], + 'projects': projects, + 'archived_projects': snapshot.get('archived_projects', []), + 'correlations': snapshot['correlations'], + 'system_logs': snapshot['system_logs'], + 'llm_runtime': llm_runtime, + 'llm_stage_filter': llm_stage_filter, + 'llm_model_filter': llm_model_filter, + 'llm_search_filter': llm_search_filter, + 'branch_scope_filter': branch_scope_filter, + 'commit_lookup_query': commit_lookup_query, + 'commit_context': _load_commit_context(commit_lookup_query, branch_scope_filter) if commit_lookup_query else None, + 'discovered_repositories': discovered_repositories, + 'llm_stage_options': [''] + sorted({trace.get('stage') for trace in all_llm_traces if trace.get('stage')}), + 'llm_model_options': [''] + sorted({trace.get('model') for trace in all_llm_traces if trace.get('model')}), + 'project_repository_map': { + project_bundle['project']['project_id']: { + 'project_name': project_bundle['project']['project_name'], + 'repository': project_bundle.get('repository') or project_bundle['project'].get('repository'), + 'repository_sync': project_bundle.get('repository_sync') or project_bundle['project'].get('repository_sync'), + 'pull_request': next((pr for pr in project_bundle.get('pull_requests', []) if pr.get('pr_state') == 'open' and not pr.get('merged')), None), + } + for project_bundle in projects + if project_bundle.get('project') + }, } - 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') - ui.button('Message Prompt Channel', on_click=send_telegram_prompt_guide_action).props('outline color=secondary') + def _update_dashboard_state() -> None: + dashboard_state.clear() + dashboard_state.update(_load_dashboard_view_model()) - with ui.grid(columns=4).classes('w-full gap-4'): - metrics = [ - ('Projects', summary['total_projects'], 'Tracked generation requests'), - ('Archived', summary.get('archived_projects', 0), 'Excluded from active automation'), - ('Completed', summary['completed_projects'], 'Finished project runs'), - ('Prompts', summary['prompt_events'], 'Recorded originating prompts'), - ('Open PRs', summary['open_pull_requests'], 'Unmerged review branches'), + def _view_model() -> dict: + if not dashboard_state: + _update_dashboard_state() + return dashboard_state + + def _render_dashboard_unavailable(message: str) -> None: + 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(message).classes('factory-muted') + ui.button('Initialize Database', on_click=init_db_action).props('unelevated') + + @ui.refreshable + def render_header() -> None: + 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=_refresh_current_dashboard_sections).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') + ui.button('Message Prompt Channel', on_click=send_telegram_prompt_guide_action).props('outline color=secondary') + + @ui.refreshable + def render_metrics() -> None: + view_model = _view_model() + if view_model.get('error'): + _render_dashboard_unavailable(view_model['error']) + return + summary = view_model['summary'] + with ui.grid(columns=4).classes('w-full gap-4'): + metrics = [ + ('Projects', summary['total_projects'], 'Tracked generation requests'), + ('Archived', summary.get('archived_projects', 0), 'Excluded from active automation'), + ('Completed', summary['completed_projects'], 'Finished project runs'), + ('Prompts', summary['prompt_events'], 'Recorded originating prompts'), + ('Open PRs', summary['open_pull_requests'], 'Unmerged review branches'), + ] + 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;') + + @ui.refreshable + def render_overview_panel() -> None: + view_model = _view_model() + if view_model.get('error'): + _render_dashboard_unavailable(view_model['error']) + return + projects = view_model['projects'] + summary = view_model['summary'] + 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 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;') + 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;') - selected_tab = _selected_tab_name() - with ui.tabs(value=selected_tab, on_change=_store_selected_tab).classes('w-full') as tabs: - ui.tab('Overview').props('name=overview') - ui.tab('Projects').props('name=projects') - ui.tab('Archived').props('name=archived') - ui.tab('Prompt Trace').props('name=trace') - ui.tab('Compare').props('name=compare') - ui.tab('Timeline').props('name=timeline') - ui.tab('System').props('name=system') - ui.tab('Health').props('name=health') + @ui.refreshable + def render_projects_panel() -> None: + view_model = _view_model() + if view_model.get('error'): + _render_dashboard_unavailable(view_model['error']) + return + projects = view_model['projects'] + 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.row().classes('items-center gap-2 q-pa-md'): + ui.button( + 'Archive', + on_click=lambda _=None, project_id=project['project_id'], project_name=project['project_name']: _render_confirmation_dialog( + 'Archive project?', + f'Archive {project_name}? Archived projects remain visible in the dashboard but are excluded from automation, Telegram routing, sync, and undo actions.', + 'Archive', + lambda: archive_project_action(project_id), + color='warning', + ), + ).props('outline color=warning') + ui.button( + 'Delete', + on_click=lambda _=None, project_id=project['project_id'], project_name=project['project_name']: _render_confirmation_dialog( + 'Delete project permanently?', + f'Delete {project_name}? This removes the local project directory, project traces from the database, and any project-owned remote repository.', + 'Delete Permanently', + lambda: delete_project_action(project_id), + ), + ).props('outline color=negative') + with ui.grid(columns=2).classes('w-full gap-4 q-pa-md'): + with ui.card().classes('q-pa-md'): + ui.label('Repository').style('font-weight: 700; color: #3a281a;') + _render_repository_block(project_bundle.get('repository') or project.get('repository')) + ui.button( + 'Sync Repo Activity', + on_click=lambda _=None, project_id=project['project_id']: sync_project_repository_action(project_id), + ).props('outline color=secondary').classes('q-mt-md') - with ui.tab_panels(tabs, value=selected_tab).classes('w-full'): - with ui.tab_panel('overview'): - 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') + @ui.refreshable + def render_archived_panel() -> None: + view_model = _view_model() + if view_model.get('error'): + _render_dashboard_unavailable(view_model['error']) + return + archived_projects = view_model['archived_projects'] + llm_stage_filter = view_model['llm_stage_filter'] + llm_model_filter = view_model['llm_model_filter'] + llm_search_filter = view_model['llm_search_filter'] + if not archived_projects: + with ui.card().classes('factory-panel q-pa-lg'): + ui.label('No archived projects yet.').classes('factory-muted') + for project_bundle in archived_projects: + project = project_bundle['project'] + with ui.expansion(f"{project['project_name']} · archived", icon='archive').classes('factory-panel w-full q-mb-md'): + with ui.row().classes('items-center gap-2 q-pa-md'): + ui.button( + 'Restore', + on_click=lambda _=None, project_id=project['project_id'], project_name=project['project_name']: _render_confirmation_dialog( + 'Restore archived project?', + f'Restore {project_name} to the active project set so the factory can work on it again?', + 'Restore Project', + lambda: unarchive_project_action(project_id), + color='positive', + ), + ).props('outline color=positive') + ui.button( + 'Delete Permanently', + on_click=lambda _=None, project_id=project['project_id'], project_name=project['project_name']: _render_confirmation_dialog( + 'Delete archived project permanently?', + f'Delete {project_name}? This removes the archived project from both the database and filesystem, and deletes any project-owned remote repository.', + 'Delete Permanently', + lambda: delete_project_action(project_id), + ), + ).props('outline color=negative') + with ui.grid(columns=2).classes('w-full gap-4 q-pa-md'): + with ui.card().classes('q-pa-md'): + ui.label('Repository').style('font-weight: 700; color: #3a281a;') + _render_repository_block(project_bundle.get('repository') or project.get('repository')) + with ui.card().classes('q-pa-md'): + ui.label('Prompt').style('font-weight: 700; color: #3a281a;') + prompts = project_bundle.get('prompts', []) + if prompts: + ui.label(prompts[0]['prompt_text']).classes('factory-code') + else: + ui.label('No prompt 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('Git Commits').style('font-weight: 700; color: #3a281a;') + _render_commit_list(project_bundle.get('commits', [])) + with ui.card().classes('q-pa-md'): + ui.label('Tracked Issues').style('font-weight: 700; color: #3a281a;') + _render_issue_list(project_bundle.get('issues', [])) + with ui.card().classes('q-pa-md'): + ui.label('Repository Sync').style('font-weight: 700; color: #3a281a;') + _render_repository_sync_block(project_bundle.get('repository_sync') or project.get('repository_sync')) + with ui.card().classes('q-pa-md'): + ui.label('Pull Request').style('font-weight: 700; color: #3a281a;') + open_pr = next((pr for pr in project_bundle.get('pull_requests', []) if pr.get('pr_state') == 'open' and not pr.get('merged')), None) + _render_pull_request_block(open_pr) + 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'}") + if prompt.get('related_issue'): + _render_issue_chip(prompt.get('related_issue')) + ui.label(prompt['prompt_text']).classes('factory-code') + else: + ui.label('No prompt recorded.').classes('factory-muted') + with ui.grid(columns=1).classes('w-full gap-4 q-pa-md'): + with ui.card().classes('q-pa-md'): + ui.label('Generated Changes').style('font-weight: 700; color: #3a281a;') + _render_change_list(project_bundle.get('code_changes', [])) + with ui.card().classes('q-pa-md'): + ui.label('Tracked Issues').style('font-weight: 700; color: #3a281a;') + _render_issue_list(project_bundle.get('issues', [])) + with ui.grid(columns=2).classes('w-full gap-4 q-pa-md'): + with ui.card().classes('q-pa-md'): + ui.label('Git Commits').style('font-weight: 700; color: #3a281a;') + _render_commit_list(project_bundle.get('commits', [])) + with ui.card().classes('q-pa-md'): + ui.label('LLM Trace').style('font-weight: 700; color: #3a281a;') + _render_llm_traces(_filter_llm_traces(project_bundle.get('llm_traces', []), llm_stage_filter, llm_model_filter, llm_search_filter)) + 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.grid(columns=1).classes('w-full gap-4 q-pa-md'): + with ui.card().classes('q-pa-md'): + ui.label('Issue Work').style('font-weight: 700; color: #3a281a;') + _render_issue_work_events(project_bundle.get('issue_work', [])) + 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') + + @ui.refreshable + def render_trace_panel() -> None: + view_model = _view_model() + if view_model.get('error'): + _render_dashboard_unavailable(view_model['error']) + return + correlations = view_model['correlations'] + project_repository_map = view_model['project_repository_map'] + llm_stage_options = view_model['llm_stage_options'] + llm_model_options = view_model['llm_model_options'] + llm_stage_filter = view_model['llm_stage_filter'] + llm_model_filter = view_model['llm_model_filter'] + llm_search_filter = view_model['llm_search_filter'] + 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') + with ui.row().classes('items-center gap-3 q-mt-md w-full'): + ui.select(options=llm_stage_options, value=llm_stage_filter, on_change=_store_llm_stage, label='LLM stage').classes('min-w-[12rem]') + ui.select(options=llm_model_options, value=llm_model_filter, on_change=_store_llm_model, label='LLM model').classes('min-w-[12rem]') + ui.input(label='Search trace text', value=llm_search_filter, on_change=_store_llm_search).classes('min-w-[18rem]') + if correlations: + for correlation in correlations: + correlation_project = project_repository_map.get(correlation['project_id'], {}) + filtered_traces = _filter_llm_traces(correlation.get('llm_traces', []), llm_stage_filter, llm_model_filter, llm_search_filter) + with ui.card().classes('q-pa-md q-mt-md'): + ui.label(correlation_project.get('project_name') or correlation['project_id']).style('font-size: 1rem; font-weight: 700; color: #2f241d;') + _render_repository_block(correlation_project.get('repository')) + _render_pull_request_block(correlation_project.get('pull_request')) + if correlation.get('related_issue'): + _render_issue_chip(correlation.get('related_issue')) + ui.label(correlation['prompt_text']).classes('factory-code q-mt-sm') + if correlation.get('revert'): + ui.label(f"Reverted by commit {correlation['revert'].get('revert_commit_hash', 'unknown')[:12]}").classes('factory-chip') + ui.label('Commits').style('font-weight: 700; color: #3a281a; margin-top: 12px;') + _render_commit_list(correlation.get('commits', [])) + ui.label('LLM Trace').style('font-weight: 700; color: #3a281a; margin-top: 12px;') + _render_llm_traces(filtered_traces) + ui.label('File Diffs').style('font-weight: 700; color: #3a281a; margin-top: 12px;') + _render_change_list(correlation['changes']) + else: + ui.label('No prompt traces recorded yet.').classes('factory-muted') + + @ui.refreshable + def render_compare_panel() -> None: + view_model = _view_model() + if view_model.get('error'): + _render_dashboard_unavailable(view_model['error']) + return + correlations = view_model['correlations'] + project_repository_map = view_model['project_repository_map'] + llm_stage_options = view_model['llm_stage_options'] + llm_model_options = view_model['llm_model_options'] + llm_stage_filter = view_model['llm_stage_filter'] + llm_model_filter = view_model['llm_model_filter'] + llm_search_filter = view_model['llm_search_filter'] + with ui.card().classes('factory-panel q-pa-lg'): + ui.label('Prompt Compare View').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;') + ui.label('Review one prompt at a time as a complete change set: repo diagnostics, commit links, and file-level diffs in one place.').classes('factory-muted') + with ui.row().classes('items-center gap-3 q-mt-md w-full'): + ui.select(options=llm_stage_options, value=llm_stage_filter, on_change=_store_llm_stage, label='LLM stage').classes('min-w-[12rem]') + ui.select(options=llm_model_options, value=llm_model_filter, on_change=_store_llm_model, label='LLM model').classes('min-w-[12rem]') + ui.input(label='Search trace text', value=llm_search_filter, on_change=_store_llm_search).classes('min-w-[18rem]') + if correlations: + for correlation in correlations: + correlation_project = project_repository_map.get(correlation['project_id'], {}) + filtered_correlation = {**correlation, 'llm_traces': _filter_llm_traces(correlation.get('llm_traces', []), llm_stage_filter, llm_model_filter, llm_search_filter)} + with ui.card().classes('q-pa-md q-mt-md'): + ui.label(correlation_project.get('project_name') or correlation['project_id']).style('font-size: 1rem; font-weight: 700; color: #2f241d;') + _render_repository_block(correlation_project.get('repository')) + _render_pull_request_block(correlation_project.get('pull_request')) + if correlation.get('related_issue'): + _render_issue_chip(correlation.get('related_issue')) + with ui.row().classes('items-center gap-2 q-mt-sm'): + if correlation.get('revert'): + ui.label('Prompt has already been reverted').classes('factory-chip') 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'): - 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.row().classes('items-center gap-2 q-pa-md'): ui.button( - 'Archive', - on_click=lambda _=None, project_id=project['project_id'], project_name=project['project_name']: _render_confirmation_dialog( - 'Archive project?', - f'Archive {project_name}? Archived projects remain visible in the dashboard but are excluded from automation, Telegram routing, sync, and undo actions.', - 'Archive', - lambda: archive_project_action(project_id), - color='warning', - ), - ).props('outline color=warning') - ui.button( - 'Delete', - on_click=lambda _=None, project_id=project['project_id'], project_name=project['project_name']: _render_confirmation_dialog( - 'Delete project permanently?', - f'Delete {project_name}? This removes the local project directory, project traces from the database, and any project-owned remote repository.', - 'Delete Permanently', - lambda: delete_project_action(project_id), - ), + 'Undo This Prompt', + on_click=lambda _=None, project_id=correlation['project_id'], prompt_id=correlation['prompt_id']: undo_prompt_action(project_id, prompt_id), ).props('outline color=negative') - with ui.grid(columns=2).classes('w-full gap-4 q-pa-md'): - with ui.card().classes('q-pa-md'): - ui.label('Repository').style('font-weight: 700; color: #3a281a;') - _render_repository_block(project_bundle.get('repository') or project.get('repository')) - ui.button( - 'Sync Repo Activity', - on_click=lambda _=None, project_id=project['project_id']: sync_project_repository_action(project_id), - ).props('outline color=secondary').classes('q-mt-md') + _render_prompt_compare(filtered_correlation) + else: + ui.label('No prompt compare data recorded yet.').classes('factory-muted') - with ui.tab_panel('archived'): - if not archived_projects: - with ui.card().classes('factory-panel q-pa-lg'): - ui.label('No archived projects yet.').classes('factory-muted') - for project_bundle in archived_projects: - project = project_bundle['project'] - with ui.expansion(f"{project['project_name']} · archived", icon='archive').classes('factory-panel w-full q-mb-md'): - with ui.row().classes('items-center gap-2 q-pa-md'): - ui.button( - 'Restore', - on_click=lambda _=None, project_id=project['project_id'], project_name=project['project_name']: _render_confirmation_dialog( - 'Restore archived project?', - f'Restore {project_name} to the active project set so the factory can work on it again?', - 'Restore Project', - lambda: unarchive_project_action(project_id), - color='positive', - ), - ).props('outline color=positive') - ui.button( - 'Delete Permanently', - on_click=lambda _=None, project_id=project['project_id'], project_name=project['project_name']: _render_confirmation_dialog( - 'Delete archived project permanently?', - f'Delete {project_name}? This removes the archived project from both the database and filesystem, and deletes any project-owned remote repository.', - 'Delete Permanently', - lambda: delete_project_action(project_id), - ), - ).props('outline color=negative') - with ui.grid(columns=2).classes('w-full gap-4 q-pa-md'): - with ui.card().classes('q-pa-md'): - ui.label('Repository').style('font-weight: 700; color: #3a281a;') - _render_repository_block(project_bundle.get('repository') or project.get('repository')) - with ui.card().classes('q-pa-md'): - ui.label('Prompt').style('font-weight: 700; color: #3a281a;') - prompts = project_bundle.get('prompts', []) - if prompts: - ui.label(prompts[0]['prompt_text']).classes('factory-code') + @ui.refreshable + def render_timeline_panel() -> None: + view_model = _view_model() + if view_model.get('error'): + _render_dashboard_unavailable(view_model['error']) + return + projects = view_model['projects'] + branch_scope_filter = view_model['branch_scope_filter'] + commit_lookup_query = view_model['commit_lookup_query'] + commit_context = view_model['commit_context'] + with ui.card().classes('factory-panel q-pa-lg q-mb-md'): + ui.label('Commit Lookup').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;') + ui.label('Submit a commit id to reconstruct the prompt, traces, repository state, and surrounding timeline that produced it.').classes('factory-muted') + with ui.row().classes('items-center gap-3 q-mt-md w-full'): + ui.select(options=['', 'main', 'pr', 'manual'], value=branch_scope_filter, on_change=_store_branch_scope, label='Branch scope').classes('min-w-[10rem]') + ui.input(label='Commit hash', value=commit_lookup_query, on_change=_store_commit_lookup, placeholder='deadbeef').classes('min-w-[18rem]') + ui.button('Lookup', on_click=_refresh_timeline_sections).props('unelevated color=dark') + if commit_lookup_query and commit_context is None: + ui.label('No recorded context found for that commit hash.').classes('factory-muted q-mt-md') + elif commit_context is not None: + _render_commit_context(commit_context) + + with ui.card().classes('factory-panel q-pa-lg'): + ui.label('Project Timelines').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;') + ui.label('Chronological view of prompts, LLM traces, commits, PR updates, repository sync events, and prompt reverts.').classes('factory-muted') + with ui.row().classes('items-center gap-3 q-mt-md w-full'): + ui.select(options=['', 'main', 'pr', 'manual'], value=branch_scope_filter, on_change=_store_branch_scope, label='Branch scope').classes('min-w-[10rem]') + if projects: + for project_bundle in projects: + project = project_bundle['project'] + with ui.expansion(f"{project['project_name']} · {project['project_id']}", icon='schedule').classes('q-mt-md w-full'): + _render_timeline(_filter_timeline_events(project_bundle.get('timeline', []), branch_scope_filter)) + else: + ui.label('No project timelines recorded yet.').classes('factory-muted') + + @ui.refreshable + def render_system_panel() -> None: + view_model = _view_model() + if view_model.get('error'): + _render_dashboard_unavailable(view_model['error']) + return + system_logs = view_model['system_logs'] + llm_runtime = view_model['llm_runtime'] + discovered_repositories = view_model['discovered_repositories'] + 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('LLM Runtime').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;') + rows = [ + ('Provider', llm_runtime.get('provider')), + ('Model', llm_runtime.get('model')), + ('Ollama URL', llm_runtime.get('ollama_url')), + ('Tool Context Limit', str(llm_runtime.get('tool_context_limit'))), + ('Max Tool Call Rounds', str(llm_runtime.get('max_tool_call_rounds'))), + ('Live Gitea Tools Configured', 'yes' if llm_runtime.get('gitea_live_tools_configured') else 'no'), + ] + 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 or 'n/a').style('font-weight: 600; color: #3a281a;') + ui.label('Mediated Tools').style('font-weight: 700; color: #3a281a; margin-top: 12px;') + for tool_name in llm_runtime.get('mediated_tools', []): + ui.label(tool_name).classes('factory-chip q-mt-sm') + ui.label('Live Tools').style('font-weight: 700; color: #3a281a; margin-top: 12px;') + for tool_name in llm_runtime.get('live_tools', []): + ui.label(tool_name).classes('factory-chip q-mt-sm') + ui.label('Live Tool Stages').style('font-weight: 700; color: #3a281a; margin-top: 12px;') + live_tools_by_stage = llm_runtime.get('live_tools_by_stage', {}) + for stage_name, stage_tools in live_tools_by_stage.items(): + ui.label(stage_name.replace('_', ' ').title()).classes('factory-muted q-mt-sm') + if stage_tools: + for tool_name in stage_tools: + ui.label(tool_name).classes('factory-chip q-mt-sm') + else: + ui.label('disabled').classes('factory-code q-mt-sm') + if llm_runtime.get('live_tool_stage_tool_map'): + ui.label('Stage Tool Overrides').style('font-weight: 700; color: #3a281a; margin-top: 12px;') + ui.label(json.dumps(llm_runtime.get('live_tool_stage_tool_map'), indent=2, sort_keys=True)).classes('factory-code q-mt-sm') + ui.label('Guardrails').style('font-weight: 700; color: #3a281a; margin-top: 12px;') + for label, text in (llm_runtime.get('guardrails') or {}).items(): + ui.label(label.replace('_', ' ').title()).classes('factory-muted q-mt-sm') + ui.label(text or 'Not configured').classes('factory-code') + system_prompts = llm_runtime.get('system_prompts', {}) + if system_prompts: + ui.label('System Prompts').style('font-weight: 700; color: #3a281a; margin-top: 12px;') + for label, text in system_prompts.items(): + ui.label(label.replace('_', ' ').title()).classes('factory-muted q-mt-sm') + ui.label(text or 'Not configured').classes('factory-code') + with ui.card().classes('factory-panel q-pa-lg'): + ui.label('Repository Onboarding').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;') + ui.label('Discover repositories in the Gitea organization, onboard manually created repos, and import their recent commits into the dashboard.').classes('factory-muted') + with ui.row().classes('items-center gap-3 q-mt-md w-full'): + ui.input(label='Owner / org', value=_selected_repo_owner(), on_change=_store_repo_owner).classes('min-w-[12rem]') + ui.input(label='Repository name', value=_selected_repo_name(), on_change=_store_repo_name).classes('min-w-[14rem]') + ui.button('Discover Repos', on_click=discover_gitea_repositories_action).props('outline color=secondary') + ui.button('Onboard Repo', on_click=lambda: onboard_repository_action(_selected_repo_owner(), _selected_repo_name())).props('unelevated color=dark') + if discovered_repositories: + for repo in discovered_repositories: + with ui.card().classes('q-pa-sm q-mt-md'): + with ui.row().classes('items-center justify-between w-full'): + with ui.column().classes('gap-1'): + ui.label(repo.get('name') or 'unknown').style('font-weight: 700; color: #2f241d;') + ui.label(repo.get('description') or 'No description').classes('factory-muted') + with ui.row().classes('items-center gap-2'): + if repo.get('onboarded'): + ui.label('onboarded').classes('factory-chip') + if repo.get('project_id'): + ui.label(repo['project_id']).classes('factory-chip') else: - ui.label('No prompt 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('Git Commits').style('font-weight: 700; color: #3a281a;') - _render_commit_list(project_bundle.get('commits', [])) - with ui.card().classes('q-pa-md'): - ui.label('Tracked Issues').style('font-weight: 700; color: #3a281a;') - _render_issue_list(project_bundle.get('issues', [])) + ui.button( + 'Onboard', + on_click=lambda _=None, owner=_selected_repo_owner(), repo_name=repo.get('name'): onboard_repository_action(owner, repo_name), + ).props('outline color=secondary') + if repo.get('html_url'): + ui.link(repo['html_url'], repo['html_url'], new_tab=True).classes('factory-code') + else: + ui.label('No discovered repositories loaded yet.').classes('factory-muted q-mt-md') + 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', '/llm/runtime', '/generate', '/projects', '/audit/projects', '/audit/prompts', '/audit/changes', '/audit/issues', + '/audit/commit-context', '/audit/timeline', '/audit/llm-traces', '/audit/correlations', '/projects/{project_id}/sync-repository', + '/gitea/repos', '/gitea/repos/onboard', '/n8n/health', '/n8n/setup', + ] + for endpoint in endpoints: + ui.label(endpoint).classes('factory-code q-mt-sm') - with ui.card().classes('q-pa-md'): - ui.label('Repository Sync').style('font-weight: 700; color: #3a281a;') - _render_repository_sync_block(project_bundle.get('repository_sync') or project.get('repository_sync')) + @ui.refreshable + def render_health_panel() -> None: + with ui.card().classes('factory-panel q-pa-lg q-mb-md'): + ui.label('Health and Diagnostics').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;') + ui.label('Use this page to verify runtime configuration, n8n API connectivity, and likely causes of provisioning failures.').classes('factory-muted') + ui.link('Open dedicated health page', '/health-ui') + with ui.card().classes('factory-panel q-pa-lg q-mb-md'): + ui.label('Telegram Prompt Channel').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;') + ui.label('Send a guide message into the same Telegram chat/channel where the bot is expected to receive prompts.').classes('factory-muted') + with ui.row().classes('justify-between w-full q-mt-sm'): + ui.label('Configured Chat ID').classes('factory-muted') + ui.label(settings.telegram_chat_id or 'Not configured').style('font-weight: 600; color: #3a281a;') + with ui.row().classes('items-center gap-2 q-mt-md'): + ui.button('Send Prompt Guide', on_click=send_telegram_prompt_guide_action).props('unelevated color=secondary') + _render_health_panels() - with ui.card().classes('q-pa-md'): - ui.label('Pull Request').style('font-weight: 700; color: #3a281a;') - open_pr = next((pr for pr in project_bundle.get('pull_requests', []) if pr.get('pr_state') == 'open' and not pr.get('merged')), None) - _render_pull_request_block(open_pr) + panel_refreshers: dict[str, callable] = {} - 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'}") - if prompt.get('related_issue'): - _render_issue_chip(prompt.get('related_issue')) - ui.label(prompt['prompt_text']).classes('factory-code') - else: - ui.label('No prompt recorded.').classes('factory-muted') + def _refresh_current_dashboard_sections() -> None: + _update_dashboard_state() + panel_refreshers['metrics']() + active_tab = _selected_tab_name() + if active_tab in panel_refreshers: + panel_refreshers[active_tab]() - with ui.grid(columns=1).classes('w-full gap-4 q-pa-md'): - with ui.card().classes('q-pa-md'): - ui.label('Generated Changes').style('font-weight: 700; color: #3a281a;') - _render_change_list(project_bundle.get('code_changes', [])) + def _refresh_all_dashboard_sections() -> None: + _update_dashboard_state() + panel_refreshers['metrics']() + for name in ('overview', 'projects', 'archived', 'trace', 'compare', 'timeline', 'system', 'health'): + panel_refreshers[name]() - with ui.card().classes('q-pa-md'): - ui.label('Tracked Issues').style('font-weight: 700; color: #3a281a;') - _render_issue_list(project_bundle.get('issues', [])) + def _refresh_llm_filtered_sections() -> None: + _update_dashboard_state() + for name in ('archived', 'trace', 'compare'): + panel_refreshers[name]() - with ui.grid(columns=2).classes('w-full gap-4 q-pa-md'): - with ui.card().classes('q-pa-md'): - ui.label('Git Commits').style('font-weight: 700; color: #3a281a;') - _render_commit_list(project_bundle.get('commits', [])) + def _refresh_timeline_sections() -> None: + _update_dashboard_state() + panel_refreshers['timeline']() - with ui.card().classes('q-pa-md'): - ui.label('LLM Trace').style('font-weight: 700; color: #3a281a;') - _render_llm_traces(_filter_llm_traces(project_bundle.get('llm_traces', []), llm_stage_filter, llm_model_filter, llm_search_filter)) + def _refresh_system_sections() -> None: + _update_dashboard_state() + panel_refreshers['system']() - 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') + def _refresh_health_sections() -> None: + panel_refreshers['health']() - with ui.grid(columns=1).classes('w-full gap-4 q-pa-md'): - with ui.card().classes('q-pa-md'): - ui.label('Issue Work').style('font-weight: 700; color: #3a281a;') - _render_issue_work_events(project_bundle.get('issue_work', [])) + _update_dashboard_state() - 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.column().classes('factory-shell w-full gap-4 q-pa-lg'): + render_header() + render_metrics() - with ui.tab_panel('trace'): - 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') - with ui.row().classes('items-center gap-3 q-mt-md w-full'): - ui.select( - options=llm_stage_options, - value=llm_stage_filter, - on_change=_store_llm_stage, - label='LLM stage', - ).classes('min-w-[12rem]') - ui.select( - options=llm_model_options, - value=llm_model_filter, - on_change=_store_llm_model, - label='LLM model', - ).classes('min-w-[12rem]') - ui.input( - label='Search trace text', - value=llm_search_filter, - on_change=_store_llm_search, - ).classes('min-w-[18rem]') - if correlations: - for correlation in correlations: - correlation_project = project_repository_map.get(correlation['project_id'], {}) - filtered_traces = _filter_llm_traces(correlation.get('llm_traces', []), llm_stage_filter, llm_model_filter, llm_search_filter) - with ui.card().classes('q-pa-md q-mt-md'): - ui.label(correlation_project.get('project_name') or correlation['project_id']).style('font-size: 1rem; font-weight: 700; color: #2f241d;') - _render_repository_block(correlation_project.get('repository')) - _render_pull_request_block(correlation_project.get('pull_request')) - if correlation.get('related_issue'): - _render_issue_chip(correlation.get('related_issue')) - ui.label(correlation['prompt_text']).classes('factory-code q-mt-sm') - if correlation.get('revert'): - ui.label(f"Reverted by commit {correlation['revert'].get('revert_commit_hash', 'unknown')[:12]}").classes('factory-chip') - ui.label('Commits').style('font-weight: 700; color: #3a281a; margin-top: 12px;') - _render_commit_list(correlation.get('commits', [])) - ui.label('LLM Trace').style('font-weight: 700; color: #3a281a; margin-top: 12px;') - _render_llm_traces(filtered_traces) - ui.label('File Diffs').style('font-weight: 700; color: #3a281a; margin-top: 12px;') - _render_change_list(correlation['changes']) - else: - ui.label('No prompt traces recorded yet.').classes('factory-muted') + selected_tab = _selected_tab_name() + with ui.tabs(value=selected_tab, on_change=_store_selected_tab).classes('w-full') as tabs: + ui.tab('Overview').props('name=overview') + ui.tab('Projects').props('name=projects') + ui.tab('Archived').props('name=archived') + ui.tab('Prompt Trace').props('name=trace') + ui.tab('Compare').props('name=compare') + ui.tab('Timeline').props('name=timeline') + ui.tab('System').props('name=system') + ui.tab('Health').props('name=health') - with ui.tab_panel('compare'): - with ui.card().classes('factory-panel q-pa-lg'): - ui.label('Prompt Compare View').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;') - ui.label('Review one prompt at a time as a complete change set: repo diagnostics, commit links, and file-level diffs in one place.').classes('factory-muted') - with ui.row().classes('items-center gap-3 q-mt-md w-full'): - ui.select( - options=llm_stage_options, - value=llm_stage_filter, - on_change=_store_llm_stage, - label='LLM stage', - ).classes('min-w-[12rem]') - ui.select( - options=llm_model_options, - value=llm_model_filter, - on_change=_store_llm_model, - label='LLM model', - ).classes('min-w-[12rem]') - ui.input( - label='Search trace text', - value=llm_search_filter, - on_change=_store_llm_search, - ).classes('min-w-[18rem]') - if correlations: - for correlation in correlations: - correlation_project = project_repository_map.get(correlation['project_id'], {}) - correlation = { - **correlation, - 'llm_traces': _filter_llm_traces(correlation.get('llm_traces', []), llm_stage_filter, llm_model_filter, llm_search_filter), - } - with ui.card().classes('q-pa-md q-mt-md'): - ui.label(correlation_project.get('project_name') or correlation['project_id']).style('font-size: 1rem; font-weight: 700; color: #2f241d;') - _render_repository_block(correlation_project.get('repository')) - _render_pull_request_block(correlation_project.get('pull_request')) - if correlation.get('related_issue'): - _render_issue_chip(correlation.get('related_issue')) - with ui.row().classes('items-center gap-2 q-mt-sm'): - if correlation.get('revert'): - ui.label('Prompt has already been reverted').classes('factory-chip') - else: - ui.button( - 'Undo This Prompt', - on_click=lambda _=None, project_id=correlation['project_id'], prompt_id=correlation['prompt_id']: undo_prompt_action(project_id, prompt_id), - ).props('outline color=negative') - _render_prompt_compare(correlation) - else: - ui.label('No prompt compare data recorded yet.').classes('factory-muted') + with ui.tab_panels(tabs, value=selected_tab).classes('w-full'): + with ui.tab_panel('overview'): + render_overview_panel() + with ui.tab_panel('projects'): + render_projects_panel() + with ui.tab_panel('archived'): + render_archived_panel() + with ui.tab_panel('trace'): + render_trace_panel() + with ui.tab_panel('compare'): + render_compare_panel() + with ui.tab_panel('timeline'): + render_timeline_panel() + with ui.tab_panel('system'): + render_system_panel() + with ui.tab_panel('health'): + render_health_panel() - with ui.tab_panel('timeline'): - with ui.card().classes('factory-panel q-pa-lg q-mb-md'): - ui.label('Commit Lookup').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;') - ui.label('Submit a commit id to reconstruct the prompt, traces, repository state, and surrounding timeline that produced it.').classes('factory-muted') - with ui.row().classes('items-center gap-3 q-mt-md w-full'): - ui.select( - options=['', 'main', 'pr', 'manual'], - value=branch_scope_filter, - on_change=_store_branch_scope, - label='Branch scope', - ).classes('min-w-[10rem]') - ui.input( - label='Commit hash', - value=commit_lookup_query, - on_change=_store_commit_lookup, - placeholder='deadbeef', - ).classes('min-w-[18rem]') - ui.button('Lookup', on_click=dashboard_body.refresh).props('unelevated color=dark') - if commit_lookup_query and commit_context is None: - ui.label('No recorded context found for that commit hash.').classes('factory-muted q-mt-md') - elif commit_context is not None: - _render_commit_context(commit_context) + panel_refreshers.update({ + 'header': render_header.refresh, + 'metrics': render_metrics.refresh, + 'overview': render_overview_panel.refresh, + 'projects': render_projects_panel.refresh, + 'archived': render_archived_panel.refresh, + 'trace': render_trace_panel.refresh, + 'compare': render_compare_panel.refresh, + 'timeline': render_timeline_panel.refresh, + 'system': render_system_panel.refresh, + 'health': render_health_panel.refresh, + }) - with ui.card().classes('factory-panel q-pa-lg'): - ui.label('Project Timelines').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;') - ui.label('Chronological view of prompts, LLM traces, commits, PR updates, repository sync events, and prompt reverts.').classes('factory-muted') - with ui.row().classes('items-center gap-3 q-mt-md w-full'): - ui.select( - options=['', 'main', 'pr', 'manual'], - value=branch_scope_filter, - on_change=_store_branch_scope, - label='Branch scope', - ).classes('min-w-[10rem]') - if projects: - for project_bundle in projects: - project = project_bundle['project'] - with ui.expansion(f"{project['project_name']} · {project['project_id']}", icon='schedule').classes('q-mt-md w-full'): - _render_timeline(_filter_timeline_events(project_bundle.get('timeline', []), branch_scope_filter)) - else: - ui.label('No project timelines recorded yet.').classes('factory-muted') - - with ui.tab_panel('system'): - 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('LLM Runtime').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;') - rows = [ - ('Provider', llm_runtime.get('provider')), - ('Model', llm_runtime.get('model')), - ('Ollama URL', llm_runtime.get('ollama_url')), - ('Tool Context Limit', str(llm_runtime.get('tool_context_limit'))), - ('Max Tool Call Rounds', str(llm_runtime.get('max_tool_call_rounds'))), - ('Live Gitea Tools Configured', 'yes' if llm_runtime.get('gitea_live_tools_configured') else 'no'), - ] - 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 or 'n/a').style('font-weight: 600; color: #3a281a;') - ui.label('Mediated Tools').style('font-weight: 700; color: #3a281a; margin-top: 12px;') - for tool_name in llm_runtime.get('mediated_tools', []): - ui.label(tool_name).classes('factory-chip q-mt-sm') - ui.label('Live Tools').style('font-weight: 700; color: #3a281a; margin-top: 12px;') - for tool_name in llm_runtime.get('live_tools', []): - ui.label(tool_name).classes('factory-chip q-mt-sm') - ui.label('Live Tool Stages').style('font-weight: 700; color: #3a281a; margin-top: 12px;') - live_tools_by_stage = llm_runtime.get('live_tools_by_stage', {}) - for stage_name, stage_tools in live_tools_by_stage.items(): - ui.label(stage_name.replace('_', ' ').title()).classes('factory-muted q-mt-sm') - if stage_tools: - for tool_name in stage_tools: - ui.label(tool_name).classes('factory-chip q-mt-sm') - else: - ui.label('disabled').classes('factory-code q-mt-sm') - if llm_runtime.get('live_tool_stage_tool_map'): - ui.label('Stage Tool Overrides').style('font-weight: 700; color: #3a281a; margin-top: 12px;') - ui.label(json.dumps(llm_runtime.get('live_tool_stage_tool_map'), indent=2, sort_keys=True)).classes('factory-code q-mt-sm') - ui.label('Guardrails').style('font-weight: 700; color: #3a281a; margin-top: 12px;') - guardrails = llm_runtime.get('guardrails', {}) - for label, text in guardrails.items(): - ui.label(label.replace('_', ' ').title()).classes('factory-muted q-mt-sm') - ui.label(text or 'Not configured').classes('factory-code') - system_prompts = llm_runtime.get('system_prompts', {}) - if system_prompts: - ui.label('System Prompts').style('font-weight: 700; color: #3a281a; margin-top: 12px;') - for label, text in system_prompts.items(): - ui.label(label.replace('_', ' ').title()).classes('factory-muted q-mt-sm') - ui.label(text or 'Not configured').classes('factory-code') - - with ui.card().classes('factory-panel q-pa-lg'): - ui.label('Repository Onboarding').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;') - ui.label('Discover repositories in the Gitea organization, onboard manually created repos, and import their recent commits into the dashboard.').classes('factory-muted') - with ui.row().classes('items-center gap-3 q-mt-md w-full'): - ui.input( - label='Owner / org', - value=_selected_repo_owner(), - on_change=_store_repo_owner, - ).classes('min-w-[12rem]') - ui.input( - label='Repository name', - value=_selected_repo_name(), - on_change=_store_repo_name, - ).classes('min-w-[14rem]') - ui.button('Discover Repos', on_click=discover_gitea_repositories_action).props('outline color=secondary') - ui.button( - 'Onboard Repo', - on_click=lambda: onboard_repository_action(_selected_repo_owner(), _selected_repo_name()), - ).props('unelevated color=dark') - if discovered_repositories: - for repo in discovered_repositories: - with ui.card().classes('q-pa-sm q-mt-md'): - with ui.row().classes('items-center justify-between w-full'): - with ui.column().classes('gap-1'): - ui.label(repo.get('name') or 'unknown').style('font-weight: 700; color: #2f241d;') - ui.label(repo.get('description') or 'No description').classes('factory-muted') - with ui.row().classes('items-center gap-2'): - if repo.get('onboarded'): - ui.label('onboarded').classes('factory-chip') - if repo.get('project_id'): - ui.label(repo['project_id']).classes('factory-chip') - else: - ui.button( - 'Onboard', - on_click=lambda _=None, owner=_selected_repo_owner(), repo_name=repo.get('name'): onboard_repository_action(owner, repo_name), - ).props('outline color=secondary') - if repo.get('html_url'): - ui.link(repo['html_url'], repo['html_url'], new_tab=True).classes('factory-code') - else: - ui.label('No discovered repositories loaded yet.').classes('factory-muted q-mt-md') - - 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', - '/llm/runtime', - '/generate', - '/projects', - '/audit/projects', - '/audit/prompts', - '/audit/changes', - '/audit/issues', - '/audit/commit-context', - '/audit/timeline', - '/audit/llm-traces', - '/audit/correlations', - '/projects/{project_id}/sync-repository', - '/gitea/repos', - '/gitea/repos/onboard', - '/n8n/health', - '/n8n/setup', - ] - for endpoint in endpoints: - ui.label(endpoint).classes('factory-code q-mt-sm') - - with ui.tab_panel('health'): - with ui.card().classes('factory-panel q-pa-lg q-mb-md'): - ui.label('Health and Diagnostics').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;') - ui.label('Use this page to verify runtime configuration, n8n API connectivity, and likely causes of provisioning failures.').classes('factory-muted') - ui.link('Open dedicated health page', '/health-ui') - with ui.card().classes('factory-panel q-pa-lg q-mb-md'): - ui.label('Telegram Prompt Channel').style('font-size: 1.25rem; font-weight: 700; color: #3a281a;') - ui.label('Send a guide message into the same Telegram chat/channel where the bot is expected to receive prompts.').classes('factory-muted') - with ui.row().classes('justify-between w-full q-mt-sm'): - ui.label('Configured Chat ID').classes('factory-muted') - ui.label(settings.telegram_chat_id or 'Not configured').style('font-weight: 600; color: #3a281a;') - with ui.row().classes('items-center gap-2 q-mt-md'): - ui.button('Send Prompt Guide', on_click=send_telegram_prompt_guide_action).props('unelevated color=secondary') - _render_health_panels() - - dashboard_body() ui.timer(15.0, _run_background_repository_sync) - ui.timer(10.0, dashboard_body.refresh) + ui.timer(10.0, _refresh_current_dashboard_sections) def run_app(port=None, reload=False, browser=True, storage_secret=None):