#!/usr/bin/env python3 """ Dashboard Registry — Project Dashboard Configuration (ADR-170)
Provides lookup functions for dashboard-enabled projects. Used by hooks, commands, and scripts to determine whether a project should trigger dashboard refresh.
Usage: from scripts.core.dashboard_registry import ( get_dashboard_config, is_dashboard_enabled, list_dashboard_projects, )
config = get_dashboard_config("BIO-QMS")
if config:
print(config["generator_script"])
ADR: ADR-170 (Multi-Project Executive Dashboard) Task: J.17.3 Created: 2026-02-16 """
import json import sqlite3 import sys from pathlib import Path from typing import Dict, List, Optional
Add parent for imports
sys.path.insert(0, str(Path(file).resolve().parent.parent.parent)) from scripts.core.paths import get_org_db_path, discover_projects_dir
def get_dashboard_config(project_id: str) -> Optional[Dict]: """ Get dashboard configuration for a project.
Returns the parsed dashboard_config JSON if the project is
dashboard-enabled, or None if not registered or disabled.
Args:
project_id: Project identifier (e.g., "BIO-QMS", "PILOT")
Returns:
Dict with dashboard configuration, or None
"""
db_path = get_org_db_path()
if not db_path.exists():
return None
try:
conn = sqlite3.connect(str(db_path))
cursor = conn.cursor()
# Check if dashboard columns exist
cursor.execute("PRAGMA table_info(projects)")
cols = {row[1] for row in cursor.fetchall()}
if "dashboard_enabled" not in cols:
conn.close()
return None
cursor.execute(
"SELECT dashboard_config FROM projects "
"WHERE project_id = ? AND dashboard_enabled = 1",
(project_id,),
)
row = cursor.fetchone()
conn.close()
if row and row[0]:
config = json.loads(row[0])
config["project_id"] = project_id
return config
return None
except (sqlite3.Error, json.JSONDecodeError):
return None
def is_dashboard_enabled(project_id: str) -> bool: """ Check if a project has dashboard refresh enabled.
Args:
project_id: Project identifier
Returns:
True if dashboard is enabled for this project
"""
return get_dashboard_config(project_id) is not None
def list_dashboard_projects() -> List[Dict]: """ List all dashboard-enabled projects.
Returns:
List of dicts with project_id, name, and dashboard_config
"""
db_path = get_org_db_path()
if not db_path.exists():
return []
try:
conn = sqlite3.connect(str(db_path))
cursor = conn.cursor()
# Check if dashboard columns exist
cursor.execute("PRAGMA table_info(projects)")
cols = {row[1] for row in cursor.fetchall()}
if "dashboard_enabled" not in cols:
conn.close()
return []
cursor.execute(
"SELECT project_id, name, dashboard_config FROM projects "
"WHERE dashboard_enabled = 1 AND status = 'active'"
)
rows = cursor.fetchall()
conn.close()
results = []
for project_id, name, config_json in rows:
config = json.loads(config_json) if config_json else {}
config["project_id"] = project_id
config["name"] = name
results.append(config)
return results
except (sqlite3.Error, json.JSONDecodeError):
return []
def resolve_project_root(config: Dict) -> Optional[Path]: """ Resolve the absolute path to a project root from dashboard config.
The project_root in config may be relative to PROJECTS_DIR.
Args:
config: Dashboard config dict from get_dashboard_config()
Returns:
Absolute Path to project root, or None
"""
project_root = config.get("project_root")
if not project_root:
return None
path = Path(project_root)
if path.is_absolute():
return path if path.exists() else None
# Resolve relative to PROJECTS_DIR
projects_dir = discover_projects_dir()
candidates = [
projects_dir / project_root,
projects_dir / "coditect-rollout-master" / project_root,
]
for candidate in candidates:
if candidate.exists():
return candidate
return None
def resolve_generator_script(config: Dict) -> Optional[Path]: """ Resolve the absolute path to a project's dashboard generator script.
Args:
config: Dashboard config dict from get_dashboard_config()
Returns:
Absolute Path to generator script, or None
"""
project_root = resolve_project_root(config)
if not project_root:
return None
script = config.get("generator_script", "scripts/generate-project-dashboard-data.js")
script_path = project_root / script
return script_path if script_path.exists() else None
def get_manifest_json() -> Dict: """ J.18.5.4: Generate project-manifest.json content from dashboard registry.
Returns a dict conforming to project-manifest-v1.schema.json with all
dashboard-enabled projects listed.
"""
from datetime import datetime, timezone
projects = list_dashboard_projects()
manifest_projects = []
for p in projects:
pid = p["project_id"]
config = get_dashboard_config(pid)
if not config:
continue
manifest_projects.append({
"id": pid,
"name": config.get("name", pid),
"title": config.get("title", pid),
"description": config.get("description", ""),
"jsonFile": f"project-dashboard-data-{pid}.json",
"footer": config.get("footer", f"{pid} | Internal"),
})
default_project = ""
if manifest_projects:
default_project = manifest_projects[0]["id"]
return {
"version": 1,
"generated": datetime.now(timezone.utc).isoformat(),
"defaultProject": default_project,
"projects": manifest_projects,
}
CLI for testing
if name == "main": import argparse
parser = argparse.ArgumentParser(description="Dashboard Registry (ADR-170)")
parser.add_argument("--list", action="store_true", help="List dashboard-enabled projects")
parser.add_argument("--check", metavar="PROJECT_ID", help="Check if project has dashboard")
parser.add_argument("--config", metavar="PROJECT_ID", help="Show dashboard config")
parser.add_argument("--manifest", action="store_true", help="Generate project manifest JSON (J.18.5.4)")
args = parser.parse_args()
if args.list:
projects = list_dashboard_projects()
if projects:
print(f"Dashboard-enabled projects ({len(projects)}):")
for p in projects:
print(f" {p['project_id']}: {p.get('name', 'N/A')}")
else:
print("No dashboard-enabled projects found")
print("Run: python3 scripts/migrations/add_dashboard_columns.py --register-bioqms")
elif args.check:
enabled = is_dashboard_enabled(args.check)
print(f"{args.check}: {'ENABLED' if enabled else 'DISABLED'}")
elif args.config:
config = get_dashboard_config(args.config)
if config:
print(json.dumps(config, indent=2))
else:
print(f"No dashboard config for {args.config}")
elif args.manifest:
manifest = get_manifest_json()
print(json.dumps(manifest, indent=2))
else:
parser.print_help()