From 69f2c803082cd4654522bf8c02fba0e5ad540a18 Mon Sep 17 00:00:00 2001 From: Dries Peeters Date: Sat, 25 Oct 2025 07:23:43 +0200 Subject: [PATCH] feat: Complete Admin Settings UI and enhance PDF logo reliability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit addresses multiple issues with the Admin Settings page and improves PDF invoice logo embedding for better cross-platform reliability. ## Admin Settings UI - Missing Fields Fixed The Admin → Settings page was incomplete, showing only basic timer and regional settings. Added all missing sections: - User Management: Self-registration toggle with admin username note - Company Branding: Full company info fields (name, email, phone, website, address, tax ID, bank info) plus logo upload with preview - Invoice Defaults: Prefix, start number, payment terms, and notes - Backup Settings: Retention days and backup time configuration - Export Settings: CSV delimiter preference selector - Privacy & Analytics: Telemetry opt-in with detailed privacy information The backend was already handling these fields - this was purely a frontend template issue where form fields were missing. ## Analytics/Telemetry Preference Synchronization Fixed critical bug where analytics checkbox in Admin Settings only updated the database but not the InstallationConfig file that the telemetry system actually reads from. Changes now properly sync both systems: - On page load: Auto-sync database from InstallationConfig (source of truth) - On save: Update both database AND InstallationConfig simultaneously - Added logging for analytics preference changes - Updated UI references: Initial setup and Telemetry dashboard now point to Admin → Settings as the primary location - Added clear privacy information explaining what data is collected ## PDF Logo Embedding Enhancement Improved logo reliability in PDF invoices by switching from file:// URIs to base64 data URIs: - More reliable across platforms (Windows, Linux, macOS) - Works consistently in Docker containers - Self-contained (no filesystem path dependencies) - Automatic MIME type detection for all formats (PNG, JPG, GIF, SVG, WEBP) - Graceful fallback to file:// URI if base64 fails - Added comprehensive debug logging for troubleshooting ## Diagnostic Tools & Documentation - Created test_logo_pdf.py: Diagnostic script to identify logo issues - Created LOGO_PDF_TROUBLESHOOTING.md: Comprehensive troubleshooting guide - Enhanced error messages with debug output throughout logo processing - Added context passing fixes for PDF template rendering ## Files Changed ### Core Fixes - app/templates/admin/settings.html: Complete rewrite with all sections - app/routes/admin.py: InstallationConfig sync for analytics preference - app/static/uploads/logos/.gitkeep: Ensure logos directory tracked by git ### PDF Logo Enhancement - app/utils/pdf_generator.py: Base64 encoding + explicit context passing - app/utils/template_filters.py: get_logo_base64() helper with debug logging - app/templates/invoices/pdf_default.html: Base64 logo embedding ### Analytics Synchronization - app/templates/setup/initial_setup.html: Updated settings reference - app/templates/admin/telemetry.html: Cross-reference to Admin → Settings ### Documentation - docs/GETTING_STARTED.md: Updated to reflect actual UI behavior - test_logo_pdf.py: New diagnostic script - LOGO_PDF_TROUBLESHOOTING.md: New troubleshooting guide ## Testing Run diagnostic script to verify logo configuration: --- app/routes/admin.py | 20 ++- app/static/uploads/logos/.gitkeep | 4 +- app/templates/admin/settings.html | 143 +++++++++++++++++ app/templates/admin/telemetry.html | 1 + app/templates/invoices/pdf_default.html | 9 +- app/templates/setup/initial_setup.html | 2 +- app/utils/pdf_generator.py | 59 ++++++- app/utils/template_filters.py | 45 ++++++ docs/GETTING_STARTED.md | 93 ++++++++---- test_logo_pdf.py | 194 ++++++++++++++++++++++++ 10 files changed, 528 insertions(+), 42 deletions(-) create mode 100644 test_logo_pdf.py diff --git a/app/routes/admin.py b/app/routes/admin.py index e8ab80c..de1960d 100644 --- a/app/routes/admin.py +++ b/app/routes/admin.py @@ -305,6 +305,13 @@ def toggle_telemetry(): def settings(): """Manage system settings""" settings_obj = Settings.get_settings() + installation_config = get_installation_config() + + # Sync analytics preference from installation config to database on load + # (installation config is the source of truth for telemetry) + if settings_obj.allow_analytics != installation_config.get_telemetry_preference(): + settings_obj.allow_analytics = installation_config.get_telemetry_preference() + db.session.commit() if request.method == 'POST': # Validate timezone @@ -343,7 +350,18 @@ def settings(): settings_obj.invoice_notes = request.form.get('invoice_notes', 'Thank you for your business!') # Update privacy and analytics settings - settings_obj.allow_analytics = request.form.get('allow_analytics') == 'on' + allow_analytics = request.form.get('allow_analytics') == 'on' + old_analytics_state = settings_obj.allow_analytics + settings_obj.allow_analytics = allow_analytics + + # Also update the installation config (used by telemetry system) + # This ensures the telemetry system sees the updated preference + installation_config.set_telemetry_preference(allow_analytics) + + # Log analytics preference change if it changed + if old_analytics_state != allow_analytics: + app_module.log_event("admin.analytics_toggled", user_id=current_user.id, new_state=allow_analytics) + app_module.track_event(current_user.id, "admin.analytics_toggled", {"enabled": allow_analytics}) if not safe_commit('admin_update_settings'): flash('Could not update settings due to a database error. Please check server logs.', 'error') diff --git a/app/static/uploads/logos/.gitkeep b/app/static/uploads/logos/.gitkeep index 8f5b058..9d486bd 100644 --- a/app/static/uploads/logos/.gitkeep +++ b/app/static/uploads/logos/.gitkeep @@ -1,2 +1,2 @@ -# This file ensures the uploads directory is preserved in git -# Company logos will be stored in this directory +# This file ensures the logos directory is tracked by git +# Logo files uploaded through the admin interface will be stored here diff --git a/app/templates/admin/settings.html b/app/templates/admin/settings.html index 8e757f7..4c52856 100644 --- a/app/templates/admin/settings.html +++ b/app/templates/admin/settings.html @@ -46,6 +46,149 @@ + + +
+

