diff --git a/frontend/src/views/SetupWizard.vue b/frontend/src/views/SetupWizard.vue index 21e8f76..d6b3b9a 100644 --- a/frontend/src/views/SetupWizard.vue +++ b/frontend/src/views/SetupWizard.vue @@ -42,33 +42,43 @@ {{ p.name }} {{ p.description }} - -
-

{{ p.provisioning_note.note }}

-

- Creates in shopdb: - {{ t }} -

-

Schema: {{ p.provisioning_note.docs }}

+ + +
+ + + + +
+

{{ p.provisioning_note.note }}

+

Creates in shopdb: {{ t }}

+

Schema: {{ p.provisioning_note.docs }}

+
+ + +
+
+ + + {{ field.help }} +
+

No optional plugins found.

- -
-

{{ p.name }} configuration

-
- - - {{ field.help }} -
-
-

Paste these into .env and restart the app (secrets are not stored in the database):

@@ -153,13 +163,21 @@ const form = ref({ }) const plugins = ref([]) const pluginConfig = ref({}) // flat map: setting/field key -> value +const pluginModes = ref({}) // mode_setting key -> 'selfhosted' | 'external' const saving = ref(false) const seeding = ref(false) const seedResult = ref('') -// Enabled plugins that declare config fields. +function modeOf(plugin) { + const key = plugin.provisioning_note?.mode_setting + return key ? (pluginModes.value[key] || 'selfhosted') : null +} + +// Enabled plugins whose external connection config should be collected right +// now: those in external mode (or with config but no mode concept). const configurablePlugins = computed(() => - plugins.value.filter(p => p.enabled && (p.config_schema || []).length)) + plugins.value.filter(p => p.enabled && (p.config_schema || []).length && + (!p.provisioning_note?.mode_setting || modeOf(p) === 'external'))) // .env lines for secret fields that have a value entered. const envLines = computed(() => { @@ -196,8 +214,15 @@ onMounted(async () => { plugins.value = response.data.data?.plugins || response.data.data || [] } catch (err) { /* ignore */ } - // Preload current values for non-secret plugin config fields. + // Preload directory mode (defaults self-hosted) + non-secret config fields. for (const plugin of plugins.value) { + const modeKey = plugin.provisioning_note?.mode_setting + if (modeKey && pluginModes.value[modeKey] === undefined) { + try { + const response = await settingsApi.get(modeKey) + pluginModes.value[modeKey] = response.data?.data?.value || 'selfhosted' + } catch (err) { pluginModes.value[modeKey] = 'selfhosted' } + } for (const field of (plugin.config_schema || [])) { if (field.secret) continue try { @@ -210,8 +235,12 @@ onMounted(async () => { }) async function saveStep() { - // Plugins step: persist non-secret config fields (secrets stay in .env). + // Plugins step: save each plugin's directory mode, plus non-secret external + // connection config (secrets stay in .env). if (current.value.key === 'plugins') { + for (const [modeKey, modeValue] of Object.entries(pluginModes.value)) { + await settingsApi.update(modeKey, modeValue) + } for (const plugin of configurablePlugins.value) { for (const field of plugin.config_schema) { if (field.secret) continue @@ -319,6 +348,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; } +.plugin-mode { margin: 0.4rem 0 0.2rem 1.9rem; } +.mode-opt { display: block; font-size: 0.86rem; margin-bottom: 0.3rem; cursor: pointer; } +.mode-opt input { margin-right: 0.4rem; } +.plugin-mode .provision-note, .plugin-mode .config-block { margin-left: 0; } .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; } diff --git a/plugins/employees/plugin.py b/plugins/employees/plugin.py index bd4b559..5f0a35b 100644 --- a/plugins/employees/plugin.py +++ b/plugins/employees/plugin.py @@ -62,6 +62,7 @@ class EmployeesPlugin(BasePlugin): def get_provisioning_note(self) -> Optional[Dict]: return { + 'mode_setting': 'employee_directory_mode', 'tables': ['directoryemployees'], 'note': ('Enabling this in self-hosted mode creates a ' '"directoryemployees" table in the shopdb database (SSO, ' diff --git a/plugins/usb/plugin.py b/plugins/usb/plugin.py index eafa27b..9744854 100644 --- a/plugins/usb/plugin.py +++ b/plugins/usb/plugin.py @@ -58,6 +58,7 @@ class USBPlugin(BasePlugin): def get_provisioning_note(self) -> Optional[Dict]: return { + 'mode_setting': 'usb_directory_mode', 'tables': ['usbdevicetypes', 'usbdevices', 'usbcheckouts'], 'note': ('Enabling this creates USB tracking tables in the shopdb ' 'database (usbdevicetypes, usbdevices, usbcheckouts) for CMMC ' diff --git a/shopdb/core/api/settings.py b/shopdb/core/api/settings.py index 7b98058..7744852 100644 --- a/shopdb/core/api/settings.py +++ b/shopdb/core/api/settings.py @@ -265,10 +265,17 @@ def build_default_settings(): }, { 'key': 'employee_directory_mode', - 'value': 'external', + 'value': 'selfhosted', 'valuetype': 'string', 'category': 'site', - 'description': "Employee directory source: 'external' (a separate HR database) or 'selfhosted' (managed in-app under Employees)" + 'description': "Employee directory source: 'selfhosted' (tables in this app, default) or 'external' (a separate HR database)" + }, + { + 'key': 'usb_directory_mode', + 'value': 'selfhosted', + 'valuetype': 'string', + 'category': 'site', + 'description': "USB check-in/out source: 'selfhosted' (tables in this app, default) or 'external' (a separate cmmc_usb database)" }, { 'key': 'site_base_url',