diff --git a/application/single_app/config.py b/application/single_app/config.py index c68a32f2..dc2a3974 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -94,7 +94,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.238.025" +VERSION = "0.239.001" SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') diff --git a/application/single_app/static/js/group/manage_group.js b/application/single_app/static/js/group/manage_group.js index 228c81c1..8a37d070 100644 --- a/application/single_app/static/js/group/manage_group.js +++ b/application/single_app/static/js/group/manage_group.js @@ -117,6 +117,14 @@ $(document).ready(function () { loadActivityTimeline(limit); }); + // Retention policy settings + $("#saveRetentionBtn").on("click", function () { + saveGroupRetentionSettings(); + }); + $('#settings-tab').on('shown.bs.tab', function () { + loadGroupRetentionSettings(); + }); + // Bulk Actions Events $("#selectAllMembers").on("change", function () { const isChecked = $(this).prop("checked"); @@ -321,14 +329,16 @@ function loadGroupInfo(doneCallback) { $("#addMemberBtn").show(); $("#addBulkMemberBtn").show(); } - + $("#pendingRequestsSection").show(); $("#activityTimelineSection").show(); $("#stats-tab-item").show(); + $("#settings-tab-item").removeClass("d-none"); loadPendingRequests(); loadGroupStats(); loadActivityTimeline(50); + loadGroupRetentionSettings(); } if (typeof doneCallback === "function") { @@ -1422,7 +1432,7 @@ async function bulkAssignRole() { async function bulkRemoveMembers() { const selectedMembers = getSelectedMembers(); - + if (selectedMembers.length === 0) { alert("No members selected"); return; @@ -1430,21 +1440,21 @@ async function bulkRemoveMembers() { // Close modal $("#bulkRemoveMembersModal").modal("hide"); - + let successCount = 0; let failedCount = 0; const failures = []; for (let i = 0; i < selectedMembers.length; i++) { const member = selectedMembers[i]; - + try { const response = await fetch(`/api/groups/${groupId}/members/${member.userId}`, { method: 'DELETE' }); const data = await response.json(); - + if (response.ok && data.success) { successCount++; } else { @@ -1470,3 +1480,102 @@ async function bulkRemoveMembers() { // Reload members and clear selection loadMembers(); } + +/* ===================== GROUP RETENTION POLICY ===================== */ + +async function loadGroupRetentionSettings() { + const convSelect = document.getElementById('group-conversation-retention-days'); + const docSelect = document.getElementById('group-document-retention-days'); + + if (!convSelect || !docSelect) return; + + try { + const orgDefaultsResp = await fetch('/api/retention-policy/defaults/group'); + const orgData = await orgDefaultsResp.json(); + + if (orgData.success) { + const convDefaultOption = convSelect.querySelector('option[value="default"]'); + const docDefaultOption = docSelect.querySelector('option[value="default"]'); + + if (convDefaultOption) { + convDefaultOption.textContent = `Using organization default (${orgData.default_conversation_label})`; + } + if (docDefaultOption) { + docDefaultOption.textContent = `Using organization default (${orgData.default_document_label})`; + } + } + } catch (error) { + console.error('Error loading group retention defaults:', error); + } + + try { + const groupResp = await fetch(`/api/groups/${groupId}`); + + if (!groupResp.ok) { + throw new Error(`Failed to fetch group: ${groupResp.status}`); + } + + const groupData = await groupResp.json(); + + if (groupData && groupData.retention_policy) { + const retentionPolicy = groupData.retention_policy; + let convRetention = retentionPolicy.conversation_retention_days; + let docRetention = retentionPolicy.document_retention_days; + + if (convRetention === undefined || convRetention === null) convRetention = 'default'; + if (docRetention === undefined || docRetention === null) docRetention = 'default'; + + convSelect.value = convRetention; + docSelect.value = docRetention; + } else { + convSelect.value = 'default'; + docSelect.value = 'default'; + } + } catch (error) { + console.error('Error loading group retention settings:', error); + convSelect.value = 'default'; + docSelect.value = 'default'; + } +} + +async function saveGroupRetentionSettings() { + const convSelect = document.getElementById('group-conversation-retention-days'); + const docSelect = document.getElementById('group-document-retention-days'); + const statusSpan = document.getElementById('group-retention-save-status'); + + if (!convSelect || !docSelect) return; + + const retentionData = { + conversation_retention_days: convSelect.value, + document_retention_days: docSelect.value + }; + + if (statusSpan) { + statusSpan.innerHTML = ' Saving...'; + } + + try { + const response = await fetch(`/api/retention-policy/group/${groupId}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(retentionData) + }); + + const data = await response.json(); + + if (response.ok && data.success) { + if (statusSpan) { + statusSpan.innerHTML = ' Saved successfully!'; + setTimeout(() => { statusSpan.innerHTML = ''; }, 3000); + } + } else { + throw new Error(data.error || 'Failed to save retention settings'); + } + } catch (error) { + console.error('Error saving group retention settings:', error); + if (statusSpan) { + statusSpan.innerHTML = ` Error: ${error.message}`; + } + showToast(`Error saving retention settings: ${error.message}`, 'danger'); + } +} diff --git a/application/single_app/static/js/public/manage_public_workspace.js b/application/single_app/static/js/public/manage_public_workspace.js index 3b31ce9b..eed59fb2 100644 --- a/application/single_app/static/js/public/manage_public_workspace.js +++ b/application/single_app/static/js/public/manage_public_workspace.js @@ -20,6 +20,14 @@ $(document).ready(function () { loadWorkspaceStats(); }); + // Retention policy settings + $("#savePublicRetentionBtn").on("click", function () { + savePublicRetentionSettings(); + }); + $('#settings-tab').on('shown.bs.tab', function () { + loadPublicRetentionSettings(); + }); + // Activity timeline pagination $('input[name="activityLimit"]').on('change', function() { const limit = parseInt($(this).val()); @@ -281,7 +289,9 @@ function loadWorkspaceInfo(callback) { $("#addBulkMemberBtn").show(); $("#pendingRequestsSection").show(); $("#activityTimelineSection").show(); + $("#settings-tab-item").removeClass("d-none"); loadPendingRequests(); + loadPublicRetentionSettings(); } if (callback) callback(); @@ -1244,7 +1254,7 @@ async function bulkAssignRole() { async function bulkRemoveMembers() { const selectedMembers = getSelectedMembers(); - + if (selectedMembers.length === 0) { alert("No members selected"); return; @@ -1252,21 +1262,21 @@ async function bulkRemoveMembers() { // Close modal $("#bulkRemoveMembersModal").modal("hide"); - + let successCount = 0; let failedCount = 0; const failures = []; for (let i = 0; i < selectedMembers.length; i++) { const member = selectedMembers[i]; - + try { const response = await fetch(`/api/public_workspaces/${workspaceId}/members/${member.userId}`, { method: 'DELETE' }); const data = await response.json(); - + if (response.ok && data.success) { successCount++; } else { @@ -1293,3 +1303,101 @@ async function bulkRemoveMembers() { loadMembers(); } +/* ===================== PUBLIC RETENTION POLICY ===================== */ + +async function loadPublicRetentionSettings() { + const convSelect = document.getElementById('public-conversation-retention-days'); + const docSelect = document.getElementById('public-document-retention-days'); + + if (!convSelect || !docSelect) return; + + try { + const orgDefaultsResp = await fetch('/api/retention-policy/defaults/public'); + const orgData = await orgDefaultsResp.json(); + + if (orgData.success) { + const convDefaultOption = convSelect.querySelector('option[value="default"]'); + const docDefaultOption = docSelect.querySelector('option[value="default"]'); + + if (convDefaultOption) { + convDefaultOption.textContent = `Using organization default (${orgData.default_conversation_label})`; + } + if (docDefaultOption) { + docDefaultOption.textContent = `Using organization default (${orgData.default_document_label})`; + } + } + } catch (error) { + console.error('Error loading public workspace retention defaults:', error); + } + + try { + const workspaceResp = await fetch(`/api/public_workspaces/${workspaceId}`); + + if (!workspaceResp.ok) { + throw new Error(`Failed to fetch workspace: ${workspaceResp.status}`); + } + + const workspaceData = await workspaceResp.json(); + + if (workspaceData && workspaceData.retention_policy) { + const retentionPolicy = workspaceData.retention_policy; + let convRetention = retentionPolicy.conversation_retention_days; + let docRetention = retentionPolicy.document_retention_days; + + if (convRetention === undefined || convRetention === null) convRetention = 'default'; + if (docRetention === undefined || docRetention === null) docRetention = 'default'; + + convSelect.value = convRetention; + docSelect.value = docRetention; + } else { + convSelect.value = 'default'; + docSelect.value = 'default'; + } + } catch (error) { + console.error('Error loading public workspace retention settings:', error); + convSelect.value = 'default'; + docSelect.value = 'default'; + } +} + +async function savePublicRetentionSettings() { + const convSelect = document.getElementById('public-conversation-retention-days'); + const docSelect = document.getElementById('public-document-retention-days'); + const statusSpan = document.getElementById('public-retention-save-status'); + + if (!convSelect || !docSelect) return; + + const retentionData = { + conversation_retention_days: convSelect.value, + document_retention_days: docSelect.value + }; + + if (statusSpan) { + statusSpan.innerHTML = ' Saving...'; + } + + try { + const response = await fetch(`/api/retention-policy/public/${workspaceId}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(retentionData) + }); + + const data = await response.json(); + + if (response.ok && data.success) { + if (statusSpan) { + statusSpan.innerHTML = ' Saved successfully!'; + setTimeout(() => { statusSpan.innerHTML = ''; }, 3000); + } + } else { + throw new Error(data.error || 'Failed to save retention settings'); + } + } catch (error) { + console.error('Error saving public workspace retention settings:', error); + if (statusSpan) { + statusSpan.innerHTML = ` Error: ${error.message}`; + } + alert(`Error saving retention settings: ${error.message}`); + } +} diff --git a/application/single_app/static/js/public/public_workspace.js b/application/single_app/static/js/public/public_workspace.js index dc6770a0..72e2a3ff 100644 --- a/application/single_app/static/js/public/public_workspace.js +++ b/application/single_app/static/js/public/public_workspace.js @@ -327,135 +327,10 @@ function updateWorkspaceUIBasedOnStatus(status) { } } -// ===================== PUBLIC RETENTION POLICY ===================== - -async function loadPublicRetentionSettings() { - if (!activePublicId) return; - - const convSelect = document.getElementById('public-conversation-retention-days'); - const docSelect = document.getElementById('public-document-retention-days'); - - if (!convSelect || !docSelect) return; // Settings tab not available - - console.log('Loading public workspace retention settings for:', activePublicId); - - try { - // Fetch organization defaults for public workspace retention - const orgDefaultsResp = await fetch('/api/retention-policy/defaults/public'); - const orgData = await orgDefaultsResp.json(); - - if (orgData.success) { - const convDefaultOption = convSelect.querySelector('option[value="default"]'); - const docDefaultOption = docSelect.querySelector('option[value="default"]'); - - if (convDefaultOption) { - convDefaultOption.textContent = `Using organization default (${orgData.default_conversation_label})`; - } - if (docDefaultOption) { - docDefaultOption.textContent = `Using organization default (${orgData.default_document_label})`; - } - console.log('Loaded org defaults:', orgData); - } - } catch (error) { - console.error('Error loading public workspace retention defaults:', error); - } - - // Load current public workspace's retention policy settings - try { - const workspaceResp = await fetch(`/api/public_workspaces/${activePublicId}`); - - if (!workspaceResp.ok) { - throw new Error(`Failed to fetch workspace: ${workspaceResp.status}`); - } - - const workspaceData = await workspaceResp.json(); - console.log('Loaded workspace data:', workspaceData); - - // API returns workspace object directly (not wrapped in success/workspace) - if (workspaceData && workspaceData.retention_policy) { - const retentionPolicy = workspaceData.retention_policy; - let convRetention = retentionPolicy.conversation_retention_days; - let docRetention = retentionPolicy.document_retention_days; - - console.log('Found retention policy:', retentionPolicy); - - // If undefined, use 'default' - if (convRetention === undefined || convRetention === null) convRetention = 'default'; - if (docRetention === undefined || docRetention === null) docRetention = 'default'; - - convSelect.value = convRetention; - docSelect.value = docRetention; - console.log('Set retention values to:', { conv: convRetention, doc: docRetention }); - } else { - // Set to organization default if no retention policy set - console.log('No retention policy found, using defaults'); - convSelect.value = 'default'; - docSelect.value = 'default'; - } - } catch (error) { - console.error('Error loading public workspace retention settings:', error); - // Set defaults on error - convSelect.value = 'default'; - docSelect.value = 'default'; - } -} - -async function savePublicRetentionSettings() { - if (!activePublicId) { - showToast('No active public workspace selected.', 'warning'); - return; - } - - const convSelect = document.getElementById('public-conversation-retention-days'); - const docSelect = document.getElementById('public-document-retention-days'); - const statusSpan = document.getElementById('public-retention-save-status'); - - if (!convSelect || !docSelect) return; - - const retentionData = { - conversation_retention_days: convSelect.value, - document_retention_days: docSelect.value - }; - - console.log('Saving public workspace retention settings:', retentionData); - - // Show saving status - if (statusSpan) { - statusSpan.innerHTML = ' Saving...'; - } - - try { - const response = await fetch(`/api/retention-policy/public/${activePublicId}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(retentionData) - }); - - const data = await response.json(); - console.log('Save response:', data); - - if (response.ok && data.success) { - if (statusSpan) { - statusSpan.innerHTML = ' Saved successfully!'; - setTimeout(() => { statusSpan.innerHTML = ''; }, 3000); - } - console.log('Public workspace retention settings saved successfully'); - } else { - throw new Error(data.error || 'Failed to save retention settings'); - } - } catch (error) { - console.error('Error saving public workspace retention settings:', error); - if (statusSpan) { - statusSpan.innerHTML = ` Error: ${error.message}`; - } - showToast(`Error saving retention settings: ${error.message}`, 'danger'); - } -} - function loadActivePublicData(){ const activeTab = document.querySelector('#publicWorkspaceTab .nav-link.active').dataset.bsTarget; if(activeTab==='#public-docs-tab') fetchPublicDocs(); else fetchPublicPrompts(); - updatePublicRoleDisplay(); updatePublicPromptsRoleUI(); updateWorkspaceStatusAlert(); loadPublicRetentionSettings(); + updatePublicRoleDisplay(); updatePublicPromptsRoleUI(); updateWorkspaceStatusAlert(); } async function fetchPublicDocs(){ diff --git a/application/single_app/templates/group_workspaces.html b/application/single_app/templates/group_workspaces.html index 92012a23..e14fe8a3 100644 --- a/application/single_app/templates/group_workspaces.html +++ b/application/single_app/templates/group_workspaces.html @@ -324,22 +324,6 @@

