diff --git a/dashboard/osa/index.html b/dashboard/osa/index.html index be9a702..122882c 100644 --- a/dashboard/osa/index.html +++ b/dashboard/osa/index.html @@ -269,6 +269,24 @@ color: #991b1b; } + .section-heading { + color: #1e293b; + margin: 1rem 0 0.5rem; + font-size: 0.95rem; + font-weight: 600; + } + .tool-badge { + display: inline-block; + padding: 0.3rem 0.7rem; + background: #f1f5f9; + border: 1px solid #e2e8f0; + border-radius: 6px; + font-size: 0.82rem; + font-family: monospace; + color: #475569; + margin: 0.2rem; + } + /* Period toggle */ .period-toggle { display: flex; gap: 0.35rem; margin-bottom: 1rem; } .period-btn { @@ -493,6 +511,10 @@ .error-msg { color: #f87171; background: #3b1219; border-color: #5c1d2a; } .site-footer { color: #6b7b92; border-color: #30363d; } .site-footer a { color: #7ba3d4; } + .section-heading { color: #c8d6e8; } + .tool-badge { background: #1c2128; border-color: #30363d; color: #8a9bb5; } + .config-banner-warning { background: #2d1f04; border-color: #78350f; color: #fbbf24; } + .config-banner-error { background: #3b1219; border-color: #5c1d2a; color: #f87171; } } @@ -864,6 +886,8 @@

${safeName.toUpperCase()}

${renderSyncInfo(sync)} + ${renderKnowledgeStats(sync)} + ${renderAvailableTools(summary.available_tools_list)}

Activity

@@ -909,6 +933,7 @@

k[key] > 0) + .map(([key, label]) => ({label, value: k[key]})); + if (items.length === 0) return ''; + const metrics = items.map(i => + `
+
${i.value.toLocaleString()}
+
${escapeHtml(i.label)}
+
` + ).join(''); + return `

Knowledge Base

${metrics}
`; + } + + function renderAvailableTools(tools) { + if (!tools || tools.length === 0) return ''; + const badges = tools.map(t => + `${escapeHtml(t)}` + ).join(''); + return `

Available Tools

${badges}
`; + } + function renderSyncInfo(sync) { if (!sync) return ''; diff --git a/src/api/routers/community.py b/src/api/routers/community.py index a8c4474..e83add3 100644 --- a/src/api/routers/community.py +++ b/src/api/routers/community.py @@ -1375,6 +1375,36 @@ async def community_metrics_public() -> dict[str, Any]: else: result["config_health"] = fallback_health + # Derive available tools from community config + if info.community_config: + try: + config = info.community_config + tools = [] + if config.documentation: + tools.append(f"retrieve_{config.id}_docs") + if config.github and config.github.repos: + tools.append(f"search_{config.id}_discussions") + tools.append(f"list_{config.id}_recent") + if config.citations and (config.citations.queries or config.citations.dois): + tools.append(f"search_{config.id}_papers") + if config.docstrings and config.docstrings.repos: + tools.append(f"search_{config.id}_code_docs") + if config.faq_generation and config.mailman: + tools.append(f"search_{config.id}_faq") + if config.discourse: + tools.append(f"search_{config.id}_forum") + result["available_tools_list"] = tools + except (AttributeError, TypeError) as e: + logger.error( + "Failed to derive tools for community %s: %s", + community_id, + e, + exc_info=True, + ) + result["available_tools_list"] = [] + else: + result["available_tools_list"] = [] + return result @router.get("/metrics/public/usage") diff --git a/src/api/routers/sync.py b/src/api/routers/sync.py index 8537d25..3436e35 100644 --- a/src/api/routers/sync.py +++ b/src/api/routers/sync.py @@ -76,6 +76,17 @@ class SyncItemStatus(BaseModel): """ISO timestamp of the next scheduled run, or None if not scheduled.""" +class KnowledgeStats(BaseModel): + """Counts of items in each knowledge category.""" + + github_items: int = 0 + papers: int = 0 + docstrings: int = 0 + discourse_topics: int = 0 + faq_entries: int = 0 + mailing_list_messages: int = 0 + + class SyncStatusResponse(BaseModel): """Complete sync status response.""" @@ -84,13 +95,17 @@ class SyncStatusResponse(BaseModel): scheduler: SchedulerStatus health: HealthStatus syncs: dict[str, SyncItemStatus] = {} - """Per-sync-type status: github, papers, docstrings, mailman, beps, faq.""" + """Per-sync-type status: github, papers, docstrings, mailman, beps, faq, discourse.""" + knowledge: KnowledgeStats | None = None + """Counts of items in each knowledge category.""" class TriggerRequest(BaseModel): """Request to trigger sync.""" - sync_type: str = "all" # "github", "papers", "docstrings", "mailman", "faq", "beps", or "all" + sync_type: str = ( + "all" # "github", "papers", "docstrings", "mailman", "faq", "beps", "discourse", or "all" + ) class TriggerResponse(BaseModel): @@ -293,7 +308,7 @@ async def get_sync_status( logger.error("Failed to get next run times: %s", e, exc_info=True) # Build per-sync-type status for all known sync types - all_sync_types = ("github", "papers", "docstrings", "mailman", "beps", "faq") + all_sync_types = ("github", "papers", "docstrings", "mailman", "beps", "faq", "discourse") syncs: dict[str, SyncItemStatus] = {} for sync_type in all_sync_types: last_sync = _get_most_recent_sync(metadata, sync_type) @@ -321,6 +336,14 @@ async def get_sync_status( ), health=_calculate_health(metadata), syncs=syncs, + knowledge=KnowledgeStats( + github_items=stats.get("github_total", 0), + papers=stats.get("papers_total", 0), + docstrings=stats.get("docstrings_total", 0), + discourse_topics=stats.get("discourse_total", 0), + faq_entries=stats.get("faq_total", 0), + mailing_list_messages=stats.get("mailing_list_total", 0), + ), ) @@ -334,12 +357,12 @@ async def trigger_sync( Requires API key authentication. Args: - request: Sync type to trigger (one of "github", "papers", "docstrings", "mailman", "faq", "beps", or "all") + request: Sync type to trigger (one of "github", "papers", "docstrings", "mailman", "faq", "beps", "discourse", or "all") Returns: Result of the sync operation """ - valid_types = ("github", "papers", "docstrings", "mailman", "faq", "beps", "all") + valid_types = ("github", "papers", "docstrings", "mailman", "faq", "beps", "discourse", "all") if request.sync_type not in valid_types: raise HTTPException( status_code=400,