# TODO: Reports Improvements **Branch:** `v20260207-01-reports-improvements` **Datum:** 2026-02-07 **Status:** Planning fase --- ## ✅ Wat is al gedaan - ✅ Scheduling placeholder verwijderd van reports overview pagina (reports.html) - ✅ Report definitions tabel gebruikt nu volle breedte --- ## 🔄 Scope Uitbreiding: Report Settings & Branding **Nieuwe requirement:** Settings sectie voor reporting met email/branding configuratie. --- ## 🔄 Wat moet nog: Relative Period Selector ### Doel Gebruikers moeten relatieve periodes kunnen kiezen (zoals "Previous month", "Last 7 days") in plaats van alleen vaste datums. Dit is essentieel voor scheduled reports: als je een maandelijks rapport hebt met "Previous month", wordt elke keer automatisch de juiste maand berekend. ### UI Design **Period selector in reports_new.html:** ``` ┌─ Reporting period ────────────────────────────────────┐ │ │ │ Period type: │ │ ○ Relative period (recommended for scheduled reports)│ │ └─ Select period: │ │ [Dropdown ▼] │ │ --- Recent periods --- │ │ - Yesterday │ │ - Last 7 days │ │ - Last 14 days │ │ - Last 30 days │ │ - Last 90 days │ │ - Last 6 months │ │ --- Weeks --- │ │ - Last week (full) │ │ - Current week (to date) │ │ --- Months --- │ │ - Previous month (full) │ │ - Current month (to date) │ │ --- Quarters --- │ │ - Last quarter (full) │ │ - Current quarter (to date) │ │ --- Years --- │ │ - Previous year (full) │ │ - Current year (to date) │ │ │ │ ○ Custom date range (for one-time reports) │ │ └─ Start: [date] [time] │ │ End: [date] [time] │ │ │ │ [Preset: Current month] [Last month] [Last month full]│ └───────────────────────────────────────────────────────┘ ``` ### Voordelen - ✅ Scheduled reports werken automatisch zonder datum aanpassingen - ✅ Duidelijk zichtbaar welke periode gebruikt wordt ("Previous month" ipv "2026-01-01 to 2026-01-31") - ✅ Eenvoudiger te configureren - ✅ Minder foutgevoelig - ✅ Betere UX voor recurring reports --- ## 📋 Implementatie Plan (Prioriteit) ### Phase 1: Settings > Reporting Section #### 1A. Database Model - SystemSettings **Bestand:** `containers/backupchecks/src/backend/app/models.py` **Add reporting_* fields** (see "Database Model - SystemSettings" section above) #### 1B. Database Migration **Bestand:** `containers/backupchecks/src/backend/app/migrations.py` ```python def migrate_add_reporting_settings(): """Add reporting configuration fields to system_settings table.""" pass ``` #### 1C. Settings UI **Bestand:** `containers/backupchecks/src/templates/main/settings.html` - Add new "Reporting" section/tab - Email settings form (from_email, from_name, reply_to) - Branding form (company_name, logo upload, brand_color) - Footer form (contact info, footer text) - Save button + validation #### 1D. Settings Routes **Bestand:** `containers/backupchecks/src/backend/app/main/routes_settings.py` - Extend POST /settings to handle reporting_form_touched - Add POST /settings/reporting/test-email route - Logo upload handling (validate, store blob) - Email validation ### Phase 2: Relative Periods #### 2A. Database Model - Report **Bestand:** `containers/backupchecks/src/backend/app/models.py` **Huidige Report model velden:** - `period_start` (String) - ISO datetime - `period_end` (String) - ISO datetime **Nieuwe velden toevoegen:** - `period_type` (String) - "relative" of "custom" - `relative_period` (String) - "previous_month", "last_7_days", etc. - `email_body_template` (Text) - Custom HTML email body with placeholders - `schedule_enabled` (Boolean) - Is scheduling active for this report? - `schedule_frequency` (String) - "weekly", "monthly", "quarterly", "yearly" - `schedule_time` (String) - "HH:MM" in ui_timezone - `schedule_day_of_week` (Integer) - 0-6 (Monday-Sunday) for weekly - `schedule_day_of_month` (Integer) - 1-31 for monthly - `schedule_month` (Integer) - 1-12 for yearly - `schedule_recipients` (String) - Comma-separated email addresses - `schedule_last_run_at` (DateTime) - Last successful run - `schedule_next_run_at` (DateTime) - Calculated next run time - `schedule_last_error` (String) - Last error message - `schedule_retry_count` (Integer) - Current retry count (max 3) **Migratie:** - `migrate_add_report_relative_periods()` functie - Bestaande reports krijgen `period_type="custom"` (backwards compatible) - Idempotent, veilig te draaien #### 2B. Backend Routes Update **Bestand:** `containers/backupchecks/src/backend/app/main/routes_reports.py` **POST/PUT /api/reports:** - Accept new fields: `period_type`, `relative_period` - Validate relative_period values - Store in database **POST /api/reports/{id}/generate:** - Als `period_type == "relative"`: - Calculate concrete dates from `relative_period` - Use calculated dates for report generation - Als `period_type == "custom"`: - Use existing `period_start` and `period_end` #### 2C. Relative Period Calculator **Nieuw bestand:** `containers/backupchecks/src/backend/app/report_periods.py` ```python def calculate_period(relative_period: str, ui_timezone: str = "Europe/Amsterdam") -> tuple[datetime, datetime]: """ Calculate concrete start/end dates for a relative period. Returns (start_datetime, end_datetime) in the specified timezone, then converted to UTC. IMPORTANT: All calculations use ui_timezone (not UTC). "To date" periods go up to END OF YESTERDAY (23:59:59) for complete days. Example for ui_timezone="Europe/Amsterdam" on 2026-02-07: - yesterday: 2026-02-06 00:00:00 → 2026-02-06 23:59:59 (Amsterdam time) - last_7_days: 2026-01-31 00:00:00 → 2026-02-06 23:59:59 (Amsterdam time) - current_month: 2026-02-01 00:00:00 → 2026-02-06 23:59:59 (Amsterdam time) Supported periods: - yesterday: Yesterday full day (00:00 to 23:59) - last_7_days: 7 days ago 00:00 to yesterday 23:59 - last_14_days: 14 days ago 00:00 to yesterday 23:59 - last_30_days: 30 days ago 00:00 to yesterday 23:59 - last_90_days: 90 days ago 00:00 to yesterday 23:59 - last_6_months: 6 months ago 00:00 to yesterday 23:59 - last_week: Previous week Monday 00:00 to Sunday 23:59 - current_week: This week Monday 00:00 to yesterday 23:59 - previous_month: Full previous month (1st 00:00 to last day 23:59) - current_month: Current month 1st 00:00 to yesterday 23:59 - last_quarter: Previous quarter first day 00:00 to last day 23:59 - current_quarter: Current quarter first day 00:00 to yesterday 23:59 - previous_year: Full previous year (Jan 1 00:00 to Dec 31 23:59) - current_year: Current year Jan 1 00:00 to yesterday 23:59 """ pass ``` #### 2D. UI Update - reports_new.html **Bestand:** `containers/backupchecks/src/templates/main/reports_new.html` **Changes:** - Add radio buttons: "Relative period" vs "Custom date range" - Add dropdown for relative periods (only visible when "Relative" selected) - Show/hide date pickers based on selection - Update JavaScript to handle new fields - Update validation logic - Preserve presets for custom date range mode **JavaScript changes:** - `selectedPeriodType()` function - `updatePeriodUi()` function to show/hide sections - Update `createReport()` payload building - Update `applyInitialReport()` for edit mode - Add email body template field with placeholders help text #### 2E. UI Update - reports.html **Bestand:** `containers/backupchecks/src/templates/main/reports.html` **Changes:** - Display relative period label instead of dates when `period_type == "relative"` - Format: Badge with label - Example: `[Previous month ●]` where ● is a small badge/indicator - Show tooltip on hover: "Automatically calculated on each generation" - For custom dates: Keep current format "2026-01-01 → 2026-01-31" ### Phase 3: Email Sending (Future - Scheduling) #### 3A. Graph API Email Sender **Nieuw bestand:** `containers/backupchecks/src/backend/app/report_email.py` See "Graph API Email Sending" section above for implementation details. #### 3B. Template Processor **Functie om placeholders te vervangen:** ```python import re import html from typing import Dict, List ALLOWED_PLACEHOLDERS = { 'customer_name', 'period', 'total_jobs', 'success_count', 'failed_count', 'success_rate', 'company_name', 'logo_base64' } def validate_email_template(template: str) -> tuple[bool, List[str]]: """ Validate email template for invalid placeholders. Returns: (is_valid, list_of_invalid_placeholders) """ if not template: return True, [] # Find all placeholders placeholders = re.findall(r'\{([^}]+)\}', template) invalid = [p for p in placeholders if p not in ALLOWED_PLACEHOLDERS] return len(invalid) == 0, invalid def process_email_template(template: str, context: dict, settings) -> str: """ Replace placeholders in email template with actual values. Placeholders: {customer_name}, {period}, {total_jobs}, etc. Sanitizes HTML to prevent XSS. """ if not template: # Use default template from settings template = settings.reporting_default_email_template or get_hardcoded_default_template() # Add logo to context if not present if 'logo_base64' not in context and settings.reporting_logo_blob: mime_type = settings.reporting_logo_mime_type or 'image/png' logo_b64 = base64.b64encode(settings.reporting_logo_blob).decode('utf-8') context['logo_base64'] = f"data:{mime_type};base64,{logo_b64}" else: context['logo_base64'] = "" # Empty if no logo # Add company name context['company_name'] = settings.reporting_company_name or "Backup Reports" # Escape HTML in context values (prevent XSS from data) safe_context = {k: html.escape(str(v)) for k, v in context.items() if k != 'logo_base64'} safe_context['logo_base64'] = context.get('logo_base64', '') # Don't escape base64 # Replace placeholders result = template for key, value in safe_context.items(): result = result.replace(f'{{{key}}}', str(value)) # Sanitize HTML (allow safe tags only) result = sanitize_html(result) return result def sanitize_html(html_content: str) -> str: """ Sanitize HTML to prevent XSS while allowing basic formatting. Allowed tags: , , ,
,

,