User Management

+
+
+ + +
+

+ Note: Admin users are configured via the ADMIN_USERNAMES environment variable, not in this UI. +

+
+
+ + +
+

Company Branding

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + {% if settings.has_logo() %} +
+ Company Logo +
+ + +
+
+ {% endif %} +
+ + + +
+

Allowed formats: PNG, JPG, GIF, SVG, WEBP

+
+
+ + +
+

Invoice Defaults

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+

Backup Settings

+
+
+ + +
+
+ + +
+
+
+ + +
+

Export Settings

+
+
+ + +
+
+
+ + +
+

Privacy & Analytics

+
+
+ + +
+
+

Help improve TimeTracker by sharing anonymous usage data:

+
    +
  • Platform and version information
  • +
  • Feature usage patterns (no personal data)
  • +
  • Performance and error metrics
  • +
+

Privacy: All data is anonymized. No personal information, time entries, or client data is ever collected.

+

This is the same setting as the telemetry preference shown during initial setup.

+
+
+
diff --git a/app/templates/admin/telemetry.html b/app/templates/admin/telemetry.html index d534710..25efeb0 100644 --- a/app/templates/admin/telemetry.html +++ b/app/templates/admin/telemetry.html @@ -44,6 +44,7 @@ +

Tip: You can also manage this setting in Admin → Settings (Privacy & Analytics section)

