The API URL hint still showed the old device.warranty path; align it with the corrected sbil/eapi/v5 endpoint the provider now defaults to. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1322 lines
39 KiB
Vue
1322 lines
39 KiB
Vue
<template>
|
|
<div class="page-header">
|
|
<h1>System Settings</h1>
|
|
</div>
|
|
|
|
<div class="settings-search">
|
|
<input v-model="settingsSearch" type="text" class="form-control"
|
|
placeholder="Search settings (zabbix, smtp, toner, fqdn...)" />
|
|
</div>
|
|
|
|
<div class="settings-layout">
|
|
<nav class="settings-tabs">
|
|
<button
|
|
v-for="tab in visibleTabs"
|
|
:key="tab.key"
|
|
class="settings-tab"
|
|
:class="{ active: activeTab === tab.key }"
|
|
@click="activeTab = tab.key"
|
|
>{{ tab.label }}</button>
|
|
<p v-if="!visibleTabs.length" class="settings-tab-empty">No matching settings</p>
|
|
</nav>
|
|
|
|
<div class="settings-sections">
|
|
<!-- Integrations Section -->
|
|
<div class="section-card" v-show="isVisible('integrations')">
|
|
<h2 class="section-title">Integrations</h2>
|
|
|
|
<div class="setting-group">
|
|
<h3>Zabbix Integration</h3>
|
|
<p class="setting-description">
|
|
Connect to Zabbix for real-time printer supply monitoring. When enabled, supply levels
|
|
are fetched from Zabbix API and displayed on printer detail pages.
|
|
</p>
|
|
|
|
<div class="setting-row">
|
|
<label class="toggle-label">
|
|
<span>Enable Zabbix Integration</span>
|
|
<button
|
|
class="toggle-btn"
|
|
:class="{ active: settings.zabbix_enabled }"
|
|
@click="toggleSetting('zabbix_enabled')"
|
|
:disabled="saving"
|
|
>
|
|
<span class="toggle-slider"></span>
|
|
</button>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-row" v-if="settings.zabbix_enabled">
|
|
<label>
|
|
<span>Zabbix API URL</span>
|
|
<input
|
|
type="url"
|
|
v-model="settings.zabbix_url"
|
|
placeholder="http://zabbix.example.com:8080"
|
|
@blur="saveSetting('zabbix_url', settings.zabbix_url)"
|
|
:disabled="saving"
|
|
>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-row" v-if="settings.zabbix_enabled">
|
|
<label>
|
|
<span>Zabbix API Token</span>
|
|
<input
|
|
type="password"
|
|
v-model="settings.zabbix_token"
|
|
placeholder="Enter API token"
|
|
@blur="saveSetting('zabbix_token', settings.zabbix_token)"
|
|
:disabled="saving"
|
|
>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="status-indicator" v-if="settings.zabbix_enabled">
|
|
<span class="status-dot" :class="zabbixStatus"></span>
|
|
<span>{{ zabbixMessage }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="setting-group">
|
|
<h3>Dell Warranty Lookup</h3>
|
|
<p class="setting-description">
|
|
Look up Dell coverage by service tag via the Dell TechDirect Warranty API.
|
|
When enabled, the Refresh button on a Dell warranty pulls the current service
|
|
level and end date. Requires a Dell TechDirect API client id and secret.
|
|
</p>
|
|
|
|
<div class="setting-row">
|
|
<label class="toggle-label">
|
|
<span>Enable Dell Warranty Lookup</span>
|
|
<button
|
|
class="toggle-btn"
|
|
:class="{ active: settings.warranty_dell_enabled }"
|
|
@click="toggleSetting('warranty_dell_enabled')"
|
|
:disabled="saving"
|
|
>
|
|
<span class="toggle-slider"></span>
|
|
</button>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-row" v-if="settings.warranty_dell_enabled">
|
|
<label>
|
|
<span>Client ID</span>
|
|
<input
|
|
type="text"
|
|
v-model="settings.warranty_dell_clientid"
|
|
placeholder="Dell TechDirect client id"
|
|
@blur="saveSetting('warranty_dell_clientid', settings.warranty_dell_clientid)"
|
|
:disabled="saving"
|
|
>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-row" v-if="settings.warranty_dell_enabled">
|
|
<label>
|
|
<span>Client Secret</span>
|
|
<input
|
|
type="password"
|
|
v-model="settings.warranty_dell_clientsecret"
|
|
placeholder="Enter client secret"
|
|
@blur="saveSetting('warranty_dell_clientsecret', settings.warranty_dell_clientsecret)"
|
|
:disabled="saving"
|
|
>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-row" v-if="settings.warranty_dell_enabled">
|
|
<label>
|
|
<span>Token URL <small>(blank = Dell default)</small></span>
|
|
<input
|
|
type="url"
|
|
v-model="settings.warranty_dell_tokenurl"
|
|
placeholder="https://apigtwb2c.us.dell.com/auth/oauth/v2/token"
|
|
@blur="saveSetting('warranty_dell_tokenurl', settings.warranty_dell_tokenurl)"
|
|
:disabled="saving"
|
|
>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-row" v-if="settings.warranty_dell_enabled">
|
|
<label>
|
|
<span>API URL <small>(blank = Dell default)</small></span>
|
|
<input
|
|
type="url"
|
|
v-model="settings.warranty_dell_apiurl"
|
|
placeholder="https://apigtwb2c.us.dell.com/PROD/sbil/eapi/v5/asset-entitlements"
|
|
@blur="saveSetting('warranty_dell_apiurl', settings.warranty_dell_apiurl)"
|
|
:disabled="saving"
|
|
>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="status-indicator" v-if="settings.warranty_dell_enabled">
|
|
<span class="status-dot" :class="dellStatus"></span>
|
|
<span>{{ dellMessage }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Email Section -->
|
|
<div class="section-card" v-show="isVisible('email')">
|
|
<h2 class="section-title">Email / SMTP</h2>
|
|
|
|
<div class="setting-group">
|
|
<h3>Email Notifications</h3>
|
|
<p class="setting-description">
|
|
Configure SMTP settings to enable email notifications for alerts, toner reports,
|
|
and other system notifications.
|
|
</p>
|
|
|
|
<div class="setting-row">
|
|
<label class="toggle-label">
|
|
<span>Enable Email Notifications</span>
|
|
<button
|
|
class="toggle-btn"
|
|
:class="{ active: settings.smtp_enabled }"
|
|
@click="toggleSetting('smtp_enabled')"
|
|
:disabled="saving"
|
|
>
|
|
<span class="toggle-slider"></span>
|
|
</button>
|
|
</label>
|
|
</div>
|
|
|
|
<template v-if="settings.smtp_enabled">
|
|
<div class="settings-grid">
|
|
<div class="setting-row">
|
|
<label>
|
|
<span>SMTP Host</span>
|
|
<input
|
|
type="text"
|
|
v-model="settings.smtp_host"
|
|
placeholder="smtp.example.com"
|
|
@blur="saveSetting('smtp_host', settings.smtp_host)"
|
|
:disabled="saving"
|
|
>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-row">
|
|
<label>
|
|
<span>SMTP Port</span>
|
|
<input
|
|
type="number"
|
|
v-model="settings.smtp_port"
|
|
placeholder="587"
|
|
@blur="saveSetting('smtp_port', settings.smtp_port)"
|
|
:disabled="saving"
|
|
>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-row">
|
|
<label>
|
|
<span>Username</span>
|
|
<input
|
|
type="text"
|
|
v-model="settings.smtp_username"
|
|
placeholder="username"
|
|
@blur="saveSetting('smtp_username', settings.smtp_username)"
|
|
:disabled="saving"
|
|
>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-row">
|
|
<label>
|
|
<span>Password</span>
|
|
<input
|
|
type="password"
|
|
v-model="settings.smtp_password"
|
|
placeholder="password"
|
|
@blur="saveSetting('smtp_password', settings.smtp_password)"
|
|
:disabled="saving"
|
|
>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="setting-row">
|
|
<label class="toggle-label">
|
|
<span>Use TLS Encryption</span>
|
|
<button
|
|
class="toggle-btn"
|
|
:class="{ active: settings.smtp_use_tls }"
|
|
@click="toggleSetting('smtp_use_tls')"
|
|
:disabled="saving"
|
|
>
|
|
<span class="toggle-slider"></span>
|
|
</button>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="settings-grid">
|
|
<div class="setting-row">
|
|
<label>
|
|
<span>From Address</span>
|
|
<input
|
|
type="email"
|
|
v-model="settings.smtp_from_address"
|
|
placeholder="noreply@example.com"
|
|
@blur="saveSetting('smtp_from_address', settings.smtp_from_address)"
|
|
:disabled="saving"
|
|
>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-row">
|
|
<label>
|
|
<span>From Name</span>
|
|
<input
|
|
type="text"
|
|
v-model="settings.smtp_from_name"
|
|
placeholder="ShopDB"
|
|
@blur="saveSetting('smtp_from_name', settings.smtp_from_name)"
|
|
:disabled="saving"
|
|
>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="setting-row full-width">
|
|
<label>
|
|
<span>Default Alert Recipients</span>
|
|
<input
|
|
type="text"
|
|
v-model="settings.alert_recipients"
|
|
placeholder="user1@example.com, user2@example.com"
|
|
@blur="saveSetting('alert_recipients', settings.alert_recipients)"
|
|
:disabled="saving"
|
|
>
|
|
<small class="input-hint">Comma-separated list of email addresses</small>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="status-indicator">
|
|
<span class="status-dot" :class="smtpStatus"></span>
|
|
<span>{{ smtpMessage }}</span>
|
|
</div>
|
|
|
|
<button
|
|
class="test-btn"
|
|
@click="testEmail"
|
|
:disabled="saving || testingEmail || !canTestEmail"
|
|
>
|
|
{{ testingEmail ? 'Sending...' : 'Send Test Email' }}
|
|
</button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Audit Section -->
|
|
<div class="section-card" v-show="isVisible('audit')">
|
|
<h2 class="section-title">Audit & Logging</h2>
|
|
|
|
<div class="setting-group">
|
|
<h3>Audit Log Retention</h3>
|
|
<p class="setting-description">
|
|
Configure how long audit logs are retained. Older logs will be automatically purged.
|
|
Set to 0 to keep logs indefinitely.
|
|
</p>
|
|
|
|
<div class="setting-row">
|
|
<label>
|
|
<span>Retention Period (days)</span>
|
|
<input
|
|
type="number"
|
|
v-model="settings.audit_retention_days"
|
|
min="0"
|
|
placeholder="90"
|
|
@blur="saveSetting('audit_retention_days', settings.audit_retention_days)"
|
|
:disabled="saving"
|
|
>
|
|
<small class="input-hint">0 = keep forever, recommended: 90 days</small>
|
|
</label>
|
|
</div>
|
|
|
|
<router-link to="/settings/auditlogs" class="view-logs-link">
|
|
View Audit Logs →
|
|
</router-link>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Floor Map Section -->
|
|
<div class="section-card" v-show="isVisible('map')">
|
|
<h2 class="section-title">Floor Map</h2>
|
|
|
|
<div class="setting-group">
|
|
<h3>Facility Blueprint</h3>
|
|
<p class="setting-description">
|
|
The floor-plan image and its pixel dimensions for this facility. Map
|
|
markers are positioned against these dimensions, so the width and
|
|
height must match the native size of the blueprint image. Leave the
|
|
image paths at their defaults to use the bundled sitemap.
|
|
</p>
|
|
|
|
<div class="setting-row">
|
|
<label>
|
|
<span>Blueprint image URL (light theme)</span>
|
|
<input
|
|
type="text"
|
|
v-model="settings.map_blueprint_light"
|
|
placeholder="/static/images/sitemap2025-light.png"
|
|
@blur="saveSetting('map_blueprint_light', settings.map_blueprint_light)"
|
|
:disabled="saving"
|
|
>
|
|
<small class="input-hint">Path or URL to the light-theme floor plan</small>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-row">
|
|
<label>
|
|
<span>Blueprint image URL (dark theme)</span>
|
|
<input
|
|
type="text"
|
|
v-model="settings.map_blueprint_dark"
|
|
placeholder="/static/images/sitemap2025-dark.png"
|
|
@blur="saveSetting('map_blueprint_dark', settings.map_blueprint_dark)"
|
|
:disabled="saving"
|
|
>
|
|
<small class="input-hint">Path or URL to the dark-theme floor plan</small>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-row">
|
|
<label>
|
|
<span>Blueprint width (pixels)</span>
|
|
<input
|
|
type="number"
|
|
v-model="settings.map_width"
|
|
min="1"
|
|
placeholder="3300"
|
|
@blur="saveSetting('map_width', settings.map_width)"
|
|
:disabled="saving"
|
|
>
|
|
<small class="input-hint">Native pixel width of the blueprint image</small>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-row">
|
|
<label>
|
|
<span>Blueprint height (pixels)</span>
|
|
<input
|
|
type="number"
|
|
v-model="settings.map_height"
|
|
min="1"
|
|
placeholder="2550"
|
|
@blur="saveSetting('map_height', settings.map_height)"
|
|
:disabled="saving"
|
|
>
|
|
<small class="input-hint">Native pixel height of the blueprint image</small>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Authentication Section -->
|
|
<div class="section-card" v-show="isVisible('auth')">
|
|
<h2 class="section-title">Authentication</h2>
|
|
|
|
<div class="setting-group">
|
|
<h3>SAML Single Sign-On</h3>
|
|
<p class="setting-description">
|
|
Enable SAML SSO for enterprise authentication. Users can sign in using your organization's
|
|
identity provider (Azure AD, Okta, etc.). Local login can remain enabled as a fallback.
|
|
</p>
|
|
|
|
<div class="setting-row">
|
|
<label class="toggle-label">
|
|
<span>Enable SAML SSO</span>
|
|
<button
|
|
class="toggle-btn"
|
|
:class="{ active: settings.saml_enabled }"
|
|
@click="toggleSetting('saml_enabled')"
|
|
:disabled="saving"
|
|
>
|
|
<span class="toggle-slider"></span>
|
|
</button>
|
|
</label>
|
|
</div>
|
|
|
|
<template v-if="settings.saml_enabled">
|
|
<div class="setting-row">
|
|
<label>
|
|
<span>Identity Provider Metadata URL</span>
|
|
<input
|
|
type="url"
|
|
v-model="settings.saml_idp_metadata_url"
|
|
placeholder="https://login.microsoftonline.com/.../federationmetadata.xml"
|
|
@blur="saveSetting('saml_idp_metadata_url', settings.saml_idp_metadata_url)"
|
|
:disabled="saving"
|
|
>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-row">
|
|
<label>
|
|
<span>Service Provider Entity ID</span>
|
|
<input
|
|
type="text"
|
|
v-model="settings.saml_entity_id"
|
|
placeholder="https://shopdb.example.com"
|
|
@blur="saveSetting('saml_entity_id', settings.saml_entity_id)"
|
|
:disabled="saving"
|
|
>
|
|
<small class="input-hint">Unique identifier for this application</small>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-row">
|
|
<label>
|
|
<span>Assertion Consumer Service URL</span>
|
|
<input
|
|
type="url"
|
|
v-model="settings.saml_acs_url"
|
|
placeholder="https://shopdb.example.com/api/auth/saml/acs"
|
|
@blur="saveSetting('saml_acs_url', settings.saml_acs_url)"
|
|
:disabled="saving"
|
|
>
|
|
<small class="input-hint">URL where SAML responses are sent</small>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-row">
|
|
<label>
|
|
<span>Admin Group Name</span>
|
|
<input
|
|
type="text"
|
|
v-model="settings.saml_admin_group"
|
|
placeholder="ShopDB-Admins"
|
|
@blur="saveSetting('saml_admin_group', settings.saml_admin_group)"
|
|
:disabled="saving"
|
|
>
|
|
<small class="input-hint">SAML group that grants admin role (leave empty to manage manually)</small>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-row">
|
|
<label class="toggle-label">
|
|
<span>Allow Local Login</span>
|
|
<button
|
|
class="toggle-btn"
|
|
:class="{ active: settings.saml_allow_local_login }"
|
|
@click="toggleSetting('saml_allow_local_login')"
|
|
:disabled="saving"
|
|
>
|
|
<span class="toggle-slider"></span>
|
|
</button>
|
|
</label>
|
|
<small class="input-hint toggle-hint">Keep enabled for admin fallback access</small>
|
|
</div>
|
|
|
|
<div class="setting-row">
|
|
<label class="toggle-label">
|
|
<span>Auto-Create Users</span>
|
|
<button
|
|
class="toggle-btn"
|
|
:class="{ active: settings.saml_auto_create_users }"
|
|
@click="toggleSetting('saml_auto_create_users')"
|
|
:disabled="saving"
|
|
>
|
|
<span class="toggle-slider"></span>
|
|
</button>
|
|
</label>
|
|
<small class="input-hint toggle-hint">Automatically create user accounts on first SAML login</small>
|
|
</div>
|
|
|
|
<div class="status-indicator">
|
|
<span class="status-dot" :class="samlStatus"></span>
|
|
<span>{{ samlMessage }}</span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<div class="setting-group">
|
|
<h3>User Management</h3>
|
|
<p class="setting-description">
|
|
Manage local user accounts and roles. Local accounts work alongside SAML when both are enabled.
|
|
</p>
|
|
|
|
<router-link to="/settings/users" class="view-logs-link">
|
|
Manage Users →
|
|
</router-link>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Asset Identifiers Section -->
|
|
<div class="section-card" v-show="isVisible('identifiers')">
|
|
<h2 class="section-title">Asset Identifiers</h2>
|
|
|
|
<div class="setting-group">
|
|
<p class="setting-description">
|
|
Enable or disable optional asset identifiers per asset type. When disabled
|
|
for a type, the identifier is hidden from that type's forms and detail
|
|
pages across the system.
|
|
</p>
|
|
|
|
<div class="table-container">
|
|
<table class="identifier-matrix">
|
|
<thead>
|
|
<tr>
|
|
<th>Identifier</th>
|
|
<th v-for="col in assetTypeCols" :key="col.key">{{ col.label }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="row in identifierRows" :key="row.name">
|
|
<td class="identifier-name">{{ row.label }}</td>
|
|
<td v-for="col in assetTypeCols" :key="col.key">
|
|
<button
|
|
class="toggle-btn"
|
|
:class="{ active: matrixValue(row.name, col.key) }"
|
|
@click="toggleIdentifier(row.name, col.key)"
|
|
:disabled="saving"
|
|
>
|
|
<span class="toggle-slider"></span>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Global Search Section -->
|
|
<div class="section-card" v-show="isVisible('search')">
|
|
<h2 class="section-title">Global Search</h2>
|
|
|
|
<div class="setting-group">
|
|
<p class="setting-description">
|
|
Choose which content types appear in global search results. Disabling a
|
|
type here hides it from search without disabling the plugin elsewhere.
|
|
</p>
|
|
|
|
<div class="setting-row" v-for="domain in searchDomains" :key="domain.key">
|
|
<label class="toggle-label">
|
|
<span>{{ domain.label }}</span>
|
|
<button
|
|
class="toggle-btn"
|
|
:class="{ active: searchValue(domain.key) }"
|
|
@click="toggleSearchDomain(domain.key)"
|
|
:disabled="saving"
|
|
>
|
|
<span class="toggle-slider"></span>
|
|
</button>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Collector PC Type Mapping Section -->
|
|
<div class="section-card" v-show="isVisible('pctype')" v-if="pcTypeMappings.length">
|
|
<h2 class="section-title">Collector PC Type Mapping</h2>
|
|
|
|
<div class="setting-group">
|
|
<p class="setting-description">
|
|
When the collector ingests a PC, its imaging pc-type (from
|
|
C:\Enrollment\pc-type.txt) is mapped to one of your Computer Types.
|
|
Adjust the mapping per site.
|
|
</p>
|
|
|
|
<div class="table-container">
|
|
<table class="identifier-matrix">
|
|
<thead>
|
|
<tr><th>Imaging pc-type</th><th>Computer Type</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="row in pcTypeMappings" :key="row.pxetype">
|
|
<td class="identifier-name">{{ row.pxetype }}</td>
|
|
<td>
|
|
<select
|
|
:value="row.computertype"
|
|
@change="changePcTypeMapping(row.pxetype, $event.target.value)"
|
|
:disabled="saving"
|
|
>
|
|
<option v-for="ct in computerTypes" :key="ct" :value="ct">{{ ct }}</option>
|
|
</select>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="error" class="error-message">{{ error }}</div>
|
|
<div v-if="success" class="success-message">{{ success }}</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import { settingsApi, computersApi } from '../../api'
|
|
import { setIdentifierFlag } from '../../composables/identifierSettings'
|
|
|
|
// Section tabs - one section visible at a time to avoid a long scroll. The
|
|
// search box filters tabs (and, while searching, shows every matching section).
|
|
const SETTINGS_TABS = [
|
|
{ key: 'integrations', label: 'Integrations', keywords: 'zabbix toner supply printer monitoring api integration' },
|
|
{ key: 'email', label: 'Email / SMTP', keywords: 'email smtp mail notifications alerts tls from recipients' },
|
|
{ key: 'audit', label: 'Audit & Logging', keywords: 'audit log retention history' },
|
|
{ key: 'auth', label: 'Authentication', keywords: 'auth saml sso login users idp' },
|
|
{ key: 'identifiers', label: 'Asset Identifiers', keywords: 'identifier gauge lab maintenance fqdn hostname asset' },
|
|
{ key: 'search', label: 'Global Search', keywords: 'search results domains' },
|
|
{ key: 'map', label: 'Floor Map', keywords: 'map floor plan blueprint image facility site dimensions width height' },
|
|
{ key: 'pctype', label: 'PC Type Mapping', keywords: 'pc type mapping collector enrollment shopfloor computer type' },
|
|
]
|
|
const settingsSearch = ref('')
|
|
const route = useRoute()
|
|
// Allow deep-linking to a tab, e.g. /settings/system?tab=map (Floor Map)
|
|
const activeTab = ref(
|
|
route.query.tab && SETTINGS_TABS.some(t => t.key === route.query.tab)
|
|
? String(route.query.tab)
|
|
: 'integrations'
|
|
)
|
|
|
|
const visibleTabs = computed(() => {
|
|
const term = settingsSearch.value.trim().toLowerCase()
|
|
if (!term) return SETTINGS_TABS
|
|
return SETTINGS_TABS.filter(t =>
|
|
t.label.toLowerCase().includes(term) || t.keywords.includes(term))
|
|
})
|
|
|
|
function isVisible(key) {
|
|
if (settingsSearch.value.trim()) {
|
|
return visibleTabs.value.some(t => t.key === key)
|
|
}
|
|
return activeTab.value === key
|
|
}
|
|
|
|
// Keep the active tab valid as the search narrows the list.
|
|
watch(visibleTabs, (tabs) => {
|
|
if (tabs.length && !tabs.some(t => t.key === activeTab.value)) {
|
|
activeTab.value = tabs[0].key
|
|
}
|
|
})
|
|
|
|
const settings = reactive({
|
|
// Zabbix
|
|
zabbix_enabled: false,
|
|
zabbix_url: '',
|
|
zabbix_token: '',
|
|
warranty_dell_enabled: false,
|
|
warranty_dell_clientid: '',
|
|
warranty_dell_clientsecret: '',
|
|
warranty_dell_tokenurl: '',
|
|
warranty_dell_apiurl: '',
|
|
// Email
|
|
smtp_enabled: false,
|
|
smtp_host: '',
|
|
smtp_port: 587,
|
|
smtp_username: '',
|
|
smtp_password: '',
|
|
smtp_use_tls: true,
|
|
smtp_from_address: '',
|
|
smtp_from_name: 'ShopDB',
|
|
alert_recipients: '',
|
|
// Audit
|
|
audit_retention_days: 90,
|
|
// Floor map blueprint (per-facility)
|
|
map_blueprint_light: '',
|
|
map_blueprint_dark: '',
|
|
map_width: 3300,
|
|
map_height: 2550,
|
|
// SAML
|
|
saml_enabled: false,
|
|
saml_idp_metadata_url: '',
|
|
saml_entity_id: '',
|
|
saml_acs_url: '',
|
|
saml_allow_local_login: true,
|
|
saml_auto_create_users: true,
|
|
saml_admin_group: ''
|
|
})
|
|
|
|
// Asset identifier matrix: identifier x asset type. Keys follow
|
|
// identifier_<name>_<assettype>_enabled. Missing = enabled (default on).
|
|
const identifierRows = [
|
|
{ name: 'gaugelabreference', label: 'Gauge Lab Reference' },
|
|
{ name: 'maintenancereference', label: 'Maintenance Reference' },
|
|
{ name: 'fqdn', label: 'FQDN / Hostname' }
|
|
]
|
|
const assetTypeCols = [
|
|
{ key: 'equipment', label: 'Equipment' },
|
|
{ key: 'computer', label: 'PC' },
|
|
{ key: 'printer', label: 'Printer' },
|
|
{ key: 'network_device', label: 'Network' }
|
|
]
|
|
const identifierMatrix = reactive({})
|
|
|
|
function identifierKey(name, assettype) {
|
|
return `identifier_${name}_${assettype}_enabled`
|
|
}
|
|
|
|
function matrixValue(name, assettype) {
|
|
const key = identifierKey(name, assettype)
|
|
return key in identifierMatrix ? identifierMatrix[key] : true
|
|
}
|
|
|
|
// Global-search domain toggles: keys follow search_<type>_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
|
|
}
|
|
|
|
// Collector pc-type -> ComputerType mapping (pctypemap_<pxetype> settings).
|
|
const pcTypeMappings = ref([]) // [{ pxetype, computertype }]
|
|
const computerTypes = ref([]) // ComputerType names for the dropdown
|
|
|
|
const loading = ref(true)
|
|
const saving = ref(false)
|
|
const testingEmail = ref(false)
|
|
const error = ref('')
|
|
const success = ref('')
|
|
|
|
// Zabbix status
|
|
const zabbixStatus = computed(() => {
|
|
if (!settings.zabbix_enabled) return 'inactive'
|
|
if (!settings.zabbix_url || !settings.zabbix_token) return 'warning'
|
|
return 'pending'
|
|
})
|
|
|
|
const zabbixMessage = computed(() => {
|
|
if (!settings.zabbix_enabled) return 'Disabled'
|
|
if (!settings.zabbix_url) return 'URL not configured'
|
|
if (!settings.zabbix_token) return 'Token not configured'
|
|
return 'Configured (connectivity checked on first use)'
|
|
})
|
|
|
|
// Dell warranty status
|
|
const dellStatus = computed(() => {
|
|
if (!settings.warranty_dell_enabled) return 'inactive'
|
|
if (!settings.warranty_dell_clientid || !settings.warranty_dell_clientsecret) return 'warning'
|
|
return 'pending'
|
|
})
|
|
|
|
const dellMessage = computed(() => {
|
|
if (!settings.warranty_dell_enabled) return 'Disabled'
|
|
if (!settings.warranty_dell_clientid) return 'Client id not configured'
|
|
if (!settings.warranty_dell_clientsecret) return 'Client secret not configured'
|
|
return 'Configured (used when you refresh a Dell warranty)'
|
|
})
|
|
|
|
// SMTP status
|
|
const smtpStatus = computed(() => {
|
|
if (!settings.smtp_enabled) return 'inactive'
|
|
if (!settings.smtp_host || !settings.smtp_from_address) return 'warning'
|
|
return 'pending'
|
|
})
|
|
|
|
const smtpMessage = computed(() => {
|
|
if (!settings.smtp_enabled) return 'Disabled'
|
|
if (!settings.smtp_host) return 'SMTP host not configured'
|
|
if (!settings.smtp_from_address) return 'From address not configured'
|
|
return 'Configured - use test button to verify'
|
|
})
|
|
|
|
const canTestEmail = computed(() => {
|
|
return settings.smtp_host && settings.smtp_from_address && settings.alert_recipients
|
|
})
|
|
|
|
// SAML status
|
|
const samlStatus = computed(() => {
|
|
if (!settings.saml_enabled) return 'inactive'
|
|
if (!settings.saml_idp_metadata_url || !settings.saml_entity_id) return 'warning'
|
|
return 'pending'
|
|
})
|
|
|
|
const samlMessage = computed(() => {
|
|
if (!settings.saml_enabled) return 'Disabled - using local authentication only'
|
|
if (!settings.saml_idp_metadata_url) return 'IdP metadata URL not configured'
|
|
if (!settings.saml_entity_id) return 'Entity ID not configured'
|
|
return 'Configured - SAML login available'
|
|
})
|
|
|
|
async function loadSettings() {
|
|
try {
|
|
loading.value = true
|
|
const { data } = await settingsApi.list()
|
|
|
|
const pctypeRows = []
|
|
for (const setting of data.data) {
|
|
if (setting.key in settings) {
|
|
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
|
|
} else if (setting.key.startsWith('pctypemap_')) {
|
|
pctypeRows.push({
|
|
pxetype: setting.key.slice('pctypemap_'.length),
|
|
computertype: setting.value
|
|
})
|
|
}
|
|
}
|
|
pcTypeMappings.value = pctypeRows.sort((a, b) => a.pxetype.localeCompare(b.pxetype))
|
|
|
|
// Computer type options for the mapping dropdown.
|
|
try {
|
|
const typesResponse = await computersApi.types.list({ perpage: 100 })
|
|
computerTypes.value = (typesResponse.data.data || []).map(t => t.computertype)
|
|
} catch (typesError) {
|
|
console.error('Failed to load computer types', typesError)
|
|
}
|
|
} catch (e) {
|
|
error.value = 'Failed to load settings'
|
|
console.error(e)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function changePcTypeMapping(pxetype, computertype) {
|
|
const key = `pctypemap_${pxetype}`
|
|
try {
|
|
saving.value = true
|
|
error.value = ''
|
|
success.value = ''
|
|
await settingsApi.update(key, computertype)
|
|
const row = pcTypeMappings.value.find(r => r.pxetype === pxetype)
|
|
if (row) row.computertype = computertype
|
|
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 toggleSetting(key) {
|
|
const newValue = !settings[key]
|
|
await saveSetting(key, newValue)
|
|
}
|
|
|
|
// Toggle a per-type identifier flag. The key may not be seeded yet on older
|
|
// installs, so fall back to creating it when the update returns 404.
|
|
async function toggleIdentifier(name, assettype) {
|
|
const key = identifierKey(name, assettype)
|
|
const newValue = !matrixValue(name, assettype)
|
|
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: 'identifiers',
|
|
description: `Show the ${name} identifier on ${assettype} assets`
|
|
})
|
|
} else {
|
|
throw e
|
|
}
|
|
}
|
|
identifierMatrix[key] = newValue
|
|
// Push into the shared composable so open asset views react without a
|
|
// full page reload (the composable otherwise fetches only once).
|
|
setIdentifierFlag(name, assettype, 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 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
|
|
error.value = ''
|
|
success.value = ''
|
|
|
|
await settingsApi.update(key, value)
|
|
settings[key] = value
|
|
|
|
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 testEmail() {
|
|
// TODO: Implement test email endpoint
|
|
testingEmail.value = true
|
|
error.value = ''
|
|
success.value = ''
|
|
|
|
try {
|
|
// await settingsApi.testEmail()
|
|
success.value = 'Test email feature coming soon'
|
|
} catch (e) {
|
|
error.value = e.response?.data?.message || 'Failed to send test email'
|
|
} finally {
|
|
testingEmail.value = false
|
|
setTimeout(() => { success.value = '' }, 3000)
|
|
}
|
|
}
|
|
|
|
onMounted(loadSettings)
|
|
</script>
|
|
|
|
<style scoped>
|
|
.settings-search {
|
|
margin-bottom: 1rem;
|
|
}
|
|
.settings-search input {
|
|
width: 100%;
|
|
max-width: 420px;
|
|
padding: 0.5rem 0.75rem;
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
background: var(--bg-card);
|
|
color: var(--text);
|
|
}
|
|
|
|
.settings-layout {
|
|
display: flex;
|
|
gap: 1.5rem;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.settings-tabs {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
min-width: 180px;
|
|
position: sticky;
|
|
top: 1rem;
|
|
}
|
|
.settings-tab {
|
|
text-align: left;
|
|
padding: 0.5rem 0.75rem;
|
|
border: none;
|
|
border-radius: 6px;
|
|
background: transparent;
|
|
color: var(--text);
|
|
cursor: pointer;
|
|
font-size: 0.95rem;
|
|
}
|
|
.settings-tab:hover { background: var(--bg); }
|
|
.settings-tab.active {
|
|
background: var(--primary);
|
|
color: #fff;
|
|
}
|
|
.settings-tab-empty {
|
|
color: var(--text-light);
|
|
font-size: 0.9rem;
|
|
padding: 0.5rem 0.75rem;
|
|
}
|
|
|
|
.settings-sections {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1.5rem;
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
@media (max-width: 700px) {
|
|
.settings-layout { flex-direction: column; }
|
|
.settings-tabs { flex-direction: row; flex-wrap: wrap; position: static; min-width: 0; }
|
|
}
|
|
|
|
.section-card {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.section-title {
|
|
margin: 0 0 1rem 0;
|
|
font-size: 1.1rem;
|
|
color: var(--text);
|
|
}
|
|
|
|
.setting-group {
|
|
border-top: 1px solid var(--border);
|
|
padding-top: 1rem;
|
|
}
|
|
|
|
.setting-group h3 {
|
|
margin: 0 0 0.5rem 0;
|
|
font-size: 1rem;
|
|
color: var(--text);
|
|
}
|
|
|
|
.setting-description {
|
|
color: var(--text-light);
|
|
font-size: 0.9rem;
|
|
margin: 0 0 1rem 0;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.settings-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
gap: 1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.setting-row {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.setting-row.full-width {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
.setting-row label {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.setting-row label span {
|
|
color: var(--text);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.setting-row input {
|
|
padding: 0.5rem;
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-size: 1rem;
|
|
max-width: 400px;
|
|
}
|
|
|
|
.setting-row input[type="number"] {
|
|
max-width: 120px;
|
|
}
|
|
|
|
.setting-row input:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
.input-hint {
|
|
color: var(--text-light);
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.toggle-label {
|
|
flex-direction: row !important;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
max-width: 400px;
|
|
}
|
|
|
|
.toggle-btn {
|
|
position: relative;
|
|
width: 50px;
|
|
height: 26px;
|
|
border-radius: 13px;
|
|
border: none;
|
|
background: var(--secondary);
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.toggle-btn.active {
|
|
background: var(--success);
|
|
}
|
|
|
|
.toggle-slider {
|
|
position: absolute;
|
|
top: 3px;
|
|
left: 3px;
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 50%;
|
|
background: white;
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.toggle-btn.active .toggle-slider {
|
|
transform: translateX(24px);
|
|
}
|
|
|
|
.status-indicator {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.75rem;
|
|
background: var(--bg);
|
|
border-radius: 4px;
|
|
font-size: 0.9rem;
|
|
color: var(--text-light);
|
|
max-width: 400px;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.status-dot {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.status-dot.inactive {
|
|
background: var(--secondary);
|
|
}
|
|
|
|
.status-dot.warning {
|
|
background: var(--warning);
|
|
}
|
|
|
|
.status-dot.pending {
|
|
background: var(--primary);
|
|
}
|
|
|
|
.status-dot.success {
|
|
background: var(--success);
|
|
}
|
|
|
|
.test-btn {
|
|
padding: 0.5rem 1rem;
|
|
background: var(--primary);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.test-btn:hover:not(:disabled) {
|
|
background: var(--primary-dark);
|
|
}
|
|
|
|
.test-btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.error-message {
|
|
margin-top: 1rem;
|
|
padding: 0.75rem;
|
|
background: var(--danger);
|
|
color: white;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.success-message {
|
|
margin-top: 1rem;
|
|
padding: 0.75rem;
|
|
background: var(--success);
|
|
color: white;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.view-logs-link {
|
|
display: inline-block;
|
|
margin-top: 0.5rem;
|
|
color: var(--link);
|
|
text-decoration: none;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.view-logs-link:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.toggle-hint {
|
|
display: block;
|
|
margin-top: -0.5rem;
|
|
margin-left: 0;
|
|
}
|
|
|
|
.identifier-matrix {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.identifier-matrix th,
|
|
.identifier-matrix td {
|
|
padding: 0.6rem 0.75rem;
|
|
text-align: center;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.identifier-matrix th:first-child,
|
|
.identifier-matrix td.identifier-name {
|
|
text-align: left;
|
|
}
|
|
|
|
.identifier-matrix th {
|
|
color: var(--text-light);
|
|
font-weight: 600;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.identifier-matrix .identifier-name {
|
|
color: var(--text);
|
|
}
|
|
</style>
|