diff --git a/.github/instructions/update_release_notes.instructions.md b/.github/instructions/update_release_notes.instructions.md new file mode 100644 index 00000000..353cea48 --- /dev/null +++ b/.github/instructions/update_release_notes.instructions.md @@ -0,0 +1,90 @@ +--- +applyTo: '**' +--- + +# Release Notes Update Instructions + +## When to Update Release Notes + +After completing a code change (bug fix, new feature, enhancement, or breaking change), always ask the user: + +**"Would you like me to update the release notes in `docs/explanation/release_notes.md`?"** + +## If the User Confirms Yes + +Update the release notes file following these guidelines: + +### 1. Location +Release notes are located at: `docs/explanation/release_notes.md` + +### 2. Version Placement +- Add new entries under the **current version** from `config.py` +- If the version has changed, create a new version section at the TOP of the file +- Format: `### **(vX.XXX.XXX)**` + +### 3. Entry Categories + +Organize entries under the appropriate category: + +#### New Features +```markdown +#### New Features + +* **Feature Name** + * Brief description of what the feature does and its benefits. + * Additional details about functionality or configuration. + * (Ref: relevant files, components, or concepts) +``` + +#### Bug Fixes +```markdown +#### Bug Fixes + +* **Fix Name** + * Description of what was broken and how it was fixed. + * Impact or affected areas. + * (Ref: relevant files, functions, or components) +``` + +#### User Interface Enhancements +```markdown +#### User Interface Enhancements + +* **Enhancement Name** + * Description of UI/UX improvements. + * (Ref: relevant templates, CSS, or JavaScript files) +``` + +#### Breaking Changes +```markdown +#### Breaking Changes + +* **Change Name** + * Description of what changed and why. + * **Migration**: Steps users need to take (if any). +``` + +### 4. Entry Format Guidelines + +- **Bold the title** of each entry +- Use bullet points for details +- Include a `(Ref: ...)` line with relevant file names, functions, or concepts +- Keep descriptions concise but informative +- Focus on user-facing impact, not implementation details + +### 5. Example Entry + +```markdown +* **Custom Logo Display Fix** + * Fixed issue where custom logos uploaded via Admin Settings would only display on the admin page but not on other pages (chat, sidebar, landing page). + * Root cause was overly aggressive sanitization removing logo URLs from public settings. + * (Ref: logo display, settings sanitization, template conditionals) +``` + +### 6. Checklist Before Updating + +- [ ] Confirm the current version in `config.py` +- [ ] Determine the correct category (New Feature, Bug Fix, Enhancement, Breaking Change) +- [ ] Write a clear, user-focused description +- [ ] Include relevant file/component references +- [ ] Place entry under the correct version section diff --git a/application/single_app/config.py b/application/single_app/config.py index 12906ce8..9a5c892f 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -88,7 +88,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.237.001" +VERSION = "0.237.003" SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') diff --git a/application/single_app/functions_settings.py b/application/single_app/functions_settings.py index 7a411064..5fa59f12 100644 --- a/application/single_app/functions_settings.py +++ b/application/single_app/functions_settings.py @@ -794,6 +794,15 @@ def sanitize_settings_for_user(full_settings: dict) -> dict: else: sanitized[k] = v + # Add boolean flags for logo/favicon existence so templates can check without exposing base64 data + # These fields are stripped by the base64 filter above, but templates need to know if logos exist + if 'custom_logo_base64' in full_settings: + sanitized['custom_logo_base64'] = bool(full_settings.get('custom_logo_base64')) + if 'custom_logo_dark_base64' in full_settings: + sanitized['custom_logo_dark_base64'] = bool(full_settings.get('custom_logo_dark_base64')) + if 'custom_favicon_base64' in full_settings: + sanitized['custom_favicon_base64'] = bool(full_settings.get('custom_favicon_base64')) + return sanitized def sanitize_settings_for_logging(full_settings: dict) -> dict: diff --git a/application/single_app/static/images/custom_logo.png b/application/single_app/static/images/custom_logo.png index 45a99fd3..ecf6e652 100644 Binary files a/application/single_app/static/images/custom_logo.png and b/application/single_app/static/images/custom_logo.png differ diff --git a/application/single_app/static/images/custom_logo_dark.png b/application/single_app/static/images/custom_logo_dark.png index b3beb694..4f281945 100644 Binary files a/application/single_app/static/images/custom_logo_dark.png and b/application/single_app/static/images/custom_logo_dark.png differ diff --git a/application/single_app/static/images/favicon.ico b/application/single_app/static/images/favicon.ico index d8f058f6..3dc7742a 100644 Binary files a/application/single_app/static/images/favicon.ico and b/application/single_app/static/images/favicon.ico differ diff --git a/application/single_app/templates/admin_settings.html b/application/single_app/templates/admin_settings.html index 7ef20f2d..70edcc45 100644 --- a/application/single_app/templates/admin_settings.html +++ b/application/single_app/templates/admin_settings.html @@ -2463,8 +2463,14 @@
+{% else %}
+
+{% endif %}
+```
+
+When `custom_logo_base64` was stripped entirely, this condition always evaluated to `False`, causing the default logo to display instead of the custom uploaded logo.
+
+## Solution
+Modified `sanitize_settings_for_user()` to add boolean flags for logo/favicon existence **after** the main sanitization loop. This allows templates to check if logos exist without exposing the actual base64 data.
+
+### Code Change
+```python
+def sanitize_settings_for_user(full_settings: dict) -> dict:
+ # ... existing sanitization logic ...
+
+ # Add boolean flags for logo/favicon existence so templates can check without exposing base64 data
+ # These fields are stripped by the base64 filter above, but templates need to know if logos exist
+ if 'custom_logo_base64' in full_settings:
+ sanitized['custom_logo_base64'] = bool(full_settings.get('custom_logo_base64'))
+ if 'custom_logo_dark_base64' in full_settings:
+ sanitized['custom_logo_dark_base64'] = bool(full_settings.get('custom_logo_dark_base64'))
+ if 'custom_favicon_base64' in full_settings:
+ sanitized['custom_favicon_base64'] = bool(full_settings.get('custom_favicon_base64'))
+
+ return sanitized
+```
+
+### How It Works
+1. The sensitive base64 data is still stripped during the main loop
+2. After sanitization, boolean flags are added:
+ - `True` if the logo exists (base64 string is non-empty)
+ - `False` if no logo is set (base64 string is empty)
+3. Templates can still use `{% if app_settings.custom_logo_base64 %}` and it will correctly evaluate to `True` or `False`
+4. The actual base64 data is never exposed to the frontend
+
+## Files Modified
+- [functions_settings.py](../../application/single_app/functions_settings.py) - Modified `sanitize_settings_for_user()` function
+
+## Version
+**Fixed in version:** 0.237.002
+
+## Testing
+A functional test was created: [test_custom_logo_sanitization_fix.py](../../functional_tests/test_custom_logo_sanitization_fix.py)
+
+### Test Cases
+1. **Logo flags preserved as True** - When logos exist, boolean flags are `True`
+2. **Logo flags preserved as False** - When logos are empty, boolean flags are `False`
+3. **No spurious flags added** - If logo keys don't exist in settings, they're not added
+4. **Template compatibility** - Boolean flags work correctly in Jinja2-style conditionals
+
+### Running the Test
+```bash
+cd functional_tests
+python test_custom_logo_sanitization_fix.py
+```
+
+## Impact
+This fix affects all pages that display the application logo:
+- Landing/Index page
+- Chat page
+- Sidebar navigation (when left nav is enabled)
+- Any other page using `base.html` that references logo settings
+
+## Security Considerations
+- ā
Actual base64 data is still never exposed to the frontend
+- ā
Only boolean True/False values are sent
+- ā
No sensitive data leakage
+- ā
Maintains the security intent of the original sanitization function
diff --git a/docs/explanation/release_notes.md b/docs/explanation/release_notes.md
index df88ebcd..3b3de6e6 100644
--- a/docs/explanation/release_notes.md
+++ b/docs/explanation/release_notes.md
@@ -1,6 +1,28 @@
# Feature Release
+### **(v0.237.003)**
+
+#### New Features
+
+* **Extended Retention Policy Timeline Options**
+ * Added additional granular retention period options for conversations and documents across all workspace types.
+ * **New Options**: 2 days, 3 days, 4 days, 6 days, 7 days (1 week), and 14 days (2 weeks).
+ * **Full Option Set**: 1, 2, 3, 4, 5, 6, 7 (1 week), 10, 14 (2 weeks), 21 (3 weeks), 30, 60, 90 (3 months), 180 (6 months), 365 (1 year), 730 (2 years) days.
+ * **Scope**: Available in Admin Settings (organization defaults), Profile page (personal settings), and Control Center (group/public workspace management).
+ * **Files Modified**: `admin_settings.html`, `profile.html`, `control_center.html`.
+ * (Ref: retention policy configuration, workspace retention settings, granular time periods)
+
+#### Bug Fixes
+
+* **Custom Logo Not Displaying Across App Fix**
+ * Fixed issue where custom logos uploaded via Admin Settings would only display on the admin page but not on other pages (chat, sidebar, landing page).
+ * **Root Cause**: The `sanitize_settings_for_user()` function was stripping `custom_logo_base64`, `custom_logo_dark_base64`, and `custom_favicon_base64` keys entirely because they contained "base64" (a sensitive term filter), preventing templates from detecting logo existence.
+ * **Solution**: Modified sanitization to add boolean flags for logo/favicon existence after filtering, allowing templates to check if logos exist without exposing actual base64 data.
+ * **Security**: Actual base64 data remains hidden from frontend; only True/False boolean values are exposed.
+ * **Files Modified**: `functions_settings.py` (`sanitize_settings_for_user()` function).
+ * (Ref: logo display, settings sanitization, template conditionals)
+
### **(v0.237.001)**
#### New Features
diff --git a/functional_tests/test_custom_logo_sanitization_fix.py b/functional_tests/test_custom_logo_sanitization_fix.py
new file mode 100644
index 00000000..419a7a1f
--- /dev/null
+++ b/functional_tests/test_custom_logo_sanitization_fix.py
@@ -0,0 +1,172 @@
+#!/usr/bin/env python3
+"""
+Functional test for custom logo sanitization fix.
+Version: 0.237.002
+Implemented in: 0.237.002
+
+This test ensures that custom logo boolean flags are preserved in sanitized settings
+so templates can detect if custom logos exist without exposing the actual base64 data.
+
+Issue: When a logo was uploaded via admin settings, it was visible on the admin page
+but not on other pages (like the chat page) because the `sanitize_settings_for_user`
+function was stripping `custom_logo_base64`, `custom_logo_dark_base64`, and
+`custom_favicon_base64` keys entirely, which templates use to conditionally display logos.
+
+Fix: Modified `sanitize_settings_for_user` to add boolean flags for logo/favicon
+existence after sanitization, allowing templates to check `app_settings.custom_logo_base64`
+(which will be True/False) without exposing the actual base64 data.
+"""
+
+import sys
+import os
+
+# Add the application directory to the path
+sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'application', 'single_app'))
+
+
+def test_sanitize_settings_preserves_logo_flags():
+ """
+ Test that sanitize_settings_for_user preserves boolean flags for logo existence.
+ """
+ print("š Testing sanitize_settings_for_user preserves logo flags...")
+
+ try:
+ from functions_settings import sanitize_settings_for_user
+
+ # Test case 1: Settings with custom logos present
+ settings_with_logos = {
+ 'app_title': 'Test App',
+ 'show_logo': True,
+ 'custom_logo_base64': 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
+ 'custom_logo_dark_base64': 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
+ 'custom_favicon_base64': 'AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAA==',
+ 'logo_version': 5,
+ 'some_api_key': 'secret-key-123',
+ 'azure_openai_key': 'another-secret-key',
+ }
+
+ sanitized = sanitize_settings_for_user(settings_with_logos)
+
+ # Verify non-sensitive fields are preserved
+ assert sanitized.get('app_title') == 'Test App', "app_title should be preserved"
+ assert sanitized.get('show_logo') == True, "show_logo should be preserved"
+ assert sanitized.get('logo_version') == 5, "logo_version should be preserved"
+
+ # Verify sensitive keys are removed (api keys, secrets)
+ assert 'some_api_key' not in sanitized, "API keys should be removed"
+ assert 'azure_openai_key' not in sanitized, "Azure OpenAI key should be removed"
+
+ # Verify logo flags are boolean True (not the actual base64 data)
+ assert sanitized.get('custom_logo_base64') == True, "custom_logo_base64 should be True (boolean flag)"
+ assert sanitized.get('custom_logo_dark_base64') == True, "custom_logo_dark_base64 should be True (boolean flag)"
+ assert sanitized.get('custom_favicon_base64') == True, "custom_favicon_base64 should be True (boolean flag)"
+
+ # Verify the actual base64 data is NOT exposed
+ assert isinstance(sanitized.get('custom_logo_base64'), bool), "custom_logo_base64 should be a boolean, not a string"
+
+ print("ā
Test 1 passed: Logo flags are preserved as boolean True when logos exist")
+
+ # Test case 2: Settings without custom logos
+ settings_without_logos = {
+ 'app_title': 'Test App',
+ 'show_logo': True,
+ 'custom_logo_base64': '',
+ 'custom_logo_dark_base64': '',
+ 'custom_favicon_base64': '',
+ }
+
+ sanitized2 = sanitize_settings_for_user(settings_without_logos)
+
+ # Verify logo flags are boolean False when logos are empty
+ assert sanitized2.get('custom_logo_base64') == False, "custom_logo_base64 should be False when empty"
+ assert sanitized2.get('custom_logo_dark_base64') == False, "custom_logo_dark_base64 should be False when empty"
+ assert sanitized2.get('custom_favicon_base64') == False, "custom_favicon_base64 should be False when empty"
+
+ print("ā
Test 2 passed: Logo flags are False when logos are empty/not set")
+
+ # Test case 3: Settings without logo keys at all
+ settings_no_logo_keys = {
+ 'app_title': 'Test App',
+ 'show_logo': False,
+ }
+
+ sanitized3 = sanitize_settings_for_user(settings_no_logo_keys)
+
+ # Verify logo keys are not added if they didn't exist
+ assert 'custom_logo_base64' not in sanitized3, "custom_logo_base64 should not be added if not in original settings"
+
+ print("ā
Test 3 passed: Logo flags are not added if keys not in original settings")
+
+ print("\nā
All tests passed!")
+ return True
+
+ except AssertionError as e:
+ print(f"ā Assertion failed: {e}")
+ import traceback
+ traceback.print_exc()
+ return False
+ except Exception as e:
+ print(f"ā Test failed with exception: {e}")
+ import traceback
+ traceback.print_exc()
+ return False
+
+
+def test_template_compatibility():
+ """
+ Test that the boolean flags work correctly in Jinja2-style conditionals.
+ """
+ print("\nš Testing template compatibility with boolean flags...")
+
+ try:
+ from functions_settings import sanitize_settings_for_user
+
+ settings = {
+ 'custom_logo_base64': 'some-base64-data',
+ 'custom_logo_dark_base64': '',
+ }
+
+ sanitized = sanitize_settings_for_user(settings)
+
+ # Simulate Jinja2 conditional: {% if app_settings.custom_logo_base64 %}
+ if sanitized.get('custom_logo_base64'):
+ light_logo_condition = "show custom light logo"
+ else:
+ light_logo_condition = "show default light logo"
+
+ assert light_logo_condition == "show custom light logo", "Light logo should use custom"
+
+ # Simulate Jinja2 conditional: {% if app_settings.custom_logo_dark_base64 %}
+ if sanitized.get('custom_logo_dark_base64'):
+ dark_logo_condition = "show custom dark logo"
+ else:
+ dark_logo_condition = "show default dark logo"
+
+ assert dark_logo_condition == "show default dark logo", "Dark logo should use default (empty base64)"
+
+ print("ā
Template compatibility test passed!")
+ return True
+
+ except Exception as e:
+ print(f"ā Template compatibility test failed: {e}")
+ import traceback
+ traceback.print_exc()
+ return False
+
+
+if __name__ == "__main__":
+ results = []
+
+ print("=" * 60)
+ print("Custom Logo Sanitization Fix - Functional Tests")
+ print("=" * 60)
+
+ results.append(test_sanitize_settings_preserves_logo_flags())
+ results.append(test_template_compatibility())
+
+ print("\n" + "=" * 60)
+ success = all(results)
+ print(f"š Results: {sum(results)}/{len(results)} tests passed")
+ print("=" * 60)
+
+ sys.exit(0 if success else 1)