Setup wizard P2: per-plugin config schema + settings-first creds

- Plugin contract gains get_config_schema(); the plugins list API returns it.
  Employees plugin declares its directory-DB fields (host/name/user + password).
- employee_connection reads host/name/user settings-first (env fallback); the
  password stays env-only.
- Setup wizard Features step renders each enabled plugin's config: non-secret
  fields save to settings; secrets are never stored - the wizard emits .env
  lines to paste. Fixed the plugins-list data path (data.plugins).
- Settings PUT now upserts (creates the row on first write) so plugin-config
  keys can be saved without pre-seeding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-07-10 08:10:48 -04:00
parent 843b225a47
commit 087ece0f8c
6 changed files with 139 additions and 6 deletions

View File

@@ -143,6 +143,10 @@ class PluginManager:
meta = temp.meta
state = self.registry.get(name)
try:
config_schema = temp.get_config_schema()
except Exception:
config_schema = []
available.append({
'name': meta.name,
'version': meta.version,
@@ -151,7 +155,8 @@ class PluginManager:
'dependencies': meta.dependencies,
'installed': state is not None,
'enabled': state.enabled if state else False,
'installedat': state.installed_at if state else None
'installedat': state.installed_at if state else None,
'config_schema': config_schema,
})
except Exception as e:
logger.warning(f"Error inspecting plugin {name}: {e}")

View File

@@ -74,6 +74,22 @@ class BasePlugin(ABC):
"""Return dict of service name -> service class."""
return {}
def get_config_schema(self) -> List[Dict]:
"""Declare the config fields this plugin needs, for the setup wizard.
Each field is a dict:
key - the Setting key (non-secret) it maps to
label - human label
type - 'text' | 'number' | 'password'
secret - True for credentials; these are NOT stored in the DB, the
wizard emits an .env line for the operator to paste instead
envvar - (secret only) the .env variable name to emit
default - optional default shown as a placeholder
help - optional hint
Return [] (default) if the plugin needs no configuration.
"""
return []
def get_setting(self, key: str, default=None):
"""Read a plugin-scoped setting from the core Setting store.