diff --git a/wildpc-api/src/wildpc_api/routes.py b/wildpc-api/src/wildpc_api/routes.py index 982d44c..541c326 100644 --- a/wildpc-api/src/wildpc_api/routes.py +++ b/wildpc-api/src/wildpc_api/routes.py @@ -233,6 +233,23 @@ def _make_systemd_info(name: str, timer: bool = False) -> SystemdInfo: return SystemdInfo(unit_name=unit_name, unit_path=unit_path, timer=timer) +def _try_load_config(root: Path | None) -> object | None: + """Load wildpc.yaml for `root`, or None when there's no config to read. + + Listing endpoints call this **once** and reuse the result: load_config() + reparses every program/deployment YAML, so a per-item load turns a listing + into an N x full-config-parse (seconds, not milliseconds). + """ + if root is None: + return None + from wildpc_core.config import load_config + + try: + return load_config(root) + except FileNotFoundError: + return None + + def _backfill_source(name: str, config: object) -> str | None: """Resolve source path from program ref in config.""" if name in config.programs: @@ -429,6 +446,10 @@ def list_services(include_remote: bool = False) -> list[ServiceSummary]: summaries: list[ServiceSummary] = [] seen: set[str] = set() + # One config load for the whole listing — the source backfill and the + # non-deployed pass below both read it. + config = _try_load_config(get_wildpc_root()) + # Services page shows services (systemd) AND statics (caddy) — both are # exposed, URL-reachable "services". Not jobs, tools, or remotes. for _kind, name, deployed in registry.all(): @@ -437,33 +458,19 @@ def list_services(include_remote: bool = False) -> list[ServiceSummary]: s = _service_from_deployed(name, deployed) s.node = hostname # Backfill source - root = get_wildpc_root() - if root and s.source is None: - try: - from wildpc_core.config import load_config - - config = load_config(root) - s.source = _backfill_source(name, config) - except FileNotFoundError: - pass + if config is not None and s.source is None: + s.source = _backfill_source(name, config) summaries.append(s) seen.add(name) # Non-deployed from wildpc.yaml - root = get_wildpc_root() - if root: - try: - from wildpc_core.config import load_config - - config = load_config(root) - for name, svc in config.services.items(): - if name not in seen: - s = _service_from_spec(name, svc, config) - s.node = hostname - summaries.append(s) - seen.add(name) - except FileNotFoundError: - pass + if config is not None: + for name, svc in config.services.items(): + if name not in seen: + s = _service_from_spec(name, svc, config) + s.node = hostname + summaries.append(s) + seen.add(name) # Remote if include_remote: @@ -553,39 +560,28 @@ def list_jobs(include_remote: bool = False) -> list[JobSummary]: summaries: list[JobSummary] = [] seen: set[str] = set() + # One config load for the whole listing (see list_services). + config = _try_load_config(get_wildpc_root()) + # Deployed jobs (scheduled) for _kind, name, deployed in registry.all(): if not deployed.schedule: continue s = _job_from_deployed(name, deployed) s.node = hostname - root = get_wildpc_root() - if root and s.source is None: - try: - from wildpc_core.config import load_config - - config = load_config(root) - s.source = _backfill_source(name, config) - except FileNotFoundError: - pass + if config is not None and s.source is None: + s.source = _backfill_source(name, config) summaries.append(s) seen.add(name) # Non-deployed from wildpc.yaml - root = get_wildpc_root() - if root: - try: - from wildpc_core.config import load_config - - config = load_config(root) - for name, job in config.jobs.items(): - if name not in seen: - s = _job_from_spec(name, job, config) - s.node = hostname - summaries.append(s) - seen.add(name) - except FileNotFoundError: - pass + if config is not None: + for name, job in config.jobs.items(): + if name not in seen: + s = _job_from_spec(name, job, config) + s.node = hostname + summaries.append(s) + seen.add(name) # Remote if include_remote: