diff --git a/application/single_app/config.py b/application/single_app/config.py index 39d9a8a90..8dd84f2b2 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -96,7 +96,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.260.007" +VERSION = "0.260.008" IS_DEVELOPMENT = is_development_env_enabled() SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax') diff --git a/application/single_app/static/js/admin/admin_card_links.js b/application/single_app/static/js/admin/admin_card_links.js new file mode 100644 index 000000000..9da7cf139 --- /dev/null +++ b/application/single_app/static/js/admin/admin_card_links.js @@ -0,0 +1,103 @@ +// admin_card_links.js +// Resolves links that point at an Admin Settings card rather than at a tab. +// +// Cross-tab links used to name a tab button directly, for example +// switchTab(event, 'workspaces-tab'). That couples the link to a tab id, so a +// tab rename or an information-architecture change silently breaks the link: +// the button no longer exists, no pane is activated, and the URL hash is left +// pointing at nothing. +// +// A link declares the card it wants instead: +// +// +// +// The owning tab is resolved from the DOM at click time, so card ids are the +// only contract. Cards can move between tabs, and tabs can be renamed or +// regrouped, without touching a single link. + +const HIGHLIGHT_CLASS = 'admin-card-link-target'; +const HIGHLIGHT_MS = 1600; + +/** + * Find the tab pane that contains a card. + * @param {string} cardId Element id of the target card. + * @returns {{card: HTMLElement, paneId: string|null}|null} + */ +export function resolveAdminCard(cardId) { + const card = document.getElementById(cardId); + if (!card) { + return null; + } + + const pane = card.closest('.tab-pane'); + return { card, paneId: pane ? pane.id : null }; +} + +/** + * Activate the tab owning a card, scroll to it, and highlight it briefly. + * @param {string} cardId Element id of the target card. + * @returns {boolean} Whether the card was found. + */ +export function openAdminCard(cardId) { + const resolved = resolveAdminCard(cardId); + if (!resolved) { + console.warn(`openAdminCard: no card with id "${cardId}"`); + return false; + } + + const { card, paneId } = resolved; + + if (paneId && typeof window.showAdminTab === 'function') { + window.showAdminTab(paneId); + syncSidebarActiveState(paneId); + } + + // Let the pane become visible before measuring scroll position. + window.setTimeout(() => { + card.scrollIntoView({ behavior: 'smooth', block: 'start' }); + card.classList.add(HIGHLIGHT_CLASS); + window.setTimeout(() => card.classList.remove(HIGHLIGHT_CLASS), HIGHLIGHT_MS); + }, 120); + + return true; +} + +/** + * Mirror the activated tab in the sidebar, when the sidebar layout is in use. + * @param {string} paneId Tab pane id that was activated. + */ +function syncSidebarActiveState(paneId) { + const navLink = document.querySelector(`.admin-nav-tab[data-tab="${paneId}"]`); + if (!navLink) { + return; + } + + document.querySelectorAll('.admin-nav-tab, .admin-nav-section').forEach((link) => { + link.classList.remove('active'); + }); + navLink.classList.add('active'); +} + +/** + * Delegate clicks so links added after load keep working. + */ +export function initAdminCardLinks() { + document.addEventListener('click', (event) => { + const link = event.target.closest('[data-admin-link]'); + if (!link) { + return; + } + + const cardId = link.getAttribute('data-admin-link'); + if (!cardId) { + return; + } + + event.preventDefault(); + openAdminCard(cardId); + }); +} + +window.openAdminCard = openAdminCard; + +document.addEventListener('DOMContentLoaded', initAdminCardLinks); diff --git a/application/single_app/static/js/admin/admin_settings.js b/application/single_app/static/js/admin/admin_settings.js index 26726f5f3..d30fabf78 100644 --- a/application/single_app/static/js/admin/admin_settings.js +++ b/application/single_app/static/js/admin/admin_settings.js @@ -9227,31 +9227,6 @@ function clearStatusAlert(statusAlert) { } -function switchTab(event, tabButtonId) { - event.preventDefault(); - const triggerEl = document.getElementById(tabButtonId); - if (triggerEl) { - const tabObj = new bootstrap.Tab(triggerEl); - tabObj.show(); - return; - } - - const inferredTabId = tabButtonId.replace(/-tab$/, ''); - if (typeof window.showAdminTab === 'function') { - window.showAdminTab(inferredTabId); - - const navLink = document.querySelector(`.admin-nav-tab[data-tab="${inferredTabId}"]`); - if (navLink) { - document.querySelectorAll('.admin-nav-tab, .admin-nav-section').forEach(link => { - link.classList.remove('active'); - }); - navLink.classList.add('active'); - } - } -} - -window.switchTab = switchTab; - function togglePassword(btnId, inputId) { const btn = document.getElementById(btnId); const inp = document.getElementById(inputId); diff --git a/application/single_app/static/js/admin/admin_settings_dependencies.js b/application/single_app/static/js/admin/admin_settings_dependencies.js new file mode 100644 index 000000000..266490a18 --- /dev/null +++ b/application/single_app/static/js/admin/admin_settings_dependencies.js @@ -0,0 +1,220 @@ +// admin_settings_dependencies.js +// Announces and enforces Admin Settings options that require another option. +// +// Some settings only work when a different setting is enabled, and those two +// settings often live in different tabs. Previously that was communicated in +// prose, or in a tooltip, or only by a flash message after saving, so an admin +// could turn something on and have nothing happen with no visible reason. +// +// A dependent card declares what it needs: +// +//
+// +// When the prerequisite is off, a notice is inserted at the top of the card +// containing a mirror of the prerequisite control and a link to its card, so +// the admin can satisfy it inline or jump to the full configuration. +// +// Modes: +// block (default) disables the dependent inputs until the prerequisite is on +// warn leaves inputs usable, for prerequisites the backend already +// accepts as intent and reconciles later +// +// This is a usability layer only. The backend remains authoritative and still +// refuses or flashes on unmet prerequisites. + +const NOTICE_CLASS = 'admin-dependency-notice'; +const DISABLED_FLAG = 'data-dependency-disabled'; + +/** + * Read every dependency declared in the document. + * @returns {Array} Parsed dependency descriptors. + */ +function collectDependencies() { + return Array.from(document.querySelectorAll('[data-requires]')).map((card) => ({ + card, + prerequisiteId: card.getAttribute('data-requires'), + label: card.getAttribute('data-requires-label') || 'another setting', + targetCardId: card.getAttribute('data-requires-target') || '', + mode: card.getAttribute('data-requires-mode') === 'warn' ? 'warn' : 'block', + description: card.getAttribute('data-requires-description') || '', + // Optional selector limiting which controls the dependency guards, for + // cards that hold a mix of dependent and independent settings. + scope: card.getAttribute('data-requires-scope') || '', + })); +} + +/** + * Whether a prerequisite control is currently satisfied. + * @param {HTMLElement|null} control Prerequisite input. + * @returns {boolean} + */ +function isSatisfied(control) { + if (!control) { + return true; + } + if (control.type === 'checkbox' || control.type === 'radio') { + return control.checked; + } + return Boolean(control.value); +} + +/** + * Build the notice shown when a prerequisite is unmet. + * @param {object} dependency Dependency descriptor. + * @param {HTMLElement} prerequisite Prerequisite control. + * @returns {HTMLElement} + */ +function buildNotice(dependency, prerequisite) { + const notice = document.createElement('div'); + notice.className = `alert ${dependency.mode === 'warn' ? 'alert-warning' : 'alert-info'} ${NOTICE_CLASS}`; + notice.setAttribute('role', 'note'); + + const heading = document.createElement('div'); + heading.className = 'fw-semibold mb-1'; + const icon = document.createElement('i'); + icon.className = 'bi bi-info-circle me-2'; + icon.setAttribute('aria-hidden', 'true'); + heading.append(icon, document.createTextNode(`This needs ${dependency.label}`)); + notice.appendChild(heading); + + const body = document.createElement('p'); + body.className = 'mb-2'; + body.textContent = dependency.description + || (dependency.mode === 'warn' + ? `These settings are saved, but stay inactive until ${dependency.label} is enabled and configured.` + : `These settings are unavailable until ${dependency.label} is enabled.`); + notice.appendChild(body); + + const actions = document.createElement('div'); + actions.className = 'd-flex flex-wrap align-items-center gap-3'; + + // Inline mirror: satisfy the prerequisite without leaving this tab. It + // carries no name attribute, so only the canonical input is submitted. + if (prerequisite && prerequisite.type === 'checkbox') { + const wrapper = document.createElement('div'); + wrapper.className = 'form-check form-switch mb-0'; + + const proxy = document.createElement('input'); + proxy.type = 'checkbox'; + proxy.className = 'form-check-input'; + proxy.id = `${dependency.card.id}-requires-proxy`; + proxy.checked = prerequisite.checked; + proxy.setAttribute('data-dependency-proxy-for', prerequisite.id); + proxy.setAttribute('data-ignore-settings-change', 'true'); + + proxy.addEventListener('change', () => { + prerequisite.checked = proxy.checked; + prerequisite.dispatchEvent(new Event('change', { bubbles: true })); + }); + + const proxyLabel = document.createElement('label'); + proxyLabel.className = 'form-check-label ms-2'; + proxyLabel.setAttribute('for', proxy.id); + proxyLabel.textContent = `Enable ${dependency.label}`; + + wrapper.append(proxy, proxyLabel); + actions.appendChild(wrapper); + } + + if (dependency.targetCardId) { + const link = document.createElement('a'); + link.href = `#${dependency.targetCardId}`; + link.setAttribute('data-admin-link', dependency.targetCardId); + link.className = 'small'; + link.textContent = `Go to ${dependency.label}`; + actions.appendChild(link); + } + + notice.appendChild(actions); + return notice; +} + +/** + * Enable or disable the controls a dependency guards. + * @param {object} dependency Dependency descriptor. + * @param {boolean} satisfied Whether the prerequisite is met. + */ +function setControlsDisabled(dependency, satisfied) { + if (dependency.mode === 'warn') { + return; + } + + const selector = dependency.scope || 'input, select, textarea, button'; + dependency.card.querySelectorAll(selector).forEach((control) => { + if (control.closest(`.${NOTICE_CLASS}`)) { + return; + } + + if (!satisfied) { + // Remember controls that were already disabled for other + // reasons so re-enabling does not override them. + if (!control.disabled) { + control.setAttribute(DISABLED_FLAG, 'true'); + control.disabled = true; + } + } else if (control.getAttribute(DISABLED_FLAG) === 'true') { + control.removeAttribute(DISABLED_FLAG); + control.disabled = false; + } + }); +} + +/** + * Apply the current state of one dependency. + * @param {object} dependency Dependency descriptor. + */ +function applyDependency(dependency) { + const prerequisite = document.getElementById(dependency.prerequisiteId); + const satisfied = isSatisfied(prerequisite); + + const existing = dependency.card.querySelector(`:scope > .${NOTICE_CLASS}`); + if (existing) { + existing.remove(); + } + + setControlsDisabled(dependency, satisfied); + + if (satisfied) { + return; + } + + const notice = buildNotice(dependency, prerequisite); + const heading = dependency.card.querySelector('h4, h5, h6'); + if (heading && heading.parentElement === dependency.card) { + heading.insertAdjacentElement('afterend', notice); + } else { + dependency.card.prepend(notice); + } +} + +/** + * Wire up every declared dependency and keep it live. + */ +export function initAdminSettingsDependencies() { + const dependencies = collectDependencies(); + if (!dependencies.length) { + return; + } + + dependencies.forEach((dependency) => { + applyDependency(dependency); + + const prerequisite = document.getElementById(dependency.prerequisiteId); + if (!prerequisite) { + console.warn( + `admin dependencies: "${dependency.card.id}" requires missing control ` + + `"${dependency.prerequisiteId}"`, + ); + return; + } + + prerequisite.addEventListener('change', () => applyDependency(dependency)); + prerequisite.addEventListener('input', () => applyDependency(dependency)); + }); +} + +document.addEventListener('DOMContentLoaded', initAdminSettingsDependencies); diff --git a/application/single_app/templates/admin/_panes/citation.html b/application/single_app/templates/admin/_panes/citation.html index 3c6542a29..94f9788e1 100644 --- a/application/single_app/templates/admin/_panes/citation.html +++ b/application/single_app/templates/admin/_panes/citation.html @@ -192,10 +192,10 @@
Video Files

