| 409 | obj.save() |
| 410 | |
| 411 | def list_plugins(self) -> List[Dict[str, Any]]: |
| 412 | from .models import PluginConfig, PluginRepo |
| 413 | |
| 414 | plugins: List[Dict[str, Any]] = [] |
| 415 | with self._lock: |
| 416 | registry_snapshot = dict(self._registry) |
| 417 | try: |
| 418 | configs = {c.key: c for c in PluginConfig.objects.select_related("source_repo").all()} |
| 419 | except Exception as e: |
| 420 | # Database might not be migrated yet; fall back to registry only |
| 421 | logger.warning("PluginConfig table unavailable; listing registry only: %s", e) |
| 422 | configs = {} |
| 423 | |
| 424 | # Build repo latest-version lookup from cached manifests |
| 425 | repo_latest = {} # slug -> latest_version |
| 426 | try: |
| 427 | for repo in PluginRepo.objects.filter(enabled=True): |
| 428 | manifest_data = repo.cached_manifest or {} |
| 429 | manifest = manifest_data.get("manifest", manifest_data) |
| 430 | for rp in manifest.get("plugins", []): |
| 431 | s = rp.get("slug", "") |
| 432 | if s: |
| 433 | repo_latest[s] = rp.get("latest_version", "") |
| 434 | except Exception: |
| 435 | pass |
| 436 | |
| 437 | # First, include all discovered plugins |
| 438 | for key, lp in registry_snapshot.items(): |
| 439 | conf = configs.get(key) |
| 440 | conf_slug = conf.slug if conf else "" |
| 441 | trusted = bool(conf and (conf.ever_enabled or conf.enabled)) |
| 442 | logo_url = self._get_logo_url(key, path=lp.path) |
| 443 | plugins.append( |
| 444 | { |
| 445 | "key": key, |
| 446 | "name": lp.name, |
| 447 | "version": lp.version, |
| 448 | "description": lp.description, |
| 449 | "author": getattr(lp, "author", "") or "", |
| 450 | "help_url": getattr(lp, "help_url", "") or "", |
| 451 | "enabled": conf.enabled if conf else False, |
| 452 | "ever_enabled": conf.ever_enabled if conf else False, |
| 453 | "fields": lp.fields or [], |
| 454 | "settings": (conf.settings if conf else {}), |
| 455 | "actions": lp.actions or [], |
| 456 | "missing": False, |
| 457 | "trusted": trusted, |
| 458 | "loaded": bool(lp.loaded), |
| 459 | "legacy": bool(getattr(lp, "legacy", False)), |
| 460 | "logo_url": logo_url, |
| 461 | "source_repo": conf.source_repo_id if conf else None, |
| 462 | "source_repo_name": conf.source_repo.name if conf and conf.source_repo else None, |
| 463 | "is_official_repo": bool(conf and conf.source_repo and conf.source_repo.is_official), |
| 464 | "slug": conf_slug, |
| 465 | "is_managed": bool(conf and conf.source_repo_id), |
| 466 | "installed_version_is_prerelease": bool( |
| 467 | conf and conf.installed_version_is_prerelease |
| 468 | ), |