Decouple core from plugins; wire widgets hook; drop dead search hook

Architectural pass from the skill review ("plugin is the product" boundary).

Core no longer imports plugin models at module load (was a hard import-time
dependency that broke core if the computers plugin was absent/disabled):
- collector.py, applications.py, reports.py: lazy + guarded imports of the
  computers plugin models. Endpoints that need install-tracking now return 503
  when the plugin is absent instead of failing at import.

Search honors runtime enable/disable:
- search.py: _require_enabled(name) raises ImportError for a disabled plugin,
  so each plugin-scoped block skips it (a disabled plugin's rows leave search).
- Replace hardcoded root/rootpassword employee-DB connection in _search_employees
  with the shared env-backed employee_connection helper.

Plugin hooks (integrating-plugin-hooks: every hook needs a consumer):
- get_dashboard_widgets: add the consumer GET /api/dashboard/widgets (5 plugins
  already implemented the hook; it had none). Skips disabled, isolates in prod.
- get_searchable_fields: REMOVED. Zero plugins implemented it and there was no
  consumer; global search is a core concern over the asset model. Contract
  reduction, __contract_version__ 0.3.0 -> 0.4.0.

Docs/contract: PLUGIN-HOOKS.md (widgets consumer note, searchable-fields removal,
0.4.0), PLUGIN-QUICKSTART.md, ADR-001 hook list. Tests: widgets endpoint
aggregate + disabled-skip; contract tests for the removed/added hooks.

151 tests pass, naming/style green, app boots all 6 plugins.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-06-26 20:00:10 -04:00
parent 6979edfc0c
commit a515c28e3b
12 changed files with 940 additions and 792 deletions

View File

@@ -178,31 +178,3 @@ class BasePlugin(ABC):
}
"""
return []
def get_searchable_fields(self) -> List[Dict]:
"""
Return fields this plugin contributes to global search.
Each field: {
'model': Type, # SQLAlchemy model class
'field': str, # Column name to search
'result_type': str, # Type identifier for search results
'url_template': str, # URL template with {id} placeholder
'title_field': str, # Field to use for result title
'subtitle_field': str, # Optional field for subtitle
'relevance_boost': int # Optional relevance score multiplier
}
Example for equipment plugin:
return [{
'model': Equipment,
'join_model': Asset,
'join_condition': Equipment.assetid == Asset.assetid,
'search_fields': ['assetnumber', 'name', 'serialnumber'],
'result_type': 'equipment',
'url_template': '/equipment/{id}',
'title_field': 'assetnumber',
'subtitle_field': 'name',
}]
"""
return []