Reference: - + Enable Video File Support - (see the Workspaces tab) + (see AI Video Intelligence under Search and Extract)

{% endif %} @@ -231,10 +231,10 @@
Audio Files

Reference: - + Enable Audio File Support - (see the Workspaces tab) + (see AI Voice Conversations under Search and Extract)

{% endif %} diff --git a/application/single_app/templates/admin/_panes/file-sync.html b/application/single_app/templates/admin/_panes/file-sync.html index fb31daf88..2d56920d2 100644 --- a/application/single_app/templates/admin/_panes/file-sync.html +++ b/application/single_app/templates/admin/_panes/file-sync.html @@ -3,7 +3,12 @@ Configure file synchronization for personal, group, and public workspaces.

-
+
File Sync @@ -20,9 +25,9 @@
File Sync can scan large remote shares and create many document versions. Start with narrow filters, conservative schedules, and low per-run limits before enabling broad workspace access.
- {% if not settings.file_sync_redis_ready %} + {% if settings.enable_redis_cache and not settings.file_sync_redis_ready %} {% elif settings.requested_enable_file_sync and not settings.file_sync_effective_enabled %}
- Enter the full Key Vault secret name. + Enter the full Key Vault secret name. Enable Key Vault for Agent and Action Secrets must be enabled and configured.
diff --git a/application/single_app/templates/admin/_panes/search-extract.html b/application/single_app/templates/admin/_panes/search-extract.html index 37dfa5574..1db055f2b 100644 --- a/application/single_app/templates/admin/_panes/search-extract.html +++ b/application/single_app/templates/admin/_panes/search-extract.html @@ -1688,7 +1688,7 @@

- + Enhanced Citations will dramatically improve the citation experience for video and audio files. diff --git a/application/single_app/templates/admin_settings.html b/application/single_app/templates/admin_settings.html index 8edfa33f4..4071a778a 100644 --- a/application/single_app/templates/admin_settings.html +++ b/application/single_app/templates/admin_settings.html @@ -39,6 +39,14 @@ vertical-align: middle; } + /* Brief highlight when a data-admin-link jumps to a card, so the + destination is obvious after the tab switch and scroll. */ + .admin-card-link-target { + outline: 2px solid var(--bs-primary, #0d6efd); + outline-offset: 3px; + transition: outline-color 0.4s ease-out; + } + /* Floating Save Button */ .floating-save-btn { position: fixed; @@ -1847,6 +1855,8 @@