Skip to main content

#!/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()