diff --git a/frontend/src/views/settings/SystemSettings.vue b/frontend/src/views/settings/SystemSettings.vue
index 57c7194..ff18a5f 100644
--- a/frontend/src/views/settings/SystemSettings.vue
+++ b/frontend/src/views/settings/SystemSettings.vue
@@ -414,6 +414,32 @@
+
+
+
+
Global Search
+
+
+
+ Choose which content types appear in global search results. Disabling a
+ type here hides it from search without disabling the plugin elsewhere.
+
+
+
+
+
+
+
{{ error }}
@@ -476,6 +502,26 @@ function matrixValue(name, assettype) {
return key in identifierMatrix ? identifierMatrix[key] : true
}
+// Global-search domain toggles: keys follow search__enabled.
+// Missing = enabled (default on).
+const searchDomains = [
+ { key: 'application', label: 'Applications' },
+ { key: 'knowledgebase', label: 'Knowledge Base' },
+ { key: 'employee', label: 'Employees' },
+ { key: 'equipment', label: 'Equipment' },
+ { key: 'computer', label: 'PCs' },
+ { key: 'printer', label: 'Printers' },
+ { key: 'network_device', label: 'Network Devices' },
+ { key: 'notification', label: 'Notifications' },
+ { key: 'subnet', label: 'Subnets' }
+]
+const searchMatrix = reactive({})
+
+function searchValue(domainKey) {
+ const key = `search_${domainKey}_enabled`
+ return key in searchMatrix ? searchMatrix[key] : true
+}
+
const loading = ref(true)
const saving = ref(false)
const testingEmail = ref(false)
@@ -538,6 +584,8 @@ async function loadSettings() {
settings[setting.key] = setting.value
} else if (/^identifier_.+_(equipment|computer|printer|network_device)_enabled$/.test(setting.key)) {
identifierMatrix[setting.key] = setting.value !== false
+ } else if (/^search_.+_enabled$/.test(setting.key)) {
+ searchMatrix[setting.key] = setting.value !== false
}
}
} catch (e) {
@@ -591,6 +639,40 @@ async function toggleIdentifier(name, assettype) {
}
}
+async function toggleSearchDomain(domainKey) {
+ const key = `search_${domainKey}_enabled`
+ const newValue = !searchValue(domainKey)
+ const label = searchDomains.find(d => d.key === domainKey)?.label || domainKey
+ try {
+ saving.value = true
+ error.value = ''
+ success.value = ''
+ try {
+ await settingsApi.update(key, newValue)
+ } catch (e) {
+ if (e.response?.status === 404) {
+ await settingsApi.create({
+ key,
+ value: newValue,
+ valuetype: 'boolean',
+ category: 'search',
+ description: `Include ${label} in global search results`
+ })
+ } else {
+ throw e
+ }
+ }
+ searchMatrix[key] = newValue
+ success.value = 'Setting saved'
+ setTimeout(() => { success.value = '' }, 2000)
+ } catch (e) {
+ error.value = e.response?.data?.message || 'Failed to save setting'
+ console.error(e)
+ } finally {
+ saving.value = false
+ }
+}
+
async function saveSetting(key, value) {
try {
saving.value = true
diff --git a/shopdb/core/api/search.py b/shopdb/core/api/search.py
index 62e2097..0bab4db 100644
--- a/shopdb/core/api/search.py
+++ b/shopdb/core/api/search.py
@@ -11,7 +11,7 @@ from sqlalchemy.orm import joinedload
from shopdb.extensions import db
from shopdb.core.models import (
- Application,
+ Application, Setting,
Asset, AssetType, Communication, Vendor, Model
)
from shopdb.utils.responses import success_response
@@ -718,6 +718,16 @@ def global_search():
seen_ids[key] = True
unique_results.append(r)
+ # Drop result types disabled in Settings > Search (search__enabled).
+ # Missing key = enabled. One query, default-on.
+ disabled_types = {
+ s.key[len('search_'):-len('_enabled')]
+ for s in Setting.query.filter_by(category='search').all()
+ if s.get_typed_value() is False
+ }
+ if disabled_types:
+ unique_results = [r for r in unique_results if r['type'] not in disabled_types]
+
# Compute type counts before truncation
type_counts = {}
for r in unique_results:
diff --git a/shopdb/core/api/settings.py b/shopdb/core/api/settings.py
index 5533f66..dff9a1c 100644
--- a/shopdb/core/api/settings.py
+++ b/shopdb/core/api/settings.py
@@ -27,6 +27,22 @@ IDENTIFIER_LABELS = {
}
IDENTIFIER_ASSETTYPES = ['equipment', 'computer', 'printer', 'network_device']
+# Global-search result types that can be toggled on/off independently of whether
+# the owning plugin is enabled. Keys match the `type` field on search results;
+# seed keys are search__enabled (boolean, default true). Drives the
+# Settings "Search" toggles and the filter in shopdb/core/api/search.py.
+SEARCH_DOMAINS = {
+ 'application': 'Applications',
+ 'knowledgebase': 'Knowledge Base',
+ 'employee': 'Employees',
+ 'equipment': 'Equipment',
+ 'computer': 'PCs',
+ 'printer': 'Printers',
+ 'network_device': 'Network Devices',
+ 'notification': 'Notifications',
+ 'subnet': 'Subnets',
+}
+
def _is_secret(key: str) -> bool:
return 'password' in key or 'token' in key or 'secret' in key
@@ -182,7 +198,19 @@ def build_default_settings():
for assettype in IDENTIFIER_ASSETTYPES
]
- defaults = identifierdefaults + [
+ # Per-domain global-search toggles (search__enabled).
+ searchdefaults = [
+ {
+ 'key': f'search_{key}_enabled',
+ 'value': 'true',
+ 'valuetype': 'boolean',
+ 'category': 'search',
+ 'description': f'Include {label} in global search results',
+ }
+ for key, label in SEARCH_DOMAINS.items()
+ ]
+
+ defaults = identifierdefaults + searchdefaults + [
# Zabbix integration
{
'key': 'zabbix_enabled',
diff --git a/tests/test_core/test_search_settings.py b/tests/test_core/test_search_settings.py
new file mode 100644
index 0000000..2514ab2
--- /dev/null
+++ b/tests/test_core/test_search_settings.py
@@ -0,0 +1,44 @@
+"""Per-domain global-search toggles (search__enabled).
+
+A content type can be hidden from search independently of whether its plugin is
+enabled, by setting search__enabled = false.
+"""
+
+from shopdb.core.api.settings import SEARCH_DOMAINS
+
+
+def _kb_types(client, auth_headers, term):
+ resp = client.get(f'/api/search?q={term}', headers=auth_headers)
+ assert resp.status_code == 200, resp.get_json()
+ return [r for r in resp.get_json()['data']['results']
+ if r.get('type') == 'knowledgebase']
+
+
+def test_domain_included_by_default(client, db, auth_headers):
+ """With no toggle set, the knowledgebase domain appears in search."""
+ client.post('/api/knowledgebase',
+ json={'shortdescription': 'searchtoggle alpha',
+ 'linkurl': 'https://kb.example/a'},
+ headers=auth_headers)
+ assert _kb_types(client, auth_headers, 'searchtoggle')
+
+
+def test_disabled_domain_excluded(client, db, auth_headers):
+ """search_knowledgebase_enabled=false hides KB results."""
+ from shopdb.core.models import Setting
+ client.post('/api/knowledgebase',
+ json={'shortdescription': 'searchtoggle beta',
+ 'linkurl': 'https://kb.example/b'},
+ headers=auth_headers)
+ Setting.set('search_knowledgebase_enabled', False, valuetype='boolean',
+ category='search')
+ assert _kb_types(client, auth_headers, 'searchtoggle') == []
+
+
+def test_search_seed_creates_domain_keys(client, db, auth_headers):
+ """Seeding produces one boolean setting per search domain."""
+ resp = client.post('/api/settings/seed', headers=auth_headers)
+ assert resp.status_code == 200
+ from shopdb.core.models import Setting
+ keys = {s.key for s in Setting.query.filter_by(category='search').all()}
+ assert {f'search_{k}_enabled' for k in SEARCH_DOMAINS} <= keys