From 0e0bbc06049656486ae0a9163d42f000432b313d Mon Sep 17 00:00:00 2001 From: cproudlock Date: Fri, 10 Jul 2026 09:53:43 -0400 Subject: [PATCH] Employees + USB default-disabled with an enable-time provisioning note Both plugins provision extra tables, so they now install disabled and explain themselves before a site opts in. - Plugin contract gains get_provisioning_note() -> {tables, note, docs}. Employees and USB implement it (what tables get created in shopdb, how they are referenced, link to the schema README; USB references the captured DLP/reminder plans). - Manifest default_enabled=false for employees + usb; the plugins list API returns provisioning_note + default_enabled; install now registers a plugin disabled when default_enabled is false. - Setup wizard Features step renders the provisioning note the moment a plugin with one is enabled. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/views/SetupWizard.vue | 25 ++++++++++++++++++++----- plugins/employees/manifest.json | 9 ++++++--- plugins/employees/plugin.py | 14 ++++++++++++++ plugins/usb/manifest.json | 17 +++++++++-------- plugins/usb/plugin.py | 14 ++++++++++++++ shopdb/plugins/__init__.py | 12 +++++++++++- shopdb/plugins/base.py | 14 ++++++++++++++ shopdb/plugins/registry.py | 7 ++++--- 8 files changed, 92 insertions(+), 20 deletions(-) diff --git a/frontend/src/views/SetupWizard.vue b/frontend/src/views/SetupWizard.vue index 091e24d..21e8f76 100644 --- a/frontend/src/views/SetupWizard.vue +++ b/frontend/src/views/SetupWizard.vue @@ -36,11 +36,22 @@

Features

Turn features on or off. Changes to routes take effect after the app restarts.

- +
+ + +
+

{{ p.provisioning_note.note }}

+

+ Creates in shopdb: + {{ t }} +

+

Schema: {{ p.provisioning_note.docs }}

+
+

No optional plugins found.