{% if telemetry.enabled %} diff --git a/app/templates/invoices/pdf_default.html b/app/templates/invoices/pdf_default.html index 8cad2fa..e0c41c1 100644 --- a/app/templates/invoices/pdf_default.html +++ b/app/templates/invoices/pdf_default.html @@ -11,7 +11,14 @@ {% if settings.has_logo() %} {% set logo_path = settings.get_logo_path() %} {% if logo_path %} - + {# Base64 encode the logo for reliable PDF embedding #} + {% set logo_data = get_logo_base64(logo_path) %} + {% if logo_data %} + + {% else %} + {# Fallback to file URI if base64 fails #} + + {% endif %} {% endif %} {% endif %}
diff --git a/app/templates/setup/initial_setup.html b/app/templates/setup/initial_setup.html index 23f7665..4a0a388 100644 --- a/app/templates/setup/initial_setup.html +++ b/app/templates/setup/initial_setup.html @@ -118,7 +118,7 @@

Why? Anonymous usage data helps us prioritize features and fix issues. - You can change this anytime in Admin → Telemetry Dashboard. + You can change this anytime in Admin → Settings (Privacy & Analytics section).

diff --git a/app/utils/pdf_generator.py b/app/utils/pdf_generator.py index 116b7f8..48500e0 100644 --- a/app/utils/pdf_generator.py +++ b/app/utils/pdf_generator.py @@ -71,8 +71,35 @@ class InvoicePDFGenerator: html = '' if not html: try: - html = render_template('invoices/pdf_default.html', invoice=self.invoice, settings=self.settings, Path=Path) - except Exception: + # Import helper functions for template + from app.utils.template_filters import get_logo_base64 + from babel.dates import format_date as babel_format_date + + def format_date(value, format='medium'): + """Format date for template""" + if babel_format_date: + return babel_format_date(value, format=format) + return value.strftime('%Y-%m-%d') if value else '' + + def format_money(value): + """Format money for template""" + try: + return f"{float(value):,.2f}" + except Exception: + return str(value) + + html = render_template('invoices/pdf_default.html', + invoice=self.invoice, + settings=self.settings, + Path=Path, + get_logo_base64=get_logo_base64, + format_date=format_date, + format_money=format_money) + except Exception as e: + # Log the exception for debugging + import traceback + print(f"Error rendering PDF template: {e}") + print(traceback.format_exc()) html = f"

{_('Invoice')} {self.invoice.invoice_number}

" return html, css @@ -237,13 +264,29 @@ class InvoicePDFGenerator: if self.settings.has_logo(): logo_path = self.settings.get_logo_path() if logo_path and os.path.exists(logo_path): - # Build a cross-platform file URI (handles Windows and POSIX paths) + # Use base64 data URI for reliable PDF embedding (works better with WeasyPrint) try: - file_url = Path(logo_path).resolve().as_uri() - except Exception: - # Fallback to naive file:// if as_uri fails - file_url = f'file://{logo_path}' - return f'' + import base64 + import mimetypes + + with open(logo_path, 'rb') as logo_file: + logo_data = base64.b64encode(logo_file.read()).decode('utf-8') + + # Detect MIME type + mime_type, _ = mimetypes.guess_type(logo_path) + if not mime_type: + # Default to PNG if can't detect + mime_type = 'image/png' + + data_uri = f'data:{mime_type};base64,{logo_data}' + return f'' + except Exception as e: + # Fallback to file URI if base64 fails + try: + file_url = Path(logo_path).resolve().as_uri() + except Exception: + file_url = f'file://{logo_path}' + return f'' return '' def _get_company_tax_info(self): diff --git a/app/utils/template_filters.py b/app/utils/template_filters.py index c068440..24ebc90 100644 --- a/app/utils/template_filters.py +++ b/app/utils/template_filters.py @@ -104,3 +104,48 @@ def register_template_filters(app): return f"{float(value):,.2f}" except Exception: return str(value) + + def get_logo_base64(logo_path): + """Convert logo file to base64 data URI for PDF embedding""" + import os + import base64 + import mimetypes + + if not logo_path: + print("DEBUG: logo_path is None or empty") + return None + + if not os.path.exists(logo_path): + print(f"DEBUG: Logo file does not exist: {logo_path}") + return None + + try: + print(f"DEBUG: Reading logo from: {logo_path}") + with open(logo_path, 'rb') as logo_file: + logo_data = base64.b64encode(logo_file.read()).decode('utf-8') + + # Detect MIME type + mime_type, _ = mimetypes.guess_type(logo_path) + if not mime_type: + # Try to detect from file extension + ext = os.path.splitext(logo_path)[1].lower() + mime_map = { + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.webp': 'image/webp' + } + mime_type = mime_map.get(ext, 'image/png') + + print(f"DEBUG: Logo encoded successfully, MIME type: {mime_type}, size: {len(logo_data)} bytes") + return f'data:{mime_type};base64,{logo_data}' + except Exception as e: + print(f"DEBUG: Error encoding logo: {e}") + import traceback + print(traceback.format_exc()) + return None + + # Make get_logo_base64 available in templates as a global function + app.jinja_env.globals.update(get_logo_base64=get_logo_base64) \ No newline at end of file diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index b6d387b..5623e31 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -31,10 +31,16 @@ export SECRET_KEY=$(python -c "import secrets; print(secrets.token_hex(32))") # Windows PowerShell: $env:SECRET_KEY = python -c "import secrets; print(secrets.token_hex(32))" -# 3. Start TimeTracker +# 3. (Optional) Set admin usernames +# Linux/macOS: +export ADMIN_USERNAMES=admin,manager +# Windows PowerShell: +$env:ADMIN_USERNAMES = "admin,manager" + +# 4. Start TimeTracker docker-compose up -d -# 4. Access the application +# 5. Access the application # Open your browser to: https://localhost # (Self‑signed certificate; your browser will show a warning the first time.) @@ -42,6 +48,8 @@ docker-compose up -d # Use the example compose that publishes the app directly: # docker-compose -f docker-compose.example.yml up -d # Then open: http://localhost:8080 + +# Note: Login with the username you set in ADMIN_USERNAMES (default: admin) to get admin access ``` **That's it!** TimeTracker is now running with PostgreSQL. @@ -92,9 +100,10 @@ python app.py - Example: `admin`, `john`, or your name - This creates your account automatically -3. **First user becomes admin** automatically - - Full access to all features - - Can manage users and settings +3. **Admin users are configured in the environment** + - Set via `ADMIN_USERNAMES` environment variable (default: `admin`) + - When you login with a username matching the list, you get admin role + - Example: If `ADMIN_USERNAMES=admin,manager`, logging in as "admin" or "manager" gives admin access 4. **You're in!** Welcome to your dashboard @@ -106,27 +115,49 @@ python app.py ### Step 1: Configure System Settings -1. Go to **Admin → System Settings** (gear icon in top right) +> **Important**: You need admin access for this step. Login with a username from `ADMIN_USERNAMES` (default: `admin`). -2. **Set your company information**: - - Company name - - Address for invoices - - Optional: Upload your logo +1. Go to **Admin → Settings** (in the left sidebar menu, expand "Admin", then click "Settings") -3. **Configure regional settings**: - - **Timezone**: Your local timezone (e.g., `America/New_York`) - - **Currency**: Your preferred currency (e.g., `USD`, `EUR`, `GBP`) +The Admin Settings page has multiple sections. Configure what you need: -4. **Adjust timer behavior**: - - **Idle Timeout**: How long before auto-pause (default: 30 minutes) - - **Single Active Timer**: Allow only one running timer per user - - **Time Rounding**: Round to nearest minute/5 minutes/15 minutes +#### General Settings +- **Timezone**: Your local timezone (e.g., `America/New_York`, `Europe/Rome`) +- **Currency**: Your preferred currency (e.g., `USD`, `EUR`, `GBP`) -5. **Set user management**: - - **Allow Self-Registration**: Let users create their own accounts - - **Admin Usernames**: Comma-separated list of admin users +#### Timer Settings +- **Rounding (Minutes)**: Round to nearest 1/5/15 minutes +- **Idle Timeout (Minutes)**: Auto-pause after idle (default: 30) +- **Single Active Timer**: Allow only one running timer per user -6. **Click Save** to apply settings +#### User Management +- **Allow Self-Registration**: ☑ Enable this to let users create accounts by entering any username on the login page +- **Note**: Admin users are set via `ADMIN_USERNAMES` environment variable, not in this UI + +#### Company Branding +- **Company Name**: Your company or business name +- **Company Email**: Contact email for invoices +- **Company Phone**: Contact phone number +- **Company Website**: Your website URL +- **Company Address**: Your billing address (multi-line) +- **Tax ID**: Optional tax identification number +- **Bank Information**: Optional bank account details for invoices +- **Company Logo**: Upload your logo (PNG, JPG, GIF, SVG, WEBP) + +#### Invoice Defaults +- **Invoice Prefix**: Prefix for invoice numbers (e.g., `INV`) +- **Invoice Start Number**: Starting number for invoices (e.g., 1000) +- **Default Payment Terms**: Terms text (e.g., "Payment due within 30 days") +- **Default Invoice Notes**: Footer notes (e.g., "Thank you for your business!") + +#### Additional Settings +- **Backup Settings**: Retention days and backup time +- **Export Settings**: CSV delimiter preference +- **Privacy & Analytics**: Allow analytics to help improve the application + +2. **Click "Save Settings"** at the bottom to apply all changes + +> **💡 Tip**: Don't confuse this with the **Settings** option in your account dropdown (top right) - that's for personal/user preferences. System-wide settings are in **Admin → Settings** in the left sidebar. ### Step 2: Add Your First Client @@ -294,7 +325,7 @@ Break your project into manageable tasks: ### Customize Your Experience -- **Upload your logo** for branded invoices +- **Company branding**: Upload your logo and set company info in Admin → Settings - **Configure notifications** for task due dates - **Set up recurring time blocks** for regular tasks - **Create saved filters** for common report views @@ -305,9 +336,10 @@ Break your project into manageable tasks: If you're setting up for a team: 1. **Add team members**: - - Go to **Admin → Users** - - Users can self-register (if enabled) or admin can add them - - Assign roles (Admin/User) + - **Self-registration** (recommended): Enable in Admin → Settings → "Allow Self-Registration" + - **Admin creates users**: Go to Admin → Users → Create User + - **Admin roles**: Set via `ADMIN_USERNAMES` environment variable (comma-separated list) + - Regular users can be assigned Manager or User roles via Admin → Users → Edit 2. **Assign projects**: - Projects are visible to all users @@ -333,10 +365,13 @@ Ready to deploy for real use? DATABASE_URL=postgresql://user:pass@localhost:5432/timetracker ``` -2. **Set a secure secret key**: +2. **Set a secure secret key and admin users**: ```bash # Generate a random key SECRET_KEY=$(python -c 'import secrets; print(secrets.token_hex(32))') + + # Set admin usernames (comma-separated) + ADMIN_USERNAMES=admin,yourusername ``` 3. **Configure for production**: @@ -411,9 +446,9 @@ docker-compose up -d ### How do I add more users? -- Enable self-registration in settings, or -- Users can just enter a username on first visit, or -- Admin can create users in Admin → Users +- **Enable self-registration**: In Admin → Settings, enable "Allow Self-Registration" - then anyone can create an account by entering a username on the login page +- **Admin creates users**: In Admin → Users → Create User (requires admin access) +- **Users in ADMIN_USERNAMES**: Any username listed in the `ADMIN_USERNAMES` environment variable will automatically get admin role when they login ### Can I export my data? diff --git a/test_logo_pdf.py b/test_logo_pdf.py new file mode 100644 index 0000000..4da1e57 --- /dev/null +++ b/test_logo_pdf.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +""" +Debug script to test company logo in PDF generation +Run this to check if logo is properly configured and can be embedded in PDFs +""" + +import os +import sys + +# Add app to path +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from app import create_app +from app.models import Settings, Invoice +from app.utils.pdf_generator import InvoicePDFGenerator + +def test_logo_setup(): + """Test if logo is properly configured""" + print("=" * 60) + print("LOGO CONFIGURATION TEST") + print("=" * 60) + + app = create_app() + with app.app_context(): + settings = Settings.get_settings() + + print(f"\n1. Logo filename in database: {settings.company_logo_filename or 'NONE'}") + + if not settings.company_logo_filename: + print(" ❌ NO LOGO UPLOADED") + print(" → Upload a logo in Admin → Settings → Company Branding") + return False + + print(f" ✓ Logo filename found: {settings.company_logo_filename}") + + logo_path = settings.get_logo_path() + print(f"\n2. Logo file path: {logo_path}") + + if not logo_path: + print(" ❌ Could not determine logo path") + return False + + if not os.path.exists(logo_path): + print(f" ❌ LOGO FILE DOES NOT EXIST AT: {logo_path}") + print(f" → Check if file exists in app/static/uploads/logos/") + return False + + print(f" ✓ Logo file exists") + + file_size = os.path.getsize(logo_path) + print(f"\n3. Logo file size: {file_size:,} bytes ({file_size/1024:.2f} KB)") + + if file_size == 0: + print(" ❌ Logo file is empty") + return False + + if file_size > 5 * 1024 * 1024: # 5MB + print(" ⚠️ Logo file is very large (>5MB). Consider optimizing.") + + print(f" ✓ Logo file has content") + + # Test base64 encoding + print(f"\n4. Testing base64 encoding...") + try: + from app.utils.template_filters import get_logo_base64 + data_uri = get_logo_base64(logo_path) + + if not data_uri: + print(" ❌ Base64 encoding failed (returned None)") + return False + + if not data_uri.startswith('data:image/'): + print(f" ❌ Invalid data URI: {data_uri[:50]}...") + return False + + encoded_size = len(data_uri) + print(f" ✓ Base64 encoding successful") + print(f" Data URI size: {encoded_size:,} bytes ({encoded_size/1024:.2f} KB)") + print(f" Data URI prefix: {data_uri[:50]}...") + + except Exception as e: + print(f" ❌ Error encoding logo: {e}") + import traceback + traceback.print_exc() + return False + + print("\n" + "=" * 60) + print("✓ LOGO CONFIGURATION IS CORRECT") + print("=" * 60) + return True + +def test_pdf_generation(): + """Test PDF generation with logo""" + print("\n" + "=" * 60) + print("PDF GENERATION TEST") + print("=" * 60) + + app = create_app() + with app.app_context(): + # Get the most recent invoice + invoice = Invoice.query.order_by(Invoice.id.desc()).first() + + if not invoice: + print("\n❌ NO INVOICES FOUND") + print(" → Create an invoice first to test PDF generation") + return False + + print(f"\n1. Testing with Invoice #{invoice.invoice_number}") + print(f" Project: {invoice.project.name}") + print(f" Client: {invoice.client_name}") + + try: + print(f"\n2. Generating PDF...") + generator = InvoicePDFGenerator(invoice) + pdf_bytes = generator.generate_pdf() + + if not pdf_bytes: + print(" ❌ PDF generation returned no data") + return False + + pdf_size = len(pdf_bytes) + print(f" ✓ PDF generated successfully") + print(f" PDF size: {pdf_size:,} bytes ({pdf_size/1024:.2f} KB)") + + # Save PDF for inspection + output_file = 'test_invoice.pdf' + with open(output_file, 'wb') as f: + f.write(pdf_bytes) + + print(f"\n3. PDF saved to: {output_file}") + print(f" → Open this file to check if logo appears") + + print("\n" + "=" * 60) + print("✓ PDF GENERATION SUCCESSFUL") + print("=" * 60) + print(f"\n📄 Check the file: {output_file}") + print(" Look for the logo in the top-left corner of the invoice") + + return True + + except Exception as e: + print(f"\n ❌ Error generating PDF: {e}") + import traceback + traceback.print_exc() + return False + +def main(): + """Run all tests""" + print("\n🔍 TimeTracker PDF Logo Diagnostic Tool\n") + + logo_ok = test_logo_setup() + + if logo_ok: + pdf_ok = test_pdf_generation() + + if pdf_ok: + print("\n" + "=" * 60) + print("✅ ALL TESTS PASSED") + print("=" * 60) + print("\nIf the logo still doesn't appear in the PDF:") + print("1. Open test_invoice.pdf and check manually") + print("2. Check server logs for DEBUG messages when generating invoices") + print("3. Try uploading a different logo (simple PNG <1MB)") + print("4. Verify the logo works in the web UI first (Admin → Settings)") + return 0 + else: + print("\n" + "=" * 60) + print("❌ PDF GENERATION FAILED") + print("=" * 60) + return 1 + else: + print("\n" + "=" * 60) + print("❌ LOGO CONFIGURATION FAILED") + print("=" * 60) + print("\nPlease fix the logo configuration first:") + print("1. Login as admin") + print("2. Go to Admin → Settings") + print("3. Scroll to Company Branding section") + print("4. Upload a logo (PNG, JPG, GIF, SVG, or WEBP)") + print("5. Run this script again") + return 1 + +if __name__ == '__main__': + try: + sys.exit(main()) + except KeyboardInterrupt: + print("\n\nTest interrupted by user") + sys.exit(130) + except Exception as e: + print(f"\n\n❌ UNEXPECTED ERROR: {e}") + import traceback + traceback.print_exc() + sys.exit(1) +