342 lines
16 KiB
HTML
342 lines
16 KiB
HTML
{% extends 'base.html' %}
|
|
{% block content %}
|
|
<div class="software-settings-page">
|
|
<div class="toolbar software-settings-header">
|
|
<div>
|
|
<h1>{{ t('software.settings.title') }}</h1>
|
|
<p class="muted">{{ t('software.settings.description') }}</p>
|
|
</div>
|
|
<a class="button button-secondary" href="/software">{{ t('software.settings.back_to_software') }}</a>
|
|
</div>
|
|
|
|
<form method="post" action="/settings/software" class="software-settings-form">
|
|
<section class="panel software-settings-section">
|
|
<div class="software-settings-section-heading">
|
|
<div>
|
|
<h2>{{ t('software.settings.dispatch_title') }}</h2>
|
|
<p class="muted">{{ t('software.settings.dispatch_help') }}</p>
|
|
</div>
|
|
<span class="software-settings-badge">{{ t('software.settings.dispatch_badge') }}</span>
|
|
</div>
|
|
|
|
<div class="software-settings-grid">
|
|
<label>
|
|
{{ t('software.settings.dispatch_delay') }}
|
|
<input type="number" name="dispatch_delay_seconds" min="0" max="60" step="1" value="{{ software_settings.get('dispatch_delay_seconds', 2) }}">
|
|
<small>{{ t('software.settings.dispatch_delay_help') }}</small>
|
|
</label>
|
|
<div class="software-callback-preview">
|
|
<span>{{ t('software.settings.timeout_check') }}</span>
|
|
<code>{{ t('software.settings.timeout_check_value') }}</code>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="panel software-settings-section">
|
|
<div class="software-settings-section-heading">
|
|
<div>
|
|
<h2>{{ t('software.settings.retry_title') }}</h2>
|
|
<p class="muted">{{ t('software.settings.retry_help') }}</p>
|
|
</div>
|
|
<span class="software-settings-badge">{{ t('software.settings.retry_badge') }}</span>
|
|
</div>
|
|
|
|
<div class="software-settings-grid">
|
|
<label class="software-toggle-option software-settings-wide">
|
|
<input type="checkbox" name="automatic_retry_enabled" {% if software_settings.get('automatic_retry_enabled', true) %}checked{% endif %}>
|
|
<span class="software-toggle-track"><span class="software-toggle-thumb"></span></span>
|
|
<span>
|
|
<strong>{{ t('software.settings.retry_enabled') }}</strong>
|
|
<small>{{ t('software.settings.retry_enabled_help') }}</small>
|
|
</span>
|
|
</label>
|
|
|
|
<label>
|
|
{{ t('software.settings.retry_max_age') }}
|
|
<input type="number" name="automatic_retry_max_age_hours" min="1" max="168" step="1" value="{{ software_settings.get('automatic_retry_max_age_hours', 12) }}">
|
|
<small>{{ t('software.settings.retry_max_age_help') }}</small>
|
|
</label>
|
|
|
|
<label>
|
|
{{ t('software.settings.retry_interval') }}
|
|
<input type="number" name="automatic_retry_interval_seconds" min="10" max="86400" step="1" value="{{ software_settings.get('automatic_retry_interval_seconds', 60) }}">
|
|
<small>{{ t('software.settings.retry_interval_help') }}</small>
|
|
</label>
|
|
|
|
<label>
|
|
{{ t('software.settings.retry_max_retries') }}
|
|
<input type="number" name="automatic_retry_max_retries" min="1" max="10" step="1" value="{{ software_settings.get('automatic_retry_max_retries', 3) }}">
|
|
<small>{{ t('software.settings.retry_max_retries_help') }}</small>
|
|
</label>
|
|
|
|
<div class="software-callback-preview">
|
|
<span>{{ t('software.settings.retry_statuses') }}</span>
|
|
<code>{{ t('software.settings.retry_statuses_value') }}</code>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="panel software-settings-section">
|
|
<div class="software-settings-section-heading">
|
|
<div>
|
|
<h2>{{ t('software.settings.callback_title') }}</h2>
|
|
<p class="muted">{{ t('software.settings.callback_help') }}</p>
|
|
</div>
|
|
<span class="software-settings-badge">{{ t('software.settings.callback_badge') }}</span>
|
|
</div>
|
|
|
|
<div class="software-settings-grid">
|
|
<label class="software-settings-wide">
|
|
{{ t('software.settings.callback_base_url') }}
|
|
<input
|
|
type="url"
|
|
name="callback_base_url"
|
|
value="{{ software_settings.get('callback_base_url', '') }}"
|
|
placeholder="https://assetmanager.example.org"
|
|
autocomplete="url"
|
|
>
|
|
<small>{{ t('software.settings.callback_base_url_help') }}</small>
|
|
</label>
|
|
|
|
<label>
|
|
{{ t('software.settings.callback_timeout') }}
|
|
<input type="number" name="callback_timeout_minutes" min="5" max="240" value="{{ software_settings.get('callback_timeout_minutes', 30) }}">
|
|
<small>{{ t('software.settings.callback_timeout_help') }}</small>
|
|
</label>
|
|
|
|
<label>
|
|
{{ t('software.settings.callback_workers') }}
|
|
<input type="number" name="callback_worker_count" min="1" max="8" value="{{ software_settings.get('callback_worker_count', 4) }}">
|
|
<small>{{ t('software.settings.callback_workers_help') }}</small>
|
|
</label>
|
|
|
|
<label>
|
|
{{ t('software.settings.test_timeout') }}
|
|
<input type="number" name="callback_test_timeout_seconds" min="2" max="60" value="{{ software_settings.get('callback_test_timeout_seconds', 10) }}">
|
|
<small>{{ t('software.settings.test_timeout_help') }}</small>
|
|
</label>
|
|
|
|
<label class="software-toggle-option">
|
|
<input type="checkbox" name="callback_test_verify_tls" {% if software_settings.get('callback_test_verify_tls', true) %}checked{% endif %}>
|
|
<span class="software-toggle-track"><span class="software-toggle-thumb"></span></span>
|
|
<span>
|
|
<strong>{{ t('software.settings.verify_tls') }}</strong>
|
|
<small>{{ t('software.settings.verify_tls_help') }}</small>
|
|
</span>
|
|
</label>
|
|
|
|
<div class="software-callback-preview software-settings-wide">
|
|
<span>{{ t('software.settings.health_url') }}</span>
|
|
<code id="callback-health-preview">{{ callback_health_url or t('software.settings.automatic_url') }}</code>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="panel software-settings-section">
|
|
<div class="software-settings-section-heading">
|
|
<div>
|
|
<h2>{{ t('software.settings.debug_title') }}</h2>
|
|
<p class="muted">{{ t('software.settings.debug_help') }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="software-settings-grid">
|
|
<label class="software-toggle-option">
|
|
<input type="checkbox" name="debug_logging" {% if software_settings.get('debug_logging', true) %}checked{% endif %}>
|
|
<span class="software-toggle-track"><span class="software-toggle-thumb"></span></span>
|
|
<span>
|
|
<strong>{{ t('software.settings.debug_logging') }}</strong>
|
|
<small>{{ t('software.settings.debug_logging_help') }}</small>
|
|
</span>
|
|
</label>
|
|
|
|
<label>
|
|
{{ t('software.settings.debug_max_lines') }}
|
|
<input type="number" name="debug_log_max_lines" min="100" max="10000" value="{{ software_settings.get('debug_log_max_lines', 1000) }}">
|
|
<small>{{ t('software.settings.debug_max_lines_help') }}</small>
|
|
</label>
|
|
|
|
<div class="software-debug-path software-settings-wide">
|
|
<span>{{ t('software.settings.debug_log_path') }}</span>
|
|
<code>{{ debug_log_path }}</code>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="panel software-settings-section">
|
|
<div class="software-settings-section-heading">
|
|
<div>
|
|
<h2>{{ t('software.settings.exclusions_title') }}</h2>
|
|
<p class="muted">{{ t('software.settings.exclusions_help') }}</p>
|
|
</div>
|
|
<span class="software-settings-badge">{{ t('software.settings.active') }}</span>
|
|
</div>
|
|
|
|
<div class="software-exclusion-list" id="software-exclusion-list">
|
|
{% for rule in software_settings.get('inventory_exclusion_rules', []) %}
|
|
<div class="software-exclusion-row">
|
|
<label>
|
|
<span>{{ t('software.settings.exclude_platform') }}</span>
|
|
<select name="inventory_exclusion_platform">
|
|
{% for platform in ['all', 'windows', 'linux', 'macos', 'unknown'] %}
|
|
<option value="{{ platform }}" {% if rule.get('platform', 'all') == platform %}selected{% endif %}>{{ t('software.platform.' ~ platform, platform) }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</label>
|
|
<label class="software-exclusion-name">
|
|
<span>{{ t('software.settings.exclude_name_contains') }}</span>
|
|
<input type="text" name="inventory_exclusion_name" maxlength="300" value="{{ rule.get('name_contains', '') }}" placeholder="{{ t('software.settings.exclude_name_placeholder') }}">
|
|
</label>
|
|
<button type="button" class="button button-secondary software-exclusion-remove" title="{{ t('software.settings.exclude_remove') }}" aria-label="{{ t('software.settings.exclude_remove') }}">{{ t('common.delete') }}</button>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<template id="software-exclusion-row-template">
|
|
<div class="software-exclusion-row">
|
|
<label>
|
|
<span>{{ t('software.settings.exclude_platform') }}</span>
|
|
<select name="inventory_exclusion_platform">
|
|
<option value="all">{{ t('software.platform.all', 'All platforms') }}</option>
|
|
<option value="windows">{{ t('software.platform.windows', 'Windows') }}</option>
|
|
<option value="linux">{{ t('software.platform.linux', 'Linux') }}</option>
|
|
<option value="macos">{{ t('software.platform.macos', 'macOS') }}</option>
|
|
<option value="unknown">{{ t('software.platform.unknown', 'Unknown') }}</option>
|
|
</select>
|
|
</label>
|
|
<label class="software-exclusion-name">
|
|
<span>{{ t('software.settings.exclude_name_contains') }}</span>
|
|
<input type="text" name="inventory_exclusion_name" maxlength="300" placeholder="{{ t('software.settings.exclude_name_placeholder') }}">
|
|
</label>
|
|
<button type="button" class="button button-secondary software-exclusion-remove" title="{{ t('software.settings.exclude_remove') }}" aria-label="{{ t('software.settings.exclude_remove') }}">{{ t('common.delete') }}</button>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="software-exclusion-actions">
|
|
<button type="button" class="button button-secondary" id="software-exclusion-add">{{ t('software.settings.exclude_add') }}</button>
|
|
<small class="muted">{{ t('software.settings.exclude_match_help') }}</small>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="form-actions software-settings-actions">
|
|
<button type="submit" class="button button-save" data-save-shortcut>{{ t('common.save') }}</button>
|
|
</div>
|
|
</form>
|
|
|
|
<section class="panel software-settings-section software-test-section">
|
|
<div class="software-settings-section-heading">
|
|
<div>
|
|
<h2>{{ t('software.settings.test_title') }}</h2>
|
|
<p class="muted">{{ t('software.settings.test_help') }}</p>
|
|
</div>
|
|
<form method="post" action="/settings/software/test-callback" class="inline-form">
|
|
<button type="submit" class="button">{{ t('software.settings.run_test') }}</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="software-debug-toolbar">
|
|
<span class="muted">{{ t('software.settings.log_auto_refresh') }}</span>
|
|
<div class="software-debug-actions">
|
|
<button type="button" class="button button-secondary" id="copy-software-debug-log">{{ t('software.copy_log') }}</button>
|
|
<form method="post" action="/settings/software/debug-log/clear" class="inline-form">
|
|
<button type="submit" class="button button-secondary">{{ t('software.settings.clear_log') }}</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<pre class="sync-log software-debug-log" id="software-debug-log" data-localize-log>{{ debug_log or t('software.settings.no_log') }}</pre>
|
|
</section>
|
|
|
|
<section class="panel software-settings-section software-future-section">
|
|
<h2>{{ t('software.settings.future_title') }}</h2>
|
|
<p class="muted">{{ t('software.settings.future_help') }}</p>
|
|
<div class="software-future-grid">
|
|
<span>{{ t('software.settings.future_inventory') }}</span>
|
|
<span>{{ t('software.settings.future_retention') }}</span>
|
|
<span>{{ t('software.settings.future_packages') }}</span>
|
|
<span>{{ t('software.settings.future_security') }}</span>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<script>
|
|
|
|
async function copyTextRobust(text) {
|
|
const value = String(text || '');
|
|
if (!value) return false;
|
|
|
|
try {
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
await navigator.clipboard.writeText(value);
|
|
return true;
|
|
}
|
|
} catch (_) {}
|
|
|
|
const area = document.createElement('textarea');
|
|
area.value = value;
|
|
area.setAttribute('readonly', '');
|
|
area.style.position = 'fixed';
|
|
area.style.left = '-9999px';
|
|
area.style.top = '0';
|
|
area.style.opacity = '0';
|
|
document.body.appendChild(area);
|
|
area.focus();
|
|
area.select();
|
|
area.setSelectionRange(0, area.value.length);
|
|
|
|
let copied = false;
|
|
try {
|
|
copied = document.execCommand('copy');
|
|
} catch (_) {
|
|
copied = false;
|
|
}
|
|
area.remove();
|
|
return copied;
|
|
}
|
|
(() => {
|
|
const baseInput = document.querySelector('input[name="callback_base_url"]');
|
|
const preview = document.getElementById('callback-health-preview');
|
|
const automaticLabel = {{ t('software.settings.automatic_url')|tojson }};
|
|
function updatePreview() {
|
|
const base = (baseInput?.value || '').trim().replace(/\/+$/, '');
|
|
preview.textContent = base ? base + '/api/software-callback/health' : automaticLabel;
|
|
}
|
|
baseInput?.addEventListener('input', updatePreview);
|
|
|
|
const exclusionList = document.getElementById('software-exclusion-list');
|
|
const exclusionTemplate = document.getElementById('software-exclusion-row-template');
|
|
function bindExclusionRow(row) {
|
|
row?.querySelector('.software-exclusion-remove')?.addEventListener('click', () => row.remove());
|
|
}
|
|
function addExclusionRow() {
|
|
if (!exclusionList || !exclusionTemplate) return;
|
|
const fragment = exclusionTemplate.content.cloneNode(true);
|
|
const row = fragment.querySelector('.software-exclusion-row');
|
|
bindExclusionRow(row);
|
|
exclusionList.appendChild(fragment);
|
|
row?.querySelector('input[name="inventory_exclusion_name"]')?.focus();
|
|
}
|
|
exclusionList?.querySelectorAll('.software-exclusion-row').forEach(bindExclusionRow);
|
|
document.getElementById('software-exclusion-add')?.addEventListener('click', addExclusionRow);
|
|
if (exclusionList && exclusionList.children.length === 0) addExclusionRow();
|
|
|
|
const log = document.getElementById('software-debug-log');
|
|
document.getElementById('copy-software-debug-log')?.addEventListener('click', async event => {
|
|
const ok = await copyTextRobust(log?.textContent || '');
|
|
event.currentTarget.textContent = ok ? {{ t('software.copied')|tojson }} : {{ t('software.copy_failed')|tojson }};
|
|
window.setTimeout(() => event.currentTarget.textContent = {{ t('software.copy_log')|tojson }}, 1600);
|
|
});
|
|
async function refreshLog() {
|
|
try {
|
|
const response = await fetch('/settings/software/debug-log', {cache: 'no-store'});
|
|
if (response.ok) {
|
|
const text = await response.text();
|
|
log.textContent = text || {{ t('software.settings.no_log')|tojson }}; window.AssetManagerTime?.scan(log);
|
|
log.scrollTop = log.scrollHeight;
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
log.scrollTop = log.scrollHeight;
|
|
window.setInterval(refreshLog, 3000);
|
|
})();
|
|
</script>
|
|
{% endblock %}
|