Group Workspace

{% endif %} - {% if app_settings.enable_retention_policy_group %} - - {% endif %} @@ -862,78 +846,6 @@

Group Workspace

{% endif %} - {% if app_settings.enable_retention_policy_group %} - -
-
-
Retention Policy Settings
-

Configure how long to keep conversations and documents in this group workspace. Items older than the specified period will be automatically deleted.

- -
- - Default: You can use the organization default or set a custom retention period. Choose "No automatic deletion" to keep items indefinitely. -
- -
-
- - - Conversations older than this will be automatically deleted. -
- -
- - - Documents older than this will be automatically deleted. -
-
- -
- - -
- -
- - Important: Deleted conversations will be archived if archiving is enabled. All deletions are logged in activity history. -
-
-
- - {% endif %} @@ -2553,7 +2465,6 @@ // Update UI elements dependent on role (applies to both tabs potentially) updateRoleDisplay(); updateGroupPromptsRoleUI(); // This is specific to prompts tab UI elements - loadGroupRetentionSettings(); // Load retention settings } function updateRoleDisplay() { @@ -2585,140 +2496,9 @@ uploadSection.style.display = showUpload ? "block" : "none"; if (uploadHr) uploadHr.style.display = showUpload ? "block" : "none"; - // Control visibility of Settings tab (only for Owners and Admins) - const settingsTabNav = document.getElementById('group-settings-tab-nav'); - const canManageSettings = ['Owner', 'Admin'].includes(userRoleInActiveGroup); - if (settingsTabNav) { - settingsTabNav.classList.toggle('d-none', !canManageSettings); - } - notifyGroupWorkspaceContext(); } - /* ===================== GROUP RETENTION POLICY ===================== */ - - async function loadGroupRetentionSettings() { - if (!activeGroupId) return; - - const convSelect = document.getElementById('group-conversation-retention-days'); - const docSelect = document.getElementById('group-document-retention-days'); - - if (!convSelect || !docSelect) return; // Settings tab not available - - console.log('Loading group retention settings for:', activeGroupId); - - try { - // Fetch organization defaults for group retention - const orgDefaultsResp = await fetch('/api/retention-policy/defaults/group'); - const orgData = await orgDefaultsResp.json(); - - if (orgData.success) { - const convDefaultOption = convSelect.querySelector('option[value="default"]'); - const docDefaultOption = docSelect.querySelector('option[value="default"]'); - - if (convDefaultOption) { - convDefaultOption.textContent = `Using organization default (${orgData.default_conversation_label})`; - } - if (docDefaultOption) { - docDefaultOption.textContent = `Using organization default (${orgData.default_document_label})`; - } - console.log('Loaded org defaults:', orgData); - } - } catch (error) { - console.error('Error loading group retention defaults:', error); - } - - // Load current group's retention policy settings - try { - const groupResp = await fetch(`/api/groups/${activeGroupId}`); - - if (!groupResp.ok) { - throw new Error(`Failed to fetch group: ${groupResp.status}`); - } - - const groupData = await groupResp.json(); - console.log('Loaded group data:', groupData); - - // API returns group object directly (not wrapped in success/group) - if (groupData && groupData.retention_policy) { - const retentionPolicy = groupData.retention_policy; - let convRetention = retentionPolicy.conversation_retention_days; - let docRetention = retentionPolicy.document_retention_days; - - console.log('Found retention policy:', retentionPolicy); - - // If undefined, use 'default' - if (convRetention === undefined || convRetention === null) convRetention = 'default'; - if (docRetention === undefined || docRetention === null) docRetention = 'default'; - - convSelect.value = convRetention; - docSelect.value = docRetention; - console.log('Set retention values to:', { conv: convRetention, doc: docRetention }); - } else { - // Set to organization default if no retention policy set - console.log('No retention policy found, using defaults'); - convSelect.value = 'default'; - docSelect.value = 'default'; - } - } catch (error) { - console.error('Error loading group retention settings:', error); - // Set defaults on error - convSelect.value = 'default'; - docSelect.value = 'default'; - } - } - - async function saveGroupRetentionSettings() { - if (!activeGroupId) { - showToast('No active group selected.', 'warning'); - return; - } - - const convSelect = document.getElementById('group-conversation-retention-days'); - const docSelect = document.getElementById('group-document-retention-days'); - const statusSpan = document.getElementById('group-retention-save-status'); - - if (!convSelect || !docSelect) return; - - const retentionData = { - conversation_retention_days: convSelect.value, - document_retention_days: docSelect.value - }; - - console.log('Saving group retention settings:', retentionData); - - // Show saving status - if (statusSpan) { - statusSpan.innerHTML = ' Saving...'; - } - - try { - const response = await fetch(`/api/retention-policy/group/${activeGroupId}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(retentionData) - }); - - const data = await response.json(); - console.log('Save response:', data); - - if (response.ok && data.success) { - if (statusSpan) { - statusSpan.innerHTML = ' Saved successfully!'; - setTimeout(() => { statusSpan.innerHTML = ''; }, 3000); - } - console.log('Group retention settings saved successfully'); - } else { - throw new Error(data.error || 'Failed to save retention settings'); - } - } catch (error) { - console.error('Error saving group retention settings:', error); - if (statusSpan) { - statusSpan.innerHTML = ` Error: ${error.message}`; - } - showToast(`Error saving retention settings: ${error.message}`, 'danger'); - } - } /* ===================== GROUP DOCUMENTS ===================== */ diff --git a/application/single_app/templates/manage_group.html b/application/single_app/templates/manage_group.html index 78f80cdb..e99d9f4c 100644 --- a/application/single_app/templates/manage_group.html +++ b/application/single_app/templates/manage_group.html @@ -222,6 +222,14 @@

