From 19e5be7e5a915e4d6d98b85b1cf1473662d5ff05 Mon Sep 17 00:00:00 2001 From: rob Date: Tue, 13 Jan 2026 19:53:40 -0400 Subject: [PATCH] Fix category display and tool counts in web UI - Categories API now includes dynamic categories from database (categories used by tools but not in predefined list) - Add total_tools to categories API meta for accurate All Tools count - Fix web routes to use total_tools instead of summing category counts - Dynamic categories get auto-generated descriptions This fixes: - "All Tools" showing 2 instead of 4 - Categories like "code-analysis" and "education" not appearing - Incorrect category counts in sidebar Co-Authored-By: Claude Opus 4.5 --- src/cmdforge/registry/app.py | 30 +++++++++++++++++++++++++++--- src/cmdforge/web/routes.py | 18 ++++++++++-------- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/cmdforge/registry/app.py b/src/cmdforge/registry/app.py index 637ad1e..ab2ff5e 100644 --- a/src/cmdforge/registry/app.py +++ b/src/cmdforge/registry/app.py @@ -850,17 +850,26 @@ def create_app() -> Flask: categories_yaml = get_repo_dir() / "categories" / "categories.yaml" if categories_yaml.exists(): categories_payload = yaml.safe_load(categories_yaml.read_text(encoding="utf-8")) or {} - categories = (categories_payload or {}).get("categories", []) + predefined_categories = (categories_payload or {}).get("categories", []) + + # Get counts for all categories in the database counts = query_all( g.db, "SELECT category, COUNT(DISTINCT owner || '/' || name) AS total FROM tools GROUP BY category", ) count_map = {row["category"]: row["total"] for row in counts} + + # Calculate total tools across all categories + total_tools = sum(row["total"] for row in counts) + + # Build data from predefined categories + predefined_names = set() data = [] - for cat in categories: + for cat in predefined_categories: name = cat.get("name") if not name: continue + predefined_names.add(name) data.append({ "name": name, "description": cat.get("description"), @@ -868,6 +877,18 @@ def create_app() -> Flask: "tool_count": count_map.get(name, 0), }) + # Add any categories from database that aren't in predefined list + for category_name, count in count_map.items(): + if category_name and category_name not in predefined_names: + # Auto-generate display info for dynamic categories + display_name = category_name.replace("-", " ").title() + data.append({ + "name": category_name, + "description": f"Tools in the {display_name} category", + "icon": None, + "tool_count": count, + }) + reverse = order == "desc" if sort == "tool_count": data.sort(key=lambda item: item["tool_count"], reverse=reverse) @@ -879,7 +900,10 @@ def create_app() -> Flask: end = start + per_page sliced = data[start:end] - response = jsonify({"data": sliced, "meta": paginate(page, per_page, total)}) + meta = paginate(page, per_page, total) + meta["total_tools"] = total_tools # Add total tools count to meta + + response = jsonify({"data": sliced, "meta": meta}) response.headers["Cache-Control"] = "max-age=3600" return response diff --git a/src/cmdforge/web/routes.py b/src/cmdforge/web/routes.py index 0c4f7d4..66245ec 100644 --- a/src/cmdforge/web/routes.py +++ b/src/cmdforge/web/routes.py @@ -65,10 +65,11 @@ def _render_readme(readme: Optional[str]) -> str: return Markup("
") + escaped + Markup("
") -def _load_categories() -> List[SimpleNamespace]: +def _load_categories() -> Tuple[List[SimpleNamespace], int]: + """Load categories with their tool counts. Returns (categories, total_tools).""" status, payload = _api_get("/api/v1/categories", params={"per_page": 100}) if status != 200: - return [] + return [], 0 categories = [] for item in payload.get("data", []): name = item.get("name") @@ -81,7 +82,9 @@ def _load_categories() -> List[SimpleNamespace]: description=item.get("description"), icon=item.get("icon"), )) - return categories + # Get total tools from meta (includes all categories, even dynamic ones) + total_tools = payload.get("meta", {}).get("total_tools", sum(c.count for c in categories)) + return categories, total_tools def _load_publisher(slug: str) -> Optional[Dict[str, Any]]: @@ -233,9 +236,7 @@ def _render_tools(category_override: Optional[str] = None): return render_template("errors/500.html"), 500 meta = payload.get("meta", {}) - categories = _load_categories() - # Sum all category counts for the "All Tools" total - all_tools_total = sum(c.count for c in categories) + categories, all_tools_total = _load_categories() return render_template( "pages/tools.html", tools=payload.get("data", []), @@ -320,13 +321,14 @@ def search(): active_min_downloads=min_downloads, ) - categories = sorted(_load_categories(), key=lambda c: c.count, reverse=True)[:8] + categories, _ = _load_categories() + popular_categories = sorted(categories, key=lambda c: c.count, reverse=True)[:8] return render_template( "pages/search.html", query="", results=[], pagination=None, - popular_categories=categories, + popular_categories=popular_categories, facets={}, active_categories=[], active_tags=[],