Files
pulse-signage/src/web/views/settings/system/index.hbs
T
2026-08-28 02:53:59 +01:00

493 lines
37 KiB
Handlebars

<div class="page-header">
<div>
<h2>System Settings</h2>
<p>Configure the defaults and content policies used across Pulse Signage.</p>
</div>
</div>
<div class="row g-3 align-items-start">
<div class="col-12 col-lg-3 col-xl-2">
<div class="card settings-nav-card">
<div class="card-body p-2">
<div class="text-uppercase text-muted small fw-semibold px-2 pt-1 pb-2">Settings</div>
<nav class="nav nav-pills flex-column gap-1" aria-label="Settings sections">
<a class="nav-link active" href="#application-defaults" data-settings-section-link>
<i class="bi bi-sliders me-2" aria-hidden="true"></i>Defaults
</a>
<a class="nav-link" href="#media-uploads" data-settings-section-link>
<i class="bi bi-cloud-arrow-up me-2" aria-hidden="true"></i>Media Uploads
</a>
<a class="nav-link" href="#icon-suggestions" data-settings-section-link>
<i class="bi bi-grid-3x3-gap me-2" aria-hidden="true"></i>Icon Suggestions
</a>
<a class="nav-link" href="#weather-providers" data-settings-section-link>
<i class="bi bi-cloud-sun me-2" aria-hidden="true"></i>Weather Providers
</a>
<a class="nav-link" href="#security-sessions" data-settings-section-link>
<i class="bi bi-shield-lock me-2" aria-hidden="true"></i>Security and Sessions
</a>
<a class="nav-link" href="#audit-logging" data-settings-section-link>
<i class="bi bi-journal-text me-2" aria-hidden="true"></i>Audit Logging
</a>
<a class="nav-link" href="#diagnostics" data-settings-section-link>
<i class="bi bi-activity me-2" aria-hidden="true"></i>Diagnostics
</a>
</nav>
</div>
</div>
</div>
<div class="col-12 col-lg-9 col-xl-10">
<form method="post" action="/settings/system" data-async-save data-settings-form="defaults">
<input type="hidden" name="settings_section" value="defaults">
<div id="application-defaults" class="card settings-section-card" data-settings-section>
<div class="card-header d-flex align-items-start justify-content-between gap-3">
<div class="min-w-0">
<h3 class="h5 mb-1 fw-semibold"><i class="bi bi-sliders me-2 text-primary" aria-hidden="true"></i>Defaults</h3>
<p class="text-muted small mb-0">Set starting values for players and announcements.</p>
</div>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12"><h4 class="h6 text-uppercase text-muted mb-0">Player defaults</h4></div>
<div class="col-12 col-md-4">
<label class="form-label" for="settings-default-slide-duration">Default slide duration</label>
<div class="input-group"><input id="settings-default-slide-duration" name="default_slide_duration_seconds" type="number" class="form-control" min="1" max="3600" required value="{{mediaSettings.defaultSlideDurationSeconds}}"><span class="input-group-text">seconds</span></div>
</div>
<div class="col-12 col-md-8">
<label class="form-label">Player behavior</label>
<div class="d-flex flex-wrap align-items-center gap-4 pt-2">
<div class="form-check form-switch">
<input id="settings-default-fade" name="default_fade_between_slides" type="checkbox" class="form-check-input" value="1" {{#if mediaSettings.defaultFadeBetweenSlides}}checked{{/if}}>
<label class="form-check-label" for="settings-default-fade">Fade between slides</label>
</div>
<div class="form-check form-switch">
<input id="settings-default-skip-rtmp" name="default_skip_unavailable_rtmp" type="checkbox" class="form-check-input" value="1" {{#if mediaSettings.defaultSkipUnavailableRtmp}}checked{{/if}}>
<label class="form-check-label" for="settings-default-skip-rtmp">Skip unavailable RTMP</label>
</div>
</div>
</div>
<div class="col-12"><hr class="my-2"><h4 class="h6 text-uppercase text-muted mb-0">Announcement defaults</h4></div>
<div class="col-12 col-md-6">
<label class="form-label" for="settings-default-announcement-icon">Default announcement icon</label>
<div class="announcement-icon-picker" data-icon-picker data-settings-default-icon-picker>
<select id="settings-default-announcement-icon" name="default_announcement_icon" class="d-none" tabindex="-1" aria-hidden="true">
{{#each suggestedAnnouncementIcons}}<option value="{{value}}" data-icon-label="{{label}}" {{#if (eq value ../mediaSettings.defaultAnnouncementIcon)}}selected{{/if}}>{{label}}</option>{{/each}}
</select>
<button type="button" class="announcement-icon-picker__toggle" data-icon-picker-toggle data-settings-default-icon-toggle aria-expanded="false" aria-controls="settings-default-announcement-icon-menu" title="Select an icon">
<span class="announcement-icon-picker__toggle-icon" data-icon-picker-preview data-settings-default-icon-preview aria-hidden="true"></span>
<span class="announcement-icon-picker__toggle-content"><span class="announcement-icon-picker__toggle-label" data-icon-picker-label data-settings-default-icon-label>Select an icon</span><span class="announcement-icon-picker__toggle-hint">Click to choose</span></span>
<i class="bi bi-chevron-down announcement-icon-picker__caret" aria-hidden="true"></i>
</button>
<div id="settings-default-announcement-icon-menu" class="announcement-icon-picker__menu" data-icon-picker-menu data-settings-default-icon-menu hidden>
<div class="announcement-icon-picker__header"><span>Choose default icon</span><button type="button" class="btn btn-sm btn-link text-decoration-none p-0" data-icon-picker-close data-settings-default-icon-close>Close</button></div>
<div class="input-group input-group-sm announcement-icon-picker__search"><span class="input-group-text"><i class="bi bi-search" aria-hidden="true"></i></span><input type="search" class="form-control" placeholder="Search icons" aria-label="Search icons" data-icon-picker-search data-settings-default-icon-search></div>
<div class="announcement-icon-picker__grid" data-icon-picker-grid data-settings-default-icon-grid>
{{#each defaultAnnouncementIconSuggestions}}<button type="button" class="announcement-icon-picker__option" data-icon-picker-option data-settings-default-icon-option data-icon-key="{{value}}" data-icon-label="{{label}}" aria-pressed="false" title="{{label}}"><i class="bi bi-{{value}}" aria-hidden="true"></i><span class="visually-hidden">{{label}}</span></button>{{/each}}
</div>
<div class="announcement-icon-picker__search-empty" data-icon-picker-empty data-settings-default-icon-empty hidden>No matching icons.</div>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label" for="settings-default-announcement-duration">Default announcement duration</label>
<div class="row g-2"><div class="col-7"><input id="settings-default-announcement-duration" name="default_announcement_duration_value" type="number" class="form-control" min="1" max="3600" required value="{{mediaSettings.defaultAnnouncementDurationValue}}"></div><div class="col-5"><select name="default_announcement_duration_unit" class="form-select" required><option value="seconds" {{#if (eq mediaSettings.defaultAnnouncementDurationUnit 'seconds')}}selected{{/if}}>Seconds</option><option value="minutes" {{#if (eq mediaSettings.defaultAnnouncementDurationUnit 'minutes')}}selected{{/if}}>Minutes</option></select></div></div>
</div>
<div class="col-12"><hr class="my-2"><h4 class="h6 text-uppercase text-muted mb-0">Data source defaults</h4></div>
<div class="col-12 col-md-6">
<label class="form-label" for="settings-rss-interval-value">RSS feed refresh interval</label>
<div class="row g-2"><div class="col-7"><input id="settings-rss-interval-value" name="rss_default_interval_value" type="number" class="form-control" min="1" max="86400" required value="{{mediaSettings.rssDefaultIntervalValue}}"></div><div class="col-5"><select name="rss_default_interval_unit" class="form-select" required><option value="seconds" {{#if (eq mediaSettings.rssDefaultIntervalUnit 'seconds')}}selected{{/if}}>Seconds</option><option value="minutes" {{#if (eq mediaSettings.rssDefaultIntervalUnit 'minutes')}}selected{{/if}}>Minutes</option><option value="hours" {{#if (eq mediaSettings.rssDefaultIntervalUnit 'hours')}}selected{{/if}}>Hours</option></select></div></div>
</div>
<div class="col-12 col-md-6">
<label class="form-label" for="settings-api-interval-value">API source refresh interval</label>
<div class="row g-2"><div class="col-7"><input id="settings-api-interval-value" name="api_default_interval_value" type="number" class="form-control" min="1" max="86400" required value="{{mediaSettings.apiDefaultIntervalValue}}"></div><div class="col-5"><select name="api_default_interval_unit" class="form-select" required><option value="seconds" {{#if (eq mediaSettings.apiDefaultIntervalUnit 'seconds')}}selected{{/if}}>Seconds</option><option value="minutes" {{#if (eq mediaSettings.apiDefaultIntervalUnit 'minutes')}}selected{{/if}}>Minutes</option><option value="hours" {{#if (eq mediaSettings.apiDefaultIntervalUnit 'hours')}}selected{{/if}}>Hours</option></select></div></div>
</div>
</div>
</div>
<div class="card-footer d-flex justify-content-end">{{#if (hasPermission currentUser 'system-settings.update')}}<button type="submit" class="btn btn-success">Save</button>{{else}}<button type="button" class="btn btn-outline-success" disabled>Save</button>{{/if}}</div>
</div>
</form>
<form method="post" action="/settings/system" data-async-save data-settings-form="icons">
<input type="hidden" name="settings_section" value="icons">
<div id="icon-suggestions" class="card settings-section-card" data-settings-section hidden>
<div class="card-header d-flex align-items-start justify-content-between gap-3">
<div class="min-w-0">
<h3 class="h5 mb-1 fw-semibold"><i class="bi bi-grid-3x3-gap me-2 text-primary" aria-hidden="true"></i>Icon Suggestions</h3>
<p class="text-muted small mb-0">Choose up to 48 icons shown first in icon pickers.</p>
</div>
</div>
<div class="card-body">
<div class="row g-3" data-selected-icon-list>
{{#each suggestedAnnouncementIcons}}
{{#if isSelected}}
<div class="col-12 col-sm-6 col-xl-3" draggable="true" data-selected-icon-item data-icon-key="{{value}}">
<div class="d-flex align-items-center gap-2 border rounded p-2 h-100">
<button type="button" class="btn btn-link text-body-secondary p-1" data-drag-icon title="Drag to reorder" aria-label="Drag {{label}} to reorder"><i class="bi bi-grip-vertical" aria-hidden="true"></i></button>
<i class="bi bi-{{value}} fs-5 text-primary" aria-hidden="true"></i>
<span class="flex-grow-1">{{label}}</span>
<input type="hidden" name="suggested_icons[]" value="{{value}}" data-ordered-icon-input>
<button type="button" class="btn btn-link text-danger p-1" data-remove-icon title="Remove {{label}}" aria-label="Remove {{label}}"><i class="bi bi-x-lg" aria-hidden="true"></i></button>
</div>
</div>
{{/if}}
{{/each}}
<span class="col-12 text-muted small" data-no-selected-icons hidden>No icons selected.</span>
</div>
<div class="d-flex flex-wrap align-items-center gap-3 mt-3">
<button type="button" class="btn btn-outline-primary btn-sm" data-add-icon-button data-bs-toggle="modal" data-bs-target="#icon-suggestions-modal"><i class="bi bi-plus-lg me-1" aria-hidden="true"></i>Add icon</button>
<span class="text-muted small"><strong data-icon-suggestions-count>0</strong> of 48 selected</span>
</div>
</div>
<div class="card-footer d-flex justify-content-end">
{{#if (hasPermission currentUser 'system-settings.update')}}
<button type="submit" class="btn btn-success">Save</button>
{{else}}
<button type="button" class="btn btn-outline-success" disabled>Save</button>
{{/if}}
</div>
</div>
</form>
<form method="post" action="/settings/system" data-async-save data-settings-form="weather">
<input type="hidden" name="settings_section" value="weather">
<div id="weather-providers" class="card settings-section-card" data-settings-section hidden>
<div class="card-header d-flex align-items-start justify-content-between gap-3">
<div class="min-w-0">
<h3 class="h5 mb-1 fw-semibold"><i class="bi bi-cloud-sun me-2 text-primary" aria-hidden="true"></i>Weather Providers</h3>
<p class="text-muted small mb-0">Manage shared credentials used by weather locations.</p>
</div>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12"><h4 class="h6 text-uppercase text-muted mb-0">Provider credentials</h4></div>
<div class="col-12 col-xl-6">
<div class="border rounded p-3 h-100">
<div class="d-flex align-items-start justify-content-between gap-2 mb-3"><div><strong>Open-Meteo</strong><div class="small text-body-secondary">Global forecast provider · API key optional</div></div>{{#if mediaSettings.weatherOpenMeteoConfigured}}<span class="badge text-bg-success flex-shrink-0">Configured</span>{{else}}<span class="badge text-bg-secondary flex-shrink-0">Keyless</span>{{/if}}</div>
<label for="settings-open-meteo-key" class="form-label">API key</label>
<input id="settings-open-meteo-key" name="open_meteo_api_key" type="password" class="form-control" autocomplete="off" placeholder="{{#if mediaSettings.weatherOpenMeteoConfigured}}{{mediaSettings.weatherOpenMeteoKeyLabel}}{{else}}Optional API key{{/if}}">
<div class="form-text">Open-Meteo can be used without a key. Leave blank to keep the existing credential.</div>
<a class="small" href="https://open-meteo.com/" target="_blank" rel="noreferrer">open-meteo.com</a>
</div>
</div>
<div class="col-12 col-xl-6">
<div class="border rounded p-3 h-100">
<div class="d-flex align-items-start justify-content-between gap-2 mb-3"><div><strong>Pirate Weather</strong><div class="small text-body-secondary">Global forecast provider · API key required</div></div>{{#if mediaSettings.weatherPirateWeatherConfigured}}<span class="badge text-bg-success flex-shrink-0">Configured</span>{{else}}<span class="badge text-bg-secondary flex-shrink-0">Not configured</span>{{/if}}</div>
<label for="settings-pirate-weather-key" class="form-label">API key</label>
<input id="settings-pirate-weather-key" name="pirate_weather_api_key" type="password" class="form-control" autocomplete="off" placeholder="{{#if mediaSettings.weatherPirateWeatherConfigured}}{{mediaSettings.weatherPirateWeatherKeyLabel}}{{else}}Enter API key{{/if}}">
<div class="form-text">Leave blank to keep the existing credential. One key is shared by all weather locations.</div>
<a class="small" href="https://pirateweather.net/" target="_blank" rel="noreferrer">pirateweather.net</a>
</div>
</div>
</div>
</div>
<div class="card-footer d-flex justify-content-end">
{{#if (hasPermission currentUser 'system-settings.update')}}<button type="submit" class="btn btn-success"><i class="bi bi-check2 me-1"></i>Save</button>{{else}}<button type="button" class="btn btn-outline-success" disabled>Save</button>{{/if}}
</div>
</div>
</form>
<form method="post" action="/settings/system" data-async-save data-settings-form="media">
<input type="hidden" name="settings_section" value="media">
<div id="media-uploads" class="card settings-section-card" data-settings-section hidden>
<div class="card-header d-flex align-items-start justify-content-between gap-3">
<div class="min-w-0">
<h3 class="h5 mb-1 fw-semibold"><i class="bi bi-cloud-arrow-up me-2 text-primary" aria-hidden="true"></i>Media Uploads</h3>
<p class="text-muted small mb-0">Set the maximum file sizes accepted by the slide and template upload flows.</p>
</div>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12 col-md-4">
<label class="form-label" for="settings-image-max">Image max</label>
<div class="input-group">
<input id="settings-image-max" name="image_max_mb" type="number" class="form-control" min="1" max="1024" required value="{{mediaSettings.imageMaxMb}}">
<span class="input-group-text">MB</span>
</div>
<div class="form-text">Between 1 MB and 1 GB.</div>
</div>
<div class="col-12 col-md-4">
<label class="form-label" for="settings-video-max">Video max</label>
<div class="input-group">
<input id="settings-video-max" name="video_max_mb" type="number" class="form-control" min="1" max="4096" required value="{{mediaSettings.videoMaxMb}}">
<span class="input-group-text">MB</span>
</div>
<div class="form-text">Between 1 MB and 4 GB.</div>
</div>
<div class="col-12 col-md-4">
<label class="form-label" for="settings-wysiwyg-image-max">Rich text image max</label>
<div class="input-group">
<input id="settings-wysiwyg-image-max" name="wysiwyg_image_max_mb" type="number" class="form-control" min="0.1" max="1024" step="0.1" required value="{{mediaSettings.wysiwygImageMaxMb}}">
<span class="input-group-text">MB</span>
</div>
<div class="form-text">0.1 MB minimum; no larger than Image max.</div>
</div>
<div class="col-12">
<label class="form-label">Allowed upload types</label>
<div class="row g-3">
<div class="col-12 col-lg-6">
<div class="small text-muted fw-semibold mb-2">Images</div>
<div class="d-flex flex-wrap gap-2">
{{#each mediaSettings.imageMimeTypes}}
<input class="btn-check" type="checkbox" id="mime-{{value}}" name="allowed_mime_types[]" value="{{value}}" {{#if isSelected}}checked{{/if}}>
<label class="btn btn-outline-primary" for="mime-{{value}}">{{label}}</label>
{{/each}}
</div>
</div>
<div class="col-12 col-lg-6">
<div class="small text-muted fw-semibold mb-2">Video</div>
<div class="d-flex flex-wrap gap-2">
{{#each mediaSettings.videoMimeTypes}}
<input class="btn-check" type="checkbox" id="mime-{{value}}" name="allowed_mime_types[]" value="{{value}}" {{#if isSelected}}checked{{/if}}>
<label class="btn btn-outline-primary" for="mime-{{value}}">{{label}}</label>
{{/each}}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card-footer d-flex justify-content-end">
{{#if (hasPermission currentUser 'system-settings.update')}}
<button type="submit" class="btn btn-success">Save</button>
{{else}}
<button type="button" class="btn btn-outline-success" disabled>Save</button>
{{/if}}
</div>
</div>
</form>
<form method="post" action="/settings/system" data-async-save data-settings-form="security">
<input type="hidden" name="settings_section" value="security">
<div id="security-sessions" class="card settings-section-card" data-settings-section hidden>
<div class="card-header d-flex align-items-start justify-content-between gap-3">
<div class="min-w-0">
<h3 class="h5 mb-1 fw-semibold"><i class="bi bi-shield-lock me-2 text-primary" aria-hidden="true"></i>Security and Sessions</h3>
<p class="text-muted small mb-0">Manage sessions, password restrictions, and account protection.</p>
</div>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12"><h4 class="h6 text-uppercase text-muted mb-0">Sessions</h4></div>
<div class="col-12 col-md-6">
<label class="form-label" for="settings-session-lifetime">Session lifetime</label>
<div class="input-group">
<input id="settings-session-lifetime" name="session_lifetime_days" type="number" class="form-control" min="1" max="365" required value="{{mediaSettings.sessionLifetimeDays}}">
<span class="input-group-text">days</span>
</div>
<div class="form-text">Applies to new logins. Existing sessions keep their current expiry.</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label" for="settings-max-active-sessions">Maximum active sessions</label>
<div class="input-group">
<input id="settings-max-active-sessions" name="max_active_sessions" type="number" class="form-control" min="0" max="100" required value="{{mediaSettings.maxActiveSessions}}">
<span class="input-group-text">sessions</span>
</div>
<div class="form-text">Use 0 for unlimited. Oldest sessions are removed first.</div>
</div>
<div class="col-12">
<div class="form-check form-switch">
<input id="settings-allow-session-revocation" name="allow_user_session_revocation" type="checkbox" class="form-check-input" value="1" {{#if mediaSettings.allowUserSessionRevocation}}checked{{/if}}>
<label class="form-check-label" for="settings-allow-session-revocation">Allow users to sign out their other active sessions</label>
</div>
</div>
<div class="col-12"><hr class="my-2"><h4 class="h6 text-uppercase text-muted mb-0">Login protection</h4></div>
<div class="col-12 col-md-4">
<label class="form-label" for="settings-login-attempts">Failed attempts before lockout</label>
<input id="settings-login-attempts" name="login_max_attempts" type="number" class="form-control" min="1" max="100" required value="{{mediaSettings.loginMaxAttempts}}">
</div>
<div class="col-12 col-md-4">
<label class="form-label" for="settings-login-lockout">Lockout duration</label>
<div class="input-group">
<input id="settings-login-lockout" name="login_lockout_minutes" type="number" class="form-control" min="1" max="1440" required value="{{mediaSettings.loginLockoutMinutes}}">
<span class="input-group-text">minutes</span>
</div>
</div>
<div class="col-12 col-md-4">
<label class="form-label" for="settings-login-scope">Track failed logins by</label>
<select id="settings-login-scope" name="login_rate_limit_scope" class="form-select" required>
<option value="both" {{#if (eq mediaSettings.loginRateLimitScope 'both')}}selected{{/if}}>Username and IP address</option>
<option value="username" {{#if (eq mediaSettings.loginRateLimitScope 'username')}}selected{{/if}}>Username</option>
<option value="ip" {{#if (eq mediaSettings.loginRateLimitScope 'ip')}}selected{{/if}}>IP address</option>
</select>
<div class="form-text">Failed login responses remain generic.</div>
</div>
<div class="col-12"><hr class="my-2"><h4 class="h6 text-uppercase text-muted mb-0">Password and account protection</h4></div>
<div class="col-12 col-md-6 col-xl-3">
<label class="form-label" for="settings-password-min-length">Minimum length</label>
<input id="settings-password-min-length" name="password_min_length" type="number" class="form-control" min="8" max="128" required value="{{mediaSettings.passwordMinLength}}">
</div>
<div class="col-12 col-md-6 col-xl-3">
<label class="form-label" for="settings-password-min-categories">Minimum required categories</label>
<input id="settings-password-min-categories" name="password_min_categories" type="number" class="form-control" min="1" max="4" required value="{{mediaSettings.passwordMinCategories}}">
<div class="form-text">Lowercase, uppercase, number, or symbol.</div>
</div>
<div class="col-12 col-xl-6">
<label class="form-label">Always require</label>
<div class="d-flex flex-wrap gap-3">
<input id="settings-password-lowercase" name="password_require_lowercase" type="checkbox" class="btn-check" value="1" autocomplete="off" {{#if mediaSettings.passwordRequireLowercase}}checked{{/if}}>
<label class="btn btn-outline-primary" for="settings-password-lowercase">Lowercase</label>
<input id="settings-password-uppercase" name="password_require_uppercase" type="checkbox" class="btn-check" value="1" autocomplete="off" {{#if mediaSettings.passwordRequireUppercase}}checked{{/if}}>
<label class="btn btn-outline-primary" for="settings-password-uppercase">Uppercase</label>
<input id="settings-password-number" name="password_require_number" type="checkbox" class="btn-check" value="1" autocomplete="off" {{#if mediaSettings.passwordRequireNumber}}checked{{/if}}>
<label class="btn btn-outline-primary" for="settings-password-number">Number</label>
<input id="settings-password-symbol" name="password_require_symbol" type="checkbox" class="btn-check" value="1" autocomplete="off" {{#if mediaSettings.passwordRequireSymbol}}checked{{/if}}>
<label class="btn btn-outline-primary" for="settings-password-symbol">Symbol</label>
</div>
</div>
<div class="col-12">
<div class="form-check form-switch mb-2">
<input id="settings-require-password-new" name="require_password_change_for_new_users" type="checkbox" class="form-check-input" value="1" {{#if mediaSettings.requirePasswordChangeForNewUsers}}checked{{/if}}>
<label class="form-check-label" for="settings-require-password-new">Require password change for newly created users</label>
</div>
<div class="form-check form-switch">
<input id="settings-require-password-reset" name="require_password_change_after_admin_reset" type="checkbox" class="form-check-input" value="1" {{#if mediaSettings.requirePasswordChangeAfterAdminReset}}checked{{/if}}>
<label class="form-check-label" for="settings-require-password-reset">Require password change after an administrator reset</label>
</div>
</div>
</div>
</div>
<div class="card-footer d-flex justify-content-end">
{{#if (hasPermission currentUser 'system-settings.update')}}
<button type="submit" class="btn btn-success">Save</button>
{{else}}
<button type="button" class="btn btn-outline-success" disabled>Save</button>
{{/if}}
</div>
</div>
</form>
<div id="diagnostics" class="card settings-section-card" data-settings-section hidden>
<div class="card-header d-flex align-items-start justify-content-between gap-3">
<div class="min-w-0">
<h3 class="h5 mb-1 fw-semibold"><i class="bi bi-activity me-2 text-primary" aria-hidden="true"></i>Diagnostics</h3>
<p class="text-muted small mb-0">Read-only application, database, data, session, and task health information.</p>
</div>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12 col-md-4"><div class="text-body-secondary small">Application version</div><div class="fw-semibold">{{diagnostics.applicationVersion}}</div></div>
<div class="col-12 col-md-4"><div class="text-body-secondary small">Schema version</div><div class="fw-semibold">{{diagnostics.schemaVersion}}</div></div>
<div class="col-12 col-md-4"><div class="text-body-secondary small">Database</div><div class="fw-semibold">{{diagnostics.databaseStatus}}</div></div>
<div class="col-12 col-md-6"><div class="text-body-secondary small">Active sessions</div><div class="fw-semibold">{{diagnostics.activeSessionCount}}</div></div>
<div class="col-12 col-md-6"><div class="text-body-secondary small">Background tasks</div><div class="fw-semibold">{{diagnostics.backgroundTaskCount}}</div></div>
<div class="col-12"><hr class="my-2"><h4 class="h6 text-uppercase text-muted mb-0">Table counts</h4></div>
{{#each diagnostics.criticalTableCounts}}
<div class="col-6 col-md-4 col-xl-3"><div class="text-body-secondary small">{{label}}</div><div class="fw-semibold">{{count}}</div></div>
{{/each}}
</div>
</div>
</div>
<form method="post" action="/settings/system" data-async-save data-settings-form="audit">
<input type="hidden" name="settings_section" value="audit">
<div id="audit-logging" class="card settings-section-card" data-settings-section hidden>
<div class="card-header d-flex align-items-start justify-content-between gap-3">
<div class="min-w-0">
<h3 class="h5 mb-1 fw-semibold"><i class="bi bi-journal-text me-2 text-primary" aria-hidden="true"></i>Audit Logging</h3>
<p class="text-muted small mb-0">Retain security, administration, content, and data-source events for review.</p>
</div>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12">
<div class="form-check form-switch">
<input id="settings-audit-enabled" name="audit_enabled" type="checkbox" class="form-check-input" value="1" {{#if mediaSettings.auditEnabled}}checked{{/if}}>
<label class="form-check-label" for="settings-audit-enabled">Enable audit logging</label>
</div>
</div>
<div class="col-12"><hr class="my-2"><h4 class="h6 text-uppercase text-muted mb-0">Audit controls</h4></div>
<div class="col-12">
<div class="form-check form-switch">
<input id="settings-audit-metadata" name="include_request_metadata" type="checkbox" class="form-check-input" value="1" {{#if mediaSettings.auditIncludeRequestMetadata}}checked{{/if}}>
<label class="form-check-label" for="settings-audit-metadata">Store IP address and user agent</label>
</div>
<div class="form-text fst-italic">Disable this for reduced request metadata storage.</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label" for="settings-audit-retention">Retain audit events for</label>
<div class="input-group settings-audit-retention-input">
<input id="settings-audit-retention" name="audit_retention_days" type="number" class="form-control" min="0" max="3650" required value="{{mediaSettings.auditRetentionDays}}">
<span class="input-group-text">days</span>
</div>
<div class="form-text fst-italic">Use 0 to retain events indefinitely. Cleanup runs daily.</div>
</div>
<div class="col-12"><hr class="my-2"><h4 class="h6 text-uppercase text-muted mb-0">Audited events</h4></div>
<div class="col-12">
<h4 class="settings-audit-category-label mb-1">General events</h4>
<p class="text-muted small fst-italic mb-2">Authentication, security, and session activity.</p>
<div class="d-flex flex-wrap gap-3">
{{#each mediaSettings.auditEventCategories}}
<div class="form-check"><input id="settings-audit-event-{{key}}" name="audit_categories[]" type="checkbox" class="form-check-input" value="{{key}}" {{#if isSelected}}checked{{/if}}><label class="form-check-label text-capitalize" for="settings-audit-event-{{key}}">{{label}}</label></div>
{{/each}}
</div>
</div>
<div class="col-12">
<h4 class="settings-audit-category-label mb-1">Administrative changes</h4>
<p class="text-muted small fst-italic mb-2">Changes to users, roles, and system settings.</p>
<div class="d-flex flex-wrap gap-3">
{{#each mediaSettings.auditAdministrationCategories}}
<div class="form-check"><input id="settings-audit-administration-{{key}}" name="audit_categories[]" type="checkbox" class="form-check-input" value="{{key}}" {{#if isSelected}}checked{{/if}}><label class="form-check-label text-capitalize" for="settings-audit-administration-{{key}}">{{label}}</label></div>
{{/each}}
</div>
</div>
<div class="col-12">
<h4 class="settings-audit-category-label mb-1">Content</h4>
<p class="text-muted small fst-italic mb-2">Changes to individual content and screen types.</p>
<div class="d-flex flex-wrap gap-3">
{{#each mediaSettings.auditContentCategories}}
<div class="form-check"><input id="settings-audit-content-{{key}}" name="audit_categories[]" type="checkbox" class="form-check-input" value="{{key}}" {{#if isSelected}}checked{{/if}}><label class="form-check-label text-capitalize" for="settings-audit-content-{{key}}">{{label}}</label></div>
{{/each}}
</div>
</div>
<div class="col-12">
<h4 class="settings-audit-category-label mb-1">Data sources</h4>
<p class="text-muted small fst-italic mb-2">Configuration changes only; automatic feed refreshes are not logged.</p>
<div class="d-flex flex-wrap gap-3">
{{#each mediaSettings.auditDataSourceCategories}}
<div class="form-check"><input id="settings-audit-data-source-{{key}}" name="audit_categories[]" type="checkbox" class="form-check-input" value="{{key}}" {{#if isSelected}}checked{{/if}}><label class="form-check-label text-capitalize" for="settings-audit-data-source-{{key}}">{{label}}</label></div>
{{/each}}
</div>
</div>
</div>
</div>
<div class="card-footer d-flex justify-content-end">
{{#if (hasPermission currentUser 'system-settings.update')}}<button type="submit" class="btn btn-success">Save</button>{{else}}<button type="button" class="btn btn-outline-success" disabled>Save</button>{{/if}}
</div>
</div>
</form>
<div class="modal fade" id="icon-suggestions-modal" tabindex="-1" aria-labelledby="icon-suggestions-modal-title" aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable modal-lg">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title fs-5" id="icon-suggestions-modal-title">Add suggested icons</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<label class="visually-hidden" for="icon-suggestions-search">Search icons</label>
<div class="input-group mb-3">
<span class="input-group-text" aria-hidden="true"><i class="bi bi-search"></i></span>
<input id="icon-suggestions-search" class="form-control" type="search" placeholder="Search all Bootstrap icons" data-icon-suggestions-search>
</div>
<div class="row g-2">
{{#each suggestedAnnouncementIcons}}
<div class="col-12 col-sm-6 col-lg-4 icon-suggestion-option" data-icon-suggestion-option data-icon-label="{{label}}">
<label class="d-flex align-items-center gap-2 border rounded px-3 py-2 h-100" for="suggested-icon-{{value}}">
<input class="form-check-input mt-0" type="checkbox" id="suggested-icon-{{value}}" value="{{value}}" data-icon-picker-checkbox {{#if isSelected}}checked{{/if}}>
<i class="bi bi-{{value}} fs-5 text-primary" aria-hidden="true"></i>
<span>{{label}}</span>
</label>
</div>
{{/each}}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Done</button>
</div>
</div>
</div>
</div>
</div>
</div>