Loading...

Stats + {% if app_settings.enable_retention_policy_group %} + + {% endif %} @@ -471,6 +479,73 @@
Activity Timeline
+ + {% if app_settings.enable_retention_policy_group %} + +
+
+
Retention Policy Settings
+

Configure how long to keep conversations and documents in this group workspace. Items older than the specified period will be automatically deleted.

+ +
+ + Default: You can use the organization default or set a custom retention period. Choose "No automatic deletion" to keep items indefinitely. +
+ +
+
+ + + Conversations older than this will be automatically deleted. +
+ +
+ + + Documents older than this will be automatically deleted. +
+
+ +
+ + +
+ +
+ + Important: Deleted conversations will be archived if archiving is enabled. All deletions are logged in activity history. +
+
+
+ {% endif %} diff --git a/application/single_app/templates/manage_public_workspace.html b/application/single_app/templates/manage_public_workspace.html index f6dc98f3..1396a447 100644 --- a/application/single_app/templates/manage_public_workspace.html +++ b/application/single_app/templates/manage_public_workspace.html @@ -210,6 +210,14 @@

Loading...

Stats + {% if app_settings.enable_retention_policy_public %} + + {% endif %} @@ -454,6 +462,73 @@
Activity Timeline
+ + {% if app_settings.enable_retention_policy_public %} + +
+
+
Retention Policy Settings
+