@@ -308,6 +319,10 @@ async function finish() { .plugin-row { display: grid; grid-template-columns: auto auto 1fr; gap: 0.6rem; align-items: baseline; padding: 0.5rem 0.6rem; background: var(--bg); border-radius: 6px; } .plugin-name { font-weight: 600; text-transform: capitalize; } .plugin-desc { color: var(--text-light); font-size: 0.85rem; } +.provision-note { margin: 0.3rem 0 0.2rem 1.9rem; padding: 0.6rem 0.75rem; background: var(--bg); border-left: 3px solid var(--warning); border-radius: 4px; font-size: 0.82rem; } +.provision-note p { margin: 0 0 0.35rem; } +.provision-note p:last-child { margin-bottom: 0; } +.provision-tables code, .provision-docs code { background: var(--bg-card); border: 1px solid var(--border); border-radius: 3px; padding: 0 0.3rem; margin-right: 0.3rem; font-size: 0.78rem; } .seed-result { margin-top: 0.75rem; color: var(--success); font-size: 0.88rem; } .config-block { margin-top: 1.25rem; padding-top: 1rem; border-top: 1px solid var(--border); } .config-title { margin: 0 0 0.75rem; font-size: 1rem; text-transform: capitalize; } diff --git a/plugins/employees/manifest.json b/plugins/employees/manifest.json index ea0fda2..0460d2b 100644 --- a/plugins/employees/manifest.json +++ b/plugins/employees/manifest.json @@ -7,6 +7,9 @@ "core_version": ">=0.1.0,<1.0.0", "api_prefix": "/api/employees", "provides": { - "features": ["employee_directory"] - } -} + "features": [ + "employee_directory" + ] + }, + "default_enabled": false +} \ No newline at end of file diff --git a/plugins/employees/plugin.py b/plugins/employees/plugin.py index d9829f8..bd4b559 100644 --- a/plugins/employees/plugin.py +++ b/plugins/employees/plugin.py @@ -60,6 +60,20 @@ class EmployeesPlugin(BasePlugin): External mode reads a separate DB via employee_connection instead.""" return [DirectoryEmployee] + def get_provisioning_note(self) -> Optional[Dict]: + return { + 'tables': ['directoryemployees'], + 'note': ('Enabling this in self-hosted mode creates a ' + '"directoryemployees" table in the shopdb database (SSO, ' + 'First_Name, Last_Name, Team, Role, Picture). It is ' + 'referenced across the site: employee search, notification ' + 'recognition (name + photo on the shopfloor/lobby displays), ' + 'and USB check-in/out name resolution. Manage people under ' + 'Settings > Employee Directory, or point at an existing HR ' + 'database instead (external mode).'), + 'docs': 'plugins/employees/README.md', + } + def get_config_schema(self) -> List[Dict]: """Employee directory DB connection. Host/name/user are settings the wizard can edit; the password stays in .env (emitted, not stored).""" diff --git a/plugins/usb/manifest.json b/plugins/usb/manifest.json index 05f7cc2..94173df 100644 --- a/plugins/usb/manifest.json +++ b/plugins/usb/manifest.json @@ -1,9 +1,10 @@ { - "name": "usb", - "version": "1.0.0", - "description": "USB device checkout management", - "author": "ShopDB Team", - "dependencies": [], - "core_version": ">=0.1.0,<1.0.0", - "api_prefix": "/api/usb" -} + "name": "usb", + "version": "1.0.0", + "description": "USB device checkout management", + "author": "ShopDB Team", + "dependencies": [], + "core_version": ">=0.1.0,<1.0.0", + "api_prefix": "/api/usb", + "default_enabled": false +} \ No newline at end of file diff --git a/plugins/usb/plugin.py b/plugins/usb/plugin.py index 0cc99b2..eafa27b 100644 --- a/plugins/usb/plugin.py +++ b/plugins/usb/plugin.py @@ -56,6 +56,20 @@ class USBPlugin(BasePlugin): """Return list of SQLAlchemy model classes.""" return [USBDeviceType, USBDevice, USBCheckout] + def get_provisioning_note(self) -> Optional[Dict]: + return { + 'tables': ['usbdevicetypes', 'usbdevices', 'usbcheckouts'], + 'note': ('Enabling this creates USB tracking tables in the shopdb ' + 'database (usbdevicetypes, usbdevices, usbcheckouts) for CMMC ' + 'removable-media check-in/out. Devices, check-in/out events, ' + 'and per-event log ids live here. Planned build-out: a USB-ID ' + 'standard, overdue-device email reminders, and DLP/Fabric ' + 'approval integration (see the captured design notes). A site ' + 'with an existing cmmc_usb database can use external mode ' + 'instead.'), + 'docs': 'plugins/usb/README.md', + } + def get_config_schema(self) -> List[Dict]: """CMMC USB check-in/out database connection. Host/name/user are settings the wizard can edit; the password stays in .env (emitted, not stored).""" diff --git a/shopdb/plugins/__init__.py b/shopdb/plugins/__init__.py index 3d57eb3..6195e1e 100644 --- a/shopdb/plugins/__init__.py +++ b/shopdb/plugins/__init__.py @@ -147,6 +147,11 @@ class PluginManager: config_schema = temp.get_config_schema() except Exception: config_schema = [] + try: + provisioning_note = temp.get_provisioning_note() + except Exception: + provisioning_note = None + manifest = self.loader.load_manifest(name) available.append({ 'name': meta.name, 'version': meta.version, @@ -157,6 +162,8 @@ class PluginManager: 'enabled': state.enabled if state else False, 'installedat': state.installed_at if state else None, 'config_schema': config_schema, + 'provisioning_note': provisioning_note, + 'default_enabled': manifest.get('default_enabled', True), }) except Exception as e: logger.warning(f"Error inspecting plugin {name}: {e}") @@ -203,7 +210,10 @@ class PluginManager: return False # Register plugin - self.registry.register(name, manifest_version) + # Plugins that provision extra tables install disabled until a site + # opts in (manifest default_enabled=false). + self.registry.register(name, manifest_version, + enabled=manifest.get('default_enabled', True)) # Load the plugin plugin = self.loader.load_plugin(name, self._app, self._db) diff --git a/shopdb/plugins/base.py b/shopdb/plugins/base.py index 3cf59b4..3042478 100644 --- a/shopdb/plugins/base.py +++ b/shopdb/plugins/base.py @@ -74,6 +74,20 @@ class BasePlugin(ABC): """Return dict of service name -> service class.""" return {} + def get_provisioning_note(self) -> Optional[Dict]: + """Transparency note shown when a site enables this plugin. + + Return None for plugins that need no special setup. For plugins that + create extra tables (e.g. a self-hosted directory or USB tables), return: + { + 'tables': ['directoryemployees', ...], # created in the shopdb DB + 'note': 'Plain-language what/why.', + 'docs': 'plugins//README.md', # where the schema lives + } + The setup wizard shows this the moment the plugin is checked. + """ + return None + def get_config_schema(self) -> List[Dict]: """Declare the config fields this plugin needs, for the setup wizard. diff --git a/shopdb/plugins/registry.py b/shopdb/plugins/registry.py index a9c7b66..ed0a936 100644 --- a/shopdb/plugins/registry.py +++ b/shopdb/plugins/registry.py @@ -53,13 +53,14 @@ class PluginRegistry: } }, f, indent=2) - def register(self, name: str, version: str) -> PluginState: - """Register a newly installed plugin.""" + def register(self, name: str, version: str, enabled: bool = True) -> PluginState: + """Register a newly installed plugin. enabled=False leaves it off until a + site opts in (used for plugins that provision extra tables).""" state = PluginState( name=name, version=version, installed_at=datetime.utcnow().isoformat(), - enabled=True + enabled=enabled ) self._plugins[name] = state self._save()