diff --git a/TODO-reports-improvements.md b/TODO-reports-improvements.md new file mode 100644 index 0000000..08cdfd9 --- /dev/null +++ b/TODO-reports-improvements.md @@ -0,0 +1,1158 @@ +# 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: , , ,
,

,