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.
-
-
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',