Configure how long to keep conversations and documents in this public workspace. Items older than the specified period will be automatically deleted.

+ +
+ + Default: You can use the organization default or set a custom retention period. Choose "No automatic deletion" to keep items indefinitely. +
+ +
+
+ + + Conversations older than this will be automatically deleted. +
+ +
+ + + Documents older than this will be automatically deleted. +
+
+ +
+ + +
+ +
+ + Important: Deleted conversations will be archived if archiving is enabled. All deletions are logged in activity history. +
+
+
+ {% endif %} diff --git a/application/single_app/templates/public_workspaces.html b/application/single_app/templates/public_workspaces.html index 4ef89b70..1562cec7 100644 --- a/application/single_app/templates/public_workspaces.html +++ b/application/single_app/templates/public_workspaces.html @@ -158,11 +158,6 @@ - {% if app_settings.enable_retention_policy_public %} - - {% endif %}
@@ -382,73 +377,6 @@
items per page
- - {% if app_settings.enable_retention_policy_public %} - -
-
-
Retention Policy Settings
-

Configure how long to keep conversations and documents in this public workspace. Items older than the specified period will be automatically deleted.

- -
- - Default: You can use the organization default or set a custom retention period. Choose "No automatic deletion" to keep items indefinitely. -
- -
-
- - - Conversations older than this will be automatically deleted. -
- -
- - - Documents older than this will be automatically deleted. -
-
- -
- - -
- -
- - Important: Deleted conversations will be archived if archiving is enabled. All deletions are logged in activity history. -
-
-
- {% endif %} diff --git a/docs/explanation/features/CONVERSATION_EXPORT.md b/docs/explanation/features/v0.239.001/CONVERSATION_EXPORT.md similarity index 100% rename from docs/explanation/features/CONVERSATION_EXPORT.md rename to docs/explanation/features/v0.239.001/CONVERSATION_EXPORT.md diff --git a/docs/explanation/fixes/AGENT_SCHEMA_REF_RESOLUTION_FIX.md b/docs/explanation/fixes/v0.239.001/AGENT_SCHEMA_REF_RESOLUTION_FIX.md similarity index 100% rename from docs/explanation/fixes/AGENT_SCHEMA_REF_RESOLUTION_FIX.md rename to docs/explanation/fixes/v0.239.001/AGENT_SCHEMA_REF_RESOLUTION_FIX.md diff --git a/docs/explanation/fixes/CHATS_USER_SETTINGS_HARDENING_FIX.md b/docs/explanation/fixes/v0.239.001/CHATS_USER_SETTINGS_HARDENING_FIX.md similarity index 100% rename from docs/explanation/fixes/CHATS_USER_SETTINGS_HARDENING_FIX.md rename to docs/explanation/fixes/v0.239.001/CHATS_USER_SETTINGS_HARDENING_FIX.md diff --git a/docs/explanation/fixes/TAG_FILTER_INJECTION_FIX.md b/docs/explanation/fixes/v0.239.001/TAG_FILTER_INJECTION_FIX.md similarity index 100% rename from docs/explanation/fixes/TAG_FILTER_INJECTION_FIX.md rename to docs/explanation/fixes/v0.239.001/TAG_FILTER_INJECTION_FIX.md diff --git a/docs/explanation/release_notes.md b/docs/explanation/release_notes.md index 9bee2f42..3292f72a 100644 --- a/docs/explanation/release_notes.md +++ b/docs/explanation/release_notes.md @@ -2,37 +2,21 @@ # Feature Release -### **(v0.238.025)** - -#### Bug Fixes - -* **Public Workspace setActive 403 Fix** - * Fixed issue where non-owner/admin/document-manager users received a 403 "Not a member" error when trying to activate a public workspace for chat. - * Root cause was an overly restrictive membership check on the `/api/public_workspaces/setActive` endpoint that only allowed owners, admins, and document managers — even though public workspaces are intended to be accessible to all authenticated users for chatting. - * Removed the membership verification from the `setActive` endpoint; the route still requires authentication (`@login_required`, `@user_required`) and the public workspaces feature flag (`@enabled_required`). - * Other admin-level endpoints (listing members, viewing stats, ownership transfer) retain their membership checks. - * (Ref: `route_backend_public_workspaces.py`, `api_set_active_public_workspace`) -* **Chats Page User Settings Hardening** - * Fixed a user-specific chats page failure where only one affected user could not load `/chats` due to malformed per-user settings data. - * **Root Cause**: The chats route assumed `user_settings["settings"]` was always a dictionary. If that field existed but had an invalid type (for example string, null, or list), the page could fail before rendering. - * **Solution**: Hardened `get_user_settings()` to normalize missing/malformed `settings` to `{}` and persist the repaired document. Hardened the chats route to use safe dictionary fallbacks when reading nested settings values. - * **Telemetry**: Added repair logging (`[UserSettings] Malformed settings repaired`) to improve diagnostics for future user-specific data-shape issues. - * **Files Modified**: `functions_settings.py`, `route_frontend_chats.py`, `config.py`. - * **Files Added**: `test_chats_user_settings_hardening_fix.py`, `CHATS_USER_SETTINGS_HARDENING_FIX.md`. - * (Ref: user settings normalization, `/chats` route resilience, `functional_tests/test_chats_user_settings_hardening_fix.py`, `docs/explanation/fixes/CHATS_USER_SETTINGS_HARDENING_FIX.md`) -* **Tag Filter Input Sanitization (Injection Prevention)** - * Added `sanitize_tags_for_filter()` function to validate tag filter inputs against the same `^[a-z0-9_-]+$` character whitelist enforced when saving tags. - * Previously, tag filter values from query parameters only passed through `normalize_tag()` (strip + lowercase) without character validation, allowing arbitrary characters to reach OData filter construction in `build_tags_filter()`. - * Hardened `build_tags_filter()` in `functions_search.py` to validate tags before interpolating into OData expressions, eliminating the OData injection vector. - * Updated tag filter parsing in personal, group, and public document routes to use `sanitize_tags_for_filter()` for defense-in-depth. - * Invalid tag filter values are silently dropped (they cannot match any stored tag). - * **Files Modified**: `functions_documents.py`, `functions_search.py`, `route_backend_documents.py`, `route_backend_group_documents.py`, `route_backend_public_documents.py`. - * (Ref: `TAG_FILTER_INJECTION_FIX.md`, `sanitize_tags_for_filter`) - -### **(v0.238.024)** +### **(v0.239.001)** #### New Features +* **Conversation Export** + * Export one or multiple conversations from the Chat page in JSON or Markdown format. + * **Single Export**: Use the ellipsis menu on any conversation to quickly export it. + * **Multi-Export**: Enter selection mode, check the conversations you want, and click the export button. + * A guided 4-step wizard walks you through selection review, format choice, packaging options (single file or ZIP archive), and download. + * Sensitive internal metadata is automatically stripped from exported data for security. + +* **Retention Policy UI for Groups and Public Workspaces** + * Can now configure conversation and document retention periods directly from the workspace and group management page. + * Choose from preset retention periods ranging from 7 days to 10 years, use the organization default, or disable automatic deletion entirely. + * **Owner-Only Group Agent and Action Management** * New admin setting to restrict group agent and group action management (create, edit, delete) to only the group Owner role. * **Admin Toggle**: "Require Owner to Manage Group Agents and Actions" located in Admin Settings > My Groups section, under the existing group creation membership setting. @@ -108,19 +92,6 @@ * **Files Modified**: `chat-documents.js`, `chat-messages.js`, `functions_search.py`, `route_backend_chats.py`, `chats.html`. * (Ref: Multi-document selection, tag filtering, OData search integration, `CHAT_DOCUMENT_AND_TAG_FILTERING.md`) -#### New Features - -* **Conversation Export** - * Export one or multiple conversations from the Chat page in JSON or Markdown format. - * **Single Export**: Use the ellipsis menu on any conversation to quickly export it. - * **Multi-Export**: Enter selection mode, check the conversations you want, and click the export button. - * A guided 4-step wizard walks you through selection review, format choice, packaging options (single file or ZIP archive), and download. - * Sensitive internal metadata is automatically stripped from exported data for security. - -* **Retention Policy UI for Groups and Public Workspaces** - * Can now configure conversation and document retention periods directly from the workspace and group management page. - * Choose from preset retention periods ranging from 7 days to 10 years, use the organization default, or disable automatic deletion entirely. - #### Bug Fixes * **Citation Parsing Bug Fix** @@ -130,6 +101,31 @@ * **Files Modified**: `chat-citations.js`. * (Ref: Citation parsing, page range handling, `CITATION_IMPROVEMENTS.md`) +* **Public Workspace setActive 403 Fix** + * Fixed issue where non-owner/admin/document-manager users received a 403 "Not a member" error when trying to activate a public workspace for chat. + * Root cause was an overly restrictive membership check on the `/api/public_workspaces/setActive` endpoint that only allowed owners, admins, and document managers — even though public workspaces are intended to be accessible to all authenticated users for chatting. + * Removed the membership verification from the `setActive` endpoint; the route still requires authentication (`@login_required`, `@user_required`) and the public workspaces feature flag (`@enabled_required`). + * Other admin-level endpoints (listing members, viewing stats, ownership transfer) retain their membership checks. + * (Ref: `route_backend_public_workspaces.py`, `api_set_active_public_workspace`) + +* **Chats Page User Settings Hardening** + * Fixed a user-specific chats page failure where only one affected user could not load `/chats` due to malformed per-user settings data. + * **Root Cause**: The chats route assumed `user_settings["settings"]` was always a dictionary. If that field existed but had an invalid type (for example string, null, or list), the page could fail before rendering. + * **Solution**: Hardened `get_user_settings()` to normalize missing/malformed `settings` to `{}` and persist the repaired document. Hardened the chats route to use safe dictionary fallbacks when reading nested settings values. + * **Telemetry**: Added repair logging (`[UserSettings] Malformed settings repaired`) to improve diagnostics for future user-specific data-shape issues. + * **Files Modified**: `functions_settings.py`, `route_frontend_chats.py`, `config.py`. + * **Files Added**: `test_chats_user_settings_hardening_fix.py`, `CHATS_USER_SETTINGS_HARDENING_FIX.md`. + * (Ref: user settings normalization, `/chats` route resilience, `functional_tests/test_chats_user_settings_hardening_fix.py`, `docs/explanation/fixes/CHATS_USER_SETTINGS_HARDENING_FIX.md`) + +* **Tag Filter Input Sanitization (Injection Prevention)** + * Added `sanitize_tags_for_filter()` function to validate tag filter inputs against the same `^[a-z0-9_-]+$` character whitelist enforced when saving tags. + * Previously, tag filter values from query parameters only passed through `normalize_tag()` (strip + lowercase) without character validation, allowing arbitrary characters to reach OData filter construction in `build_tags_filter()`. + * Hardened `build_tags_filter()` in `functions_search.py` to validate tags before interpolating into OData expressions, eliminating the OData injection vector. + * Updated tag filter parsing in personal, group, and public document routes to use `sanitize_tags_for_filter()` for defense-in-depth. + * Invalid tag filter values are silently dropped (they cannot match any stored tag). + * **Files Modified**: `functions_documents.py`, `functions_search.py`, `route_backend_documents.py`, `route_backend_group_documents.py`, `route_backend_public_documents.py`. + * (Ref: `TAG_FILTER_INJECTION_FIX.md`, `sanitize_tags_for_filter`) + #### User Interface Enhancements * **Extended Document Dropdown Width**