|
6 | 6 |
|
7 | 7 | import json |
8 | 8 | import time |
| 9 | +from datetime import date |
9 | 10 | from pathlib import Path |
10 | 11 | from typing import Any |
11 | 12 |
|
12 | 13 | from .lane_registry import get_route_add_recommendations |
13 | 14 | from .metrics import MetricsStore |
14 | | -from .provider_catalog import build_provider_refresh_guidance |
| 15 | +from .provider_catalog import ( |
| 16 | + build_provider_refresh_guidance, |
| 17 | + get_offerings_catalog, |
| 18 | + get_packages_catalog, |
| 19 | +) |
15 | 20 | from .provider_catalog_refresh import ( |
16 | 21 | build_catalog_alert_summary, |
17 | 22 | build_catalog_alerts, |
@@ -209,6 +214,100 @@ def _provider_catalog_summary(db_path: str) -> dict[str, Any]: |
209 | 214 | store.close() |
210 | 215 |
|
211 | 216 |
|
| 217 | +def _metadata_catalogs_summary() -> dict[str, Any]: |
| 218 | + """Return summary statistics for offerings and packages catalogs.""" |
| 219 | + offerings = get_offerings_catalog() |
| 220 | + packages = get_packages_catalog() |
| 221 | + |
| 222 | + # Count offerings by freshness |
| 223 | + freshness_counts = {"fresh": 0, "aging": 0, "stale": 0, "unknown": 0} |
| 224 | + for offering in offerings.values(): |
| 225 | + pricing = offering.get("pricing", {}) |
| 226 | + freshness = pricing.get("freshness_status", "unknown") |
| 227 | + if freshness in freshness_counts: |
| 228 | + freshness_counts[freshness] += 1 |
| 229 | + else: |
| 230 | + freshness_counts["unknown"] += 1 |
| 231 | + |
| 232 | + # Count packages by type and expiry |
| 233 | + package_types = {} |
| 234 | + expiring_soon = 0 |
| 235 | + today = date.today() |
| 236 | + for package in packages.values(): |
| 237 | + pkg_type = package.get("type", "unknown") |
| 238 | + package_types[pkg_type] = package_types.get(pkg_type, 0) + 1 |
| 239 | + |
| 240 | + # Check expiry |
| 241 | + expiry_str = package.get("expiry_date") |
| 242 | + if expiry_str: |
| 243 | + try: |
| 244 | + expiry_date = date.fromisoformat(expiry_str) |
| 245 | + days_left = (expiry_date - today).days |
| 246 | + if 0 <= days_left <= 7: |
| 247 | + expiring_soon += 1 |
| 248 | + except ValueError: |
| 249 | + pass |
| 250 | + |
| 251 | + return { |
| 252 | + "offerings": { |
| 253 | + "total": len(offerings), |
| 254 | + "freshness": freshness_counts, |
| 255 | + }, |
| 256 | + "packages": { |
| 257 | + "total": len(packages), |
| 258 | + "types": package_types, |
| 259 | + "expiring_soon": expiring_soon, |
| 260 | + }, |
| 261 | + } |
| 262 | + |
| 263 | + |
| 264 | +def _metadata_packages_detail() -> list[dict[str, Any]]: |
| 265 | + """Return detailed package information for dashboard.""" |
| 266 | + packages = get_packages_catalog() |
| 267 | + today = date.today() |
| 268 | + result = [] |
| 269 | + for package in packages.values(): |
| 270 | + expiry_str = package.get("expiry_date") |
| 271 | + days_left = None |
| 272 | + if expiry_str: |
| 273 | + try: |
| 274 | + expiry_date = date.fromisoformat(expiry_str) |
| 275 | + days_left = (expiry_date - today).days |
| 276 | + except ValueError: |
| 277 | + pass |
| 278 | + total = package.get("total_credits") |
| 279 | + used = package.get("used_credits", 0) |
| 280 | + remaining = total - used if total is not None else None |
| 281 | + remaining_pct = (remaining / total * 100) if total and total > 0 else 0 |
| 282 | + result.append( |
| 283 | + { |
| 284 | + "package_id": package.get("package_id"), |
| 285 | + "provider_id": package.get("provider_id"), |
| 286 | + "name": package.get("name"), |
| 287 | + "type": package.get("type"), |
| 288 | + "total_credits": total, |
| 289 | + "used_credits": used, |
| 290 | + "remaining_credits": remaining, |
| 291 | + "remaining_pct": remaining_pct, |
| 292 | + "expiry_date": expiry_str, |
| 293 | + "days_left": days_left, |
| 294 | + "currency": package.get("currency"), |
| 295 | + "price": package.get("price"), |
| 296 | + "renewal_policy": package.get("renewal_policy"), |
| 297 | + "notes": package.get("notes"), |
| 298 | + } |
| 299 | + ) |
| 300 | + # Sort by expiry (soonest first, then no expiry), then by remaining percentage (lowest first) |
| 301 | + result.sort( |
| 302 | + key=lambda p: ( |
| 303 | + 0 if p["days_left"] is not None and p["days_left"] >= 0 else 1, |
| 304 | + p["days_left"] if p["days_left"] is not None else float("inf"), |
| 305 | + p["remaining_pct"] if p["remaining_pct"] is not None else float("inf"), |
| 306 | + ) |
| 307 | + ) |
| 308 | + return result |
| 309 | + |
| 310 | + |
212 | 311 | def _lane_family_summary( |
213 | 312 | provider_rows: list[dict[str, Any]], |
214 | 313 | provider_map: dict[str, dict[str, Any]], |
@@ -1029,6 +1128,15 @@ def build_dashboard_report( |
1029 | 1128 | "review_now": _safe_int(provider_catalog_alert_summary.get("review_now")), |
1030 | 1129 | "inspect": _safe_int(provider_catalog_alert_summary.get("inspect")), |
1031 | 1130 | }, |
| 1131 | + "metadata_catalogs": { |
| 1132 | + "offerings_total": _safe_int(_metadata_catalogs_summary()["offerings"]["total"]), |
| 1133 | + "offerings_fresh": _safe_int(_metadata_catalogs_summary()["offerings"]["freshness"]["fresh"]), |
| 1134 | + "offerings_aging": _safe_int(_metadata_catalogs_summary()["offerings"]["freshness"]["aging"]), |
| 1135 | + "offerings_stale": _safe_int(_metadata_catalogs_summary()["offerings"]["freshness"]["stale"]), |
| 1136 | + "packages_total": _safe_int(_metadata_catalogs_summary()["packages"]["total"]), |
| 1137 | + "packages_expiring_soon": _safe_int(_metadata_catalogs_summary()["packages"]["expiring_soon"]), |
| 1138 | + "packages_detail": _metadata_packages_detail(), |
| 1139 | + }, |
1032 | 1140 | "drivers": { |
1033 | 1141 | "top_provider": top_provider, |
1034 | 1142 | "top_cost_provider": top_provider_cost, |
@@ -1125,6 +1233,42 @@ def _render_overview(report: dict[str, Any]) -> str: |
1125 | 1233 | f" Recent changes {_safe_int(report['cards']['provider_catalog']['recent_changes'])}", |
1126 | 1234 | ] |
1127 | 1235 | ) |
| 1236 | + |
| 1237 | + # Add metadata catalogs section |
| 1238 | + lines.extend( |
| 1239 | + [ |
| 1240 | + "", |
| 1241 | + "Metadata catalogs", |
| 1242 | + f" Offerings {_safe_int(report['cards']['metadata_catalogs']['offerings_total'])} total ({_safe_int(report['cards']['metadata_catalogs']['offerings_fresh'])} fresh)", |
| 1243 | + f" Packages {_safe_int(report['cards']['metadata_catalogs']['packages_total'])} total ({_safe_int(report['cards']['metadata_catalogs']['packages_expiring_soon'])} expiring soon)", |
| 1244 | + ] |
| 1245 | + ) |
| 1246 | + # Add package details if any packages exist |
| 1247 | + packages_detail = report["cards"]["metadata_catalogs"].get("packages_detail", []) |
| 1248 | + if packages_detail: |
| 1249 | + # Show top 3 packages (sorted by expiry and remaining) |
| 1250 | + lines.append("") |
| 1251 | + lines.append(" Package details") |
| 1252 | + for pkg in packages_detail[:3]: |
| 1253 | + provider = pkg.get("provider_id", "unknown") |
| 1254 | + name = pkg.get("name", "unnamed") |
| 1255 | + total = pkg.get("total_credits") |
| 1256 | + remaining = pkg.get("remaining_credits") |
| 1257 | + expiry = pkg.get("expiry_date") |
| 1258 | + days_left = pkg.get("days_left") |
| 1259 | + if total is not None and remaining is not None: |
| 1260 | + creds = f"{remaining}/{total}" |
| 1261 | + pct = pkg.get("remaining_pct", 0) |
| 1262 | + creds_line = f"{creds} ({pct:.0f}%)" |
| 1263 | + else: |
| 1264 | + creds_line = "n/a" |
| 1265 | + expiry_line = "" |
| 1266 | + if expiry: |
| 1267 | + if days_left is not None and days_left >= 0: |
| 1268 | + expiry_line = f", expires in {days_left} day{'s' if days_left != 1 else ''}" |
| 1269 | + else: |
| 1270 | + expiry_line = ", expired" |
| 1271 | + lines.append(f" - {provider}: {name} ({creds_line}{expiry_line})") |
1128 | 1272 | if report["alerts"]: |
1129 | 1273 | alert = report["alerts"][0] |
1130 | 1274 | lines.extend( |
|
0 commit comments