feat: better dashboard reloading mechanism, refs NOISSUE
This commit is contained in:
@@ -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,35 +876,50 @@ 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()
|
||||
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,
|
||||
}
|
||||
projects = snapshot['projects']
|
||||
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 = {
|
||||
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'),
|
||||
@@ -913,20 +928,44 @@ def create_dashboard():
|
||||
}
|
||||
for project_bundle in projects
|
||||
if project_bundle.get('project')
|
||||
},
|
||||
}
|
||||
|
||||
with ui.column().classes('factory-shell w-full gap-4 q-pa-lg'):
|
||||
def _update_dashboard_state() -> None:
|
||||
dashboard_state.clear()
|
||||
dashboard_state.update(_load_dashboard_view_model())
|
||||
|
||||
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=dashboard_body.refresh).props('outline')
|
||||
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'),
|
||||
@@ -941,19 +980,14 @@ def create_dashboard():
|
||||
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;')
|
||||
|
||||
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_panels(tabs, value=selected_tab).classes('w-full'):
|
||||
with ui.tab_panel('overview'):
|
||||
@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;')
|
||||
@@ -983,7 +1017,13 @@ def create_dashboard():
|
||||
ui.label(label).classes('factory-muted')
|
||||
ui.label(value).style('font-weight: 600; color: #3a281a;')
|
||||
|
||||
with ui.tab_panel('projects'):
|
||||
@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')
|
||||
@@ -1019,7 +1059,16 @@ def create_dashboard():
|
||||
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_panel('archived'):
|
||||
@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')
|
||||
@@ -1064,16 +1113,13 @@ def create_dashboard():
|
||||
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', [])
|
||||
@@ -1086,25 +1132,20 @@ def create_dashboard():
|
||||
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]
|
||||
@@ -1113,12 +1154,10 @@ def create_dashboard():
|
||||
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]
|
||||
@@ -1128,28 +1167,26 @@ def create_dashboard():
|
||||
else:
|
||||
ui.label('No audit events yet.').classes('factory-muted')
|
||||
|
||||
with ui.tab_panel('trace'):
|
||||
@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]')
|
||||
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'], {})
|
||||
@@ -1172,35 +1209,30 @@ def create_dashboard():
|
||||
else:
|
||||
ui.label('No prompt traces recorded yet.').classes('factory-muted')
|
||||
|
||||
with ui.tab_panel('compare'):
|
||||
@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]')
|
||||
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),
|
||||
}
|
||||
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'))
|
||||
@@ -1215,28 +1247,27 @@ def create_dashboard():
|
||||
'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)
|
||||
_render_prompt_compare(filtered_correlation)
|
||||
else:
|
||||
ui.label('No prompt compare data recorded yet.').classes('factory-muted')
|
||||
|
||||
with ui.tab_panel('timeline'):
|
||||
@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=dashboard_body.refresh).props('unelevated color=dark')
|
||||
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:
|
||||
@@ -1246,12 +1277,7 @@ def create_dashboard():
|
||||
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]')
|
||||
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']
|
||||
@@ -1260,7 +1286,15 @@ def create_dashboard():
|
||||
else:
|
||||
ui.label('No project timelines recorded yet.').classes('factory-muted')
|
||||
|
||||
with ui.tab_panel('system'):
|
||||
@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;')
|
||||
@@ -1269,7 +1303,6 @@ def create_dashboard():
|
||||
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 = [
|
||||
@@ -1303,8 +1336,7 @@ def create_dashboard():
|
||||
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():
|
||||
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', {})
|
||||
@@ -1313,26 +1345,14 @@ def create_dashboard():
|
||||
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.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')
|
||||
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'):
|
||||
@@ -1354,32 +1374,18 @@ def create_dashboard():
|
||||
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',
|
||||
'/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'):
|
||||
@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')
|
||||
@@ -1394,9 +1400,87 @@ def create_dashboard():
|
||||
ui.button('Send Prompt Guide', on_click=send_telegram_prompt_guide_action).props('unelevated color=secondary')
|
||||
_render_health_panels()
|
||||
|
||||
dashboard_body()
|
||||
panel_refreshers: dict[str, callable] = {}
|
||||
|
||||
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]()
|
||||
|
||||
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]()
|
||||
|
||||
def _refresh_llm_filtered_sections() -> None:
|
||||
_update_dashboard_state()
|
||||
for name in ('archived', 'trace', 'compare'):
|
||||
panel_refreshers[name]()
|
||||
|
||||
def _refresh_timeline_sections() -> None:
|
||||
_update_dashboard_state()
|
||||
panel_refreshers['timeline']()
|
||||
|
||||
def _refresh_system_sections() -> None:
|
||||
_update_dashboard_state()
|
||||
panel_refreshers['system']()
|
||||
|
||||
def _refresh_health_sections() -> None:
|
||||
panel_refreshers['health']()
|
||||
|
||||
_update_dashboard_state()
|
||||
|
||||
with ui.column().classes('factory-shell w-full gap-4 q-pa-lg'):
|
||||
render_header()
|
||||
render_metrics()
|
||||
|
||||
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_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()
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
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):
|
||||
|
||||
Reference in New Issue
Block a user