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,