fix: merge main branch and renumber migration 027→037
Resolve 3 merge conflicts and renumber the automation_hooks migration to follow main's migration chain (036_add_document_translation_fields). Conflicts resolved: - app/api/__init__.py: add automation_router alongside main's new routers - app/utils/settings_service.py: add automation_hooks_enabled alongside compliance_enabled - tests/conftest.py: add AutomationHook alongside AuditLog/ComplianceTemplate imports Migration renumbered: - 027_add_automation_hooks → 037_add_automation_hooks - down_revision: 026_add_scheduled_jobs → 036_add_document_translation_fields Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Settings - DocuElevate{% endblock %}
|
||||
{% block title %}{{ _("settings.title") }} - DocuElevate{% endblock %}
|
||||
|
||||
{# Category icon mapping #}
|
||||
{% set category_icons = {
|
||||
@@ -48,27 +48,27 @@
|
||||
<div class="mb-6">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start gap-4 mb-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold mb-1">Application Settings</h1>
|
||||
<h1 class="text-3xl font-bold mb-1">{{ _("settings.page_heading") }}</h1>
|
||||
<p class="text-gray-500 text-sm">
|
||||
Manage configuration. Priority: <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">DB</span> >
|
||||
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 ml-1">ENV</span> >
|
||||
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800 ml-1">DEFAULT</span>
|
||||
<span class="mx-1.5">·</span> <span class="text-red-600">*</span> = restart required <span class="mx-1.5">·</span> <i class="fas fa-lock text-xs" aria-hidden="true"></i> = encrypted at rest
|
||||
{{ _("settings.manage_config_prefix") }} <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">{{ _("settings.source_db_badge") }}</span> >
|
||||
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 ml-1">{{ _("settings.source_env_badge") }}</span> >
|
||||
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800 ml-1">{{ _("settings.source_default_badge") }}</span>
|
||||
<span class="mx-1.5">·</span> <span class="text-red-600">*</span> {{ _("settings.restart_required_suffix") }} <span class="mx-1.5">·</span> <i class="fas fa-lock text-xs" aria-hidden="true"></i> {{ _("settings.encrypted_suffix") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<a href="/setup?step=1"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
<i class="fas fa-magic mr-1.5" aria-hidden="true"></i> Wizard
|
||||
<i class="fas fa-magic mr-1.5" aria-hidden="true"></i> {{ _("settings.wizard_btn") }}
|
||||
</a>
|
||||
<a href="/database-wizard"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
<i class="fas fa-database mr-1.5" aria-hidden="true"></i> DB Wizard
|
||||
<i class="fas fa-database mr-1.5" aria-hidden="true"></i> {{ _("settings.db_wizard_btn") }}
|
||||
</a>
|
||||
<div class="relative" x-data="{ exportOpen: false }">
|
||||
<button @click="exportOpen = !exportOpen"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
<i class="fas fa-download mr-1.5" aria-hidden="true"></i> Export
|
||||
<i class="fas fa-download mr-1.5" aria-hidden="true"></i> {{ _("settings.export_btn") }}
|
||||
<i class="fas fa-chevron-down ml-1 text-xs" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div x-show="exportOpen" @click.outside="exportOpen = false" x-transition
|
||||
@@ -76,18 +76,18 @@
|
||||
<a href="/api/settings/export-env?source=db"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50"
|
||||
@click="exportOpen = false">
|
||||
<i class="fas fa-database mr-2 text-green-600" aria-hidden="true"></i>DB settings only
|
||||
<i class="fas fa-database mr-2 text-green-600" aria-hidden="true"></i>{{ _("settings.export_db_only") }}
|
||||
</a>
|
||||
<a href="/api/settings/export-env?source=effective"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50"
|
||||
@click="exportOpen = false">
|
||||
<i class="fas fa-layer-group mr-2 text-blue-600" aria-hidden="true"></i>Full effective config
|
||||
<i class="fas fa-layer-group mr-2 text-blue-600" aria-hidden="true"></i>{{ _("settings.export_full_config") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/settings/audit-log"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<i class="fas fa-history mr-1.5" aria-hidden="true"></i> Audit Log
|
||||
<i class="fas fa-history mr-1.5" aria-hidden="true"></i> {{ _("settings.audit_log_btn") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,14 +99,14 @@
|
||||
type="search"
|
||||
x-model="searchQuery"
|
||||
@input="filterSettings()"
|
||||
placeholder="Search settings by name, key, or description…"
|
||||
placeholder="{{ _('settings.search_placeholder') }}"
|
||||
class="w-full pl-10 pr-20 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
||||
aria-label="Search settings"
|
||||
aria-label="{{ _('settings.search_aria_label') }}"
|
||||
/>
|
||||
<div class="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-2">
|
||||
<span x-show="searchQuery" x-text="matchCount + ' found'" x-transition class="text-xs text-gray-400"></span>
|
||||
<button x-show="searchQuery" @click="searchQuery = ''; filterSettings()" type="button"
|
||||
class="text-gray-400 hover:text-gray-600" aria-label="Clear search">
|
||||
class="text-gray-400 hover:text-gray-600" aria-label="{{ _('settings.search_clear_aria') }}">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -125,9 +125,9 @@
|
||||
<!-- Two-column layout -->
|
||||
<div class="flex gap-6">
|
||||
<!-- Sidebar Navigation (hidden on small screens) -->
|
||||
<aside class="hidden lg:block w-56 flex-shrink-0" aria-label="Settings categories">
|
||||
<aside class="hidden lg:block w-56 flex-shrink-0" aria-label="{{ _('settings.categories_aria') }}">
|
||||
<nav class="settings-sidebar space-y-0.5 bg-white rounded-lg border border-gray-200 p-2 shadow-sm">
|
||||
<p class="px-3 py-1.5 text-xs font-semibold text-gray-400 uppercase tracking-wider">Categories</p>
|
||||
<p class="px-3 py-1.5 text-xs font-semibold text-gray-400 uppercase tracking-wider">{{ _("settings.categories_heading") }}</p>
|
||||
{% for category, settings_list in settings_data.items() %}
|
||||
<button
|
||||
type="button"
|
||||
@@ -149,11 +149,11 @@
|
||||
<div class="flex-1 min-w-0">
|
||||
<!-- Mobile category jump (visible on small screens only) -->
|
||||
<div class="lg:hidden mb-4">
|
||||
<label for="mobile-category-select" class="sr-only">Jump to category</label>
|
||||
<label for="mobile-category-select" class="sr-only">{{ _("settings.jump_to_category") }}</label>
|
||||
<select id="mobile-category-select"
|
||||
@change="scrollToCategory($event.target.value)"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm shadow-sm">
|
||||
<option value="">Jump to category…</option>
|
||||
<option value="">{{ _("settings.jump_to_category") }}</option>
|
||||
{% for category in settings_data.keys() %}
|
||||
<option value="{{ category }}">{{ category }} ({{ settings_data[category]|length }})</option>
|
||||
{% endfor %}
|
||||
@@ -163,8 +163,8 @@
|
||||
<!-- No results state -->
|
||||
<div x-show="searchQuery && matchCount === 0" class="settings-no-results bg-white rounded-lg border border-gray-200 shadow-sm">
|
||||
<i class="fas fa-search text-4xl text-gray-300 mb-3"></i>
|
||||
<p class="text-gray-500 text-lg font-medium">No settings found</p>
|
||||
<p class="text-gray-400 text-sm mt-1">Try a different search term or <button type="button" @click="searchQuery = ''; filterSettings()" class="text-blue-500 hover:underline">clear the search</button></p>
|
||||
<p class="text-gray-500 text-lg font-medium">{{ _("settings.no_results_heading") }}</p>
|
||||
<p class="text-gray-400 text-sm mt-1">{{ _("settings.no_results_hint") }} <button type="button" @click="searchQuery = ''; filterSettings()" class="text-blue-500 hover:underline">{{ _("settings.no_results_clear") }}</button></p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="saveSettings">
|
||||
@@ -191,7 +191,7 @@
|
||||
<h2 class="text-lg font-semibold text-gray-800">{{ category }}</h2>
|
||||
<span class="text-xs text-gray-400 bg-gray-100 px-2 py-0.5 rounded-full tabular-nums"
|
||||
x-text="(visibleCount['{{ category|e }}'] ?? {{ settings_list|length }}) + ' settings'">
|
||||
{{ settings_list|length }} settings
|
||||
{{ settings_list|length }} {{ _("settings.settings_count_suffix") }}
|
||||
</span>
|
||||
</div>
|
||||
<i class="fas fa-chevron-down text-gray-400 category-chevron"
|
||||
@@ -225,7 +225,7 @@
|
||||
<span class="text-red-600" title="Restart required">*</span>
|
||||
{% endif %}
|
||||
{% if setting.metadata.required %}
|
||||
<span class="text-red-600 text-xs">(required)</span>
|
||||
<span class="text-red-600 text-xs">{{ _("settings.required_label") }}</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
<!-- Source Badge -->
|
||||
@@ -262,7 +262,7 @@
|
||||
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label for="{{ setting.key }}" class="ml-2 text-sm text-gray-700">
|
||||
Enable {{ setting.key.replace('_', ' ').title() }}
|
||||
{{ _("settings.boolean_enable_prefix") }} {{ setting.key.replace('_', ' ').title() }}
|
||||
</label>
|
||||
</div>
|
||||
{% elif setting.metadata.type == 'slider' %}
|
||||
@@ -302,7 +302,7 @@
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-400">Effective value: <code x-text="formData['{{ setting.key }}'] || '(none)'"></code></p>
|
||||
<p class="mt-1 text-xs text-gray-400">{{ _("settings.effective_value_label") }} <code x-text="formData['{{ setting.key }}'] || '(none)'"></code></p>
|
||||
{% elif setting.metadata.type == 'user_autocomplete' %}
|
||||
<!-- User Autocomplete (WCAG 2.1 AA combobox pattern) -->
|
||||
<div class="relative" x-data="userAutocomplete('{{ setting.key }}')" @click.away="showSuggestions = false">
|
||||
@@ -320,7 +320,7 @@
|
||||
@keydown.enter.prevent="selectHighlighted()"
|
||||
@keydown.escape="showSuggestions = false"
|
||||
class="setting-input w-full px-3 py-2 pl-9 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="Start typing to search users…"
|
||||
placeholder="{{ _('settings.user_autocomplete_placeholder') }}"
|
||||
autocomplete="off"
|
||||
role="combobox"
|
||||
aria-autocomplete="list"
|
||||
@@ -371,11 +371,11 @@
|
||||
class="absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-md shadow-lg px-3 py-2 text-sm text-gray-500"
|
||||
role="status"
|
||||
>
|
||||
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i> No matching users found
|
||||
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i> {{ _("settings.user_autocomplete_empty") }}
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-1" id="{{ setting.key }}_help">
|
||||
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i>
|
||||
Type to search existing users by name, or enter any identifier manually.
|
||||
{{ _("settings.user_autocomplete_hint") }}
|
||||
</p>
|
||||
</div>
|
||||
{% elif setting.metadata.type == 'autocomplete' %}
|
||||
@@ -395,7 +395,7 @@
|
||||
@keydown.enter.prevent="selectHighlighted()"
|
||||
@keydown.escape="showSuggestions = false"
|
||||
class="setting-input w-full px-3 py-2 pl-9 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="Type to search or enter a value…"
|
||||
placeholder="{{ _('settings.autocomplete_placeholder') }}"
|
||||
autocomplete="off"
|
||||
role="combobox"
|
||||
aria-autocomplete="list"
|
||||
@@ -445,11 +445,11 @@
|
||||
class="absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-md shadow-lg px-3 py-2 text-sm text-gray-500"
|
||||
role="status"
|
||||
>
|
||||
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i> No matches — you can still type a custom value
|
||||
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i> {{ _("settings.autocomplete_no_matches") }}
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i>
|
||||
Type to search known values, or enter any custom value.
|
||||
{{ _("settings.autocomplete_hint") }}
|
||||
</p>
|
||||
</div>
|
||||
{% elif setting.metadata.type == 'model_picker' %}
|
||||
@@ -462,7 +462,7 @@
|
||||
list="{{ setting.key }}_models"
|
||||
x-model="formData['{{ setting.key }}']"
|
||||
class="setting-input w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="Select a common model or type a custom name…"
|
||||
placeholder="{{ _('settings.model_picker_placeholder') }}"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<datalist id="{{ setting.key }}_models">
|
||||
@@ -472,7 +472,7 @@
|
||||
</datalist>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i>
|
||||
Pick from the list or type any model name supported by your provider.
|
||||
{{ _("settings.model_picker_hint") }}
|
||||
</p>
|
||||
</div>
|
||||
{% elif setting.metadata.options %}
|
||||
@@ -483,7 +483,7 @@
|
||||
x-model="formData['{{ setting.key }}']"
|
||||
class="setting-input w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="">— select —</option>
|
||||
<option value="">{{ _("common.select_placeholder") }}</option>
|
||||
{% for opt in setting.metadata.options %}
|
||||
<option value="{{ opt }}">{{ opt }}</option>
|
||||
{% endfor %}
|
||||
@@ -503,15 +503,15 @@
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-3 space-x-2">
|
||||
<span class="text-xs text-gray-400" title="Value is encrypted at rest in database">
|
||||
<span class="text-xs text-gray-400" title="{{ _('settings.encrypted_title') }}">
|
||||
<i class="fas fa-lock"></i>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
@click="togglePassword('{{ setting.key }}')"
|
||||
class="text-gray-400 hover:text-gray-600 focus:outline-none"
|
||||
title="Show/hide value"
|
||||
aria-label="Toggle password visibility"
|
||||
title="{{ _('settings.toggle_visibility_title') }}"
|
||||
aria-label="{{ _('settings.toggle_visibility_aria') }}"
|
||||
>
|
||||
<i :class="showPassword['{{ setting.key }}'] ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
|
||||
</button>
|
||||
@@ -540,10 +540,10 @@
|
||||
@click="revertSetting('{{ setting.key }}')"
|
||||
:disabled="revertingKey === '{{ setting.key }}'"
|
||||
class="px-3 py-1 text-sm bg-red-600 text-white rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
|
||||
title="Remove DB override and revert to environment variable or default"
|
||||
title="{{ _('settings.revert_title') }}"
|
||||
>
|
||||
<span x-show="revertingKey !== '{{ setting.key }}'"><i class="fas fa-trash-alt mr-1" aria-hidden="true"></i>Remove from DB</span>
|
||||
<span x-show="revertingKey === '{{ setting.key }}'">Reverting…</span>
|
||||
<span x-show="revertingKey !== '{{ setting.key }}'"><i class="fas fa-trash-alt mr-1" aria-hidden="true"></i>{{ _("settings.revert_btn") }}</span>
|
||||
<span x-show="revertingKey === '{{ setting.key }}'">{{ _("settings.reverting") }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -552,10 +552,10 @@
|
||||
@click="saveSetting('{{ setting.key }}')"
|
||||
:disabled="savingKey === '{{ setting.key }}'"
|
||||
class="px-3 py-1 text-sm bg-green-600 text-white rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
|
||||
title="Save this setting"
|
||||
title="{{ _('settings.save_setting_title') }}"
|
||||
>
|
||||
<span x-show="savingKey !== '{{ setting.key }}'"><i class="fas fa-save mr-1" aria-hidden="true"></i>Save</span>
|
||||
<span x-show="savingKey === '{{ setting.key }}'">Saving…</span>
|
||||
<span x-show="savingKey !== '{{ setting.key }}'"><i class="fas fa-save mr-1" aria-hidden="true"></i>{{ _("common.save") }}</span>
|
||||
<span x-show="savingKey === '{{ setting.key }}'">{{ _("settings.saving_state") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -573,15 +573,15 @@
|
||||
@click="resetForm"
|
||||
class="px-6 py-2 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Reset
|
||||
{{ _("common.reset") }}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="saving"
|
||||
class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span x-show="!saving">Save All Changes</span>
|
||||
<span x-show="saving">Saving...</span>
|
||||
<span x-show="!saving">{{ _("settings.save_all_btn") }}</span>
|
||||
<span x-show="saving">{{ _("settings.saving_state") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user