Complete planning document for reporting feature expansion including: Phase 1 - Settings > Reporting: - Email settings (from_email, from_name, reply_to) - Branding (logo upload, company_name, brand_color) - Footer customization (contact info, footer text) - Default email template with HTML support - Test email function with Graph API permission check Phase 2 - Relative Periods: - 15 relative period options (yesterday to previous year) - Timezone-aware period calculation (follows ui_timezone) - "To date" periods end at yesterday 23:59 (complete days only) - Email body template per report (HTML with placeholders) - Template validation and XSS protection Phase 3 - Scheduling (Future): - Weekly, Monthly, Quarterly, Yearly frequencies - Per-report scheduling configuration - Graph API email sending (no SMTP/SPF issues) - Retry logic (max 3 attempts) - Audit logging for deliveries Includes: - Complete implementation plan with code snippets - Database model changes (25+ fields) - UI mockups for Settings and report creation - 60+ test cases covering all features - Success criteria per phase - All design decisions documented Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
46 KiB
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
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 datetimeperiod_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 placeholdersschedule_enabled(Boolean) - Is scheduling active for this report?schedule_frequency(String) - "weekly", "monthly", "quarterly", "yearly"schedule_time(String) - "HH:MM" in ui_timezoneschedule_day_of_week(Integer) - 0-6 (Monday-Sunday) for weeklyschedule_day_of_month(Integer) - 1-31 for monthlyschedule_month(Integer) - 1-12 for yearlyschedule_recipients(String) - Comma-separated email addressesschedule_last_run_at(DateTime) - Last successful runschedule_next_run_at(DateTime) - Calculated next run timeschedule_last_error(String) - Last error messageschedule_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
- Calculate concrete dates from
- Als
period_type == "custom":- Use existing
period_startandperiod_end
- Use existing
2C. Relative Period Calculator
Nieuw bestand: containers/backupchecks/src/backend/app/report_periods.py
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()functionupdatePeriodUi()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:
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: <strong>, <em>, <u>, <br>, <p>, <ul>, <ol>, <li>, <a>, <img>
"""
# Use bleach library or similar
import bleach
allowed_tags = ['strong', 'em', 'u', 'br', 'p', 'ul', 'ol', 'li', 'a', 'img', 'div', 'span']
allowed_attrs = {
'a': ['href', 'title'],
'img': ['src', 'alt', 'style'],
'div': ['style'],
'span': ['style'],
'p': ['style']
}
return bleach.clean(
html_content,
tags=allowed_tags,
attributes=allowed_attrs,
strip=True
)
def get_hardcoded_default_template() -> str:
"""Fallback template if nothing configured."""
return """
<div style="font-family: Arial, sans-serif; max-width: 600px;">
<img src="{logo_base64}" alt="{company_name}" style="max-width: 200px; margin-bottom: 20px;" />
<p>Dear <strong>{customer_name}</strong>,</p>
<p>Attached is your backup report for <strong>{period}</strong>.</p>
<ul>
<li>Total jobs: {total_jobs}</li>
<li>Successful: {success_count}</li>
<li>Failed: {failed_count}</li>
<li>Success rate: {success_rate}%</li>
</ul>
<p>Best regards,<br/>{company_name}</p>
</div>
"""
Dependencies:
pip install bleach # For HTML sanitization
✅ Testing Checklist
Settings > Reporting (Phase 1)
Basic Settings:
- Navigate to Settings > Reporting section
- Save from_email - validate email format
- Save from_name - verify stored correctly
- Save reply_to - validate email format (optional field)
- Leave reply_to empty - verify falls back to from_email
Logo Upload:
- Upload logo (PNG, 100 KB) - verify preview shows
- Upload logo (JPG, 200 KB) - verify preview shows
- Upload logo (SVG, 50 KB) - verify preview shows
- Upload logo too large (600 KB) - verify error message
- Upload invalid file type (TXT) - verify error rejected
- Remove logo - verify cleared and preview empty
Branding:
- Save company_name - verify stored
- Save brand_color - validate hex format (#1a73e8)
- Invalid brand_color (not hex) - verify error
- Brand color without # prefix - auto-add or error?
Footer:
- Save contact info (phone, email, website) - verify stored
- Save footer_text - verify stored
Default Email Template:
- Save default_email_template with placeholders - verify stored
- Invalid placeholder in template - verify validation error
- Live preview updates as typing - verify works
- Leave template empty - verify uses hardcoded default
Graph API & Test Email:
- Mail.Send permission NOT granted - verify error message when testing
- Mail.Send permission granted - verify test email works
- Click "Send test email" - verify email received
- Check test email has logo inline (base64)
- Check test email has company name
- Check test email has brand color (if applied to template)
- Check test email has footer text
- Update settings - verify changes reflected in new test email
- Test email uses default template - verify placeholders replaced
- Reporting_from_email different from graph_mailbox - verify works
Relative Periods (Phase 2)
Basic Functionality:
- Create report with relative period "Previous month"
- Create report with relative period "Last 7 days"
- Create report with custom date range (backwards compatibility)
- Edit report: change from custom to relative
- Edit report: change from relative to custom
- Edit existing custom report - should still work (backwards compatibility)
Period Calculation (verify end date = yesterday 23:59):
- Yesterday: verify full day
- Last 7 days: verify 7 days ago to yesterday
- Last 14 days: verify 14 days ago to yesterday
- Last 30 days: verify 30 days ago to yesterday
- Last 90 days: verify 90 days ago to yesterday
- Last 6 months: verify 6 months ago to yesterday
- Last week (full): verify previous Monday-Sunday
- Current week (to date): verify this Monday to yesterday
- Previous month (full): verify full previous month
- Current month (to date): verify 1st to yesterday
- Last quarter (full): verify previous quarter
- Current quarter (to date): verify quarter start to yesterday
- Previous year (full): verify full previous year
- Current year (to date): verify Jan 1 to yesterday
Timezone Handling:
- Set ui_timezone to "Europe/Amsterdam" - verify periods calculated in Amsterdam time
- Set ui_timezone to "America/New_York" - verify periods calculated in NY time
- Set ui_timezone to "UTC" - verify periods calculated in UTC
- Verify generated report shows correct timezone in period description
UI/UX:
- Generate report with relative period - verify correct dates in output
- Generate same relative period report on different days - verify dates change automatically
- Check reports overview displays relative period with badge correctly
- Check tooltip shows "Automatically calculated on each generation"
- Check raw data modal works with relative periods
- Check CSV export filename includes relative period or calculated dates
- Check PDF/HTML export shows relative period in header
Edge Cases:
- Generate "current_month" on 1st day of month - verify works
- Generate "last_quarter" on first day of new quarter - verify correct previous quarter
- Generate report in different timezone than when created - verify still correct
Email Body Template (Per Report):
- Create report with custom email_body_template (HTML)
- Leave email_body_template empty - verify uses default from Settings
- Use all placeholders in template - verify replaced correctly
- Invalid placeholder in template - verify validation error on save
- HTML tags in template - verify sanitized (XSS-safe)
- Dangerous HTML (script tag) - verify stripped/rejected
- Live preview in reports_new.html - verify shows sample data
- Edit report - change email_body_template - verify saved
- Logo placeholder {logo_base64} - verify renders inline image
- No logo uploaded in Settings - verify {logo_base64} is empty string
🎯 Relative Period Options
IMPORTANT: All periods use ui_timezone (from SystemSettings). "To date" periods end at yesterday 23:59:59 (complete days only).
Recent Periods
| Key | Label | Description |
|---|---|---|
yesterday |
Yesterday | Yesterday 00:00 to yesterday 23:59 |
last_7_days |
Last 7 days | 7 days ago 00:00 to yesterday 23:59 |
last_14_days |
Last 14 days | 14 days ago 00:00 to yesterday 23:59 |
last_30_days |
Last 30 days | 30 days ago 00:00 to yesterday 23:59 |
last_90_days |
Last 90 days | 90 days ago 00:00 to yesterday 23:59 |
last_6_months |
Last 6 months | 6 months ago 00:00 to yesterday 23:59 |
Week Periods
| Key | Label | Description |
|---|---|---|
last_week |
Last week (full) | Previous week Monday 00:00 to Sunday 23:59 |
current_week |
Current week (to date) | This week Monday 00:00 to yesterday 23:59 |
Month Periods
| Key | Label | Description |
|---|---|---|
previous_month |
Previous month (full) | 1st day 00:00 to last day 23:59 of previous month |
current_month |
Current month (to date) | 1st day 00:00 to yesterday 23:59 |
Quarter Periods
| Key | Label | Description |
|---|---|---|
last_quarter |
Last quarter (full) | Previous quarter Q1/Q2/Q3/Q4 first day 00:00 to last day 23:59 |
current_quarter |
Current quarter (to date) | Current quarter first day 00:00 to yesterday 23:59 |
Year Periods
| Key | Label | Description |
|---|---|---|
previous_year |
Previous year (full) | Jan 1 00:00 to Dec 31 23:59 of previous year |
current_year |
Current year (to date) | Jan 1 00:00 to yesterday 23:59 |
Total: 15 relative period options
📝 Beslissingen Genomen
✅ Tijdstippen
- Besluit: Optie B - Tot einde van gisteren (volledige dagen)
- Reden: Complete days only, geen partial data van vandaag
- Voorbeeld: "Last 7 days" op 7 feb = 31 jan 00:00 tot 6 feb 23:59
✅ Extra Periods
- Besluit: Alle voorgestelde periods toevoegen (15 totaal)
- Toegevoegd: Yesterday, Last 14 days, Last 6 months, Week periods, Quarter periods
✅ Weergave in Reports Lijst
- Besluit: Optie A - Badge met label
- Format:
[Previous month ●]met tooltip - Tooltip: "Automatically calculated on each generation"
✅ Scheduling
- Besluit: Wordt later besproken
- Nu: Alleen relative periods voor handmatig genereren
- Later: Scheduling implementatie met relative periods
✅ Timezone
- Besluit: ui_timezone setting volgen (niet UTC)
- Implementatie: Alle berekeningen in configured timezone, dan converteren naar UTC
- Voorbeeld: "Previous month" in Amsterdam timezone, niet UTC month
✅ Email Body Template
- Besluit: Per rapport configureren (niet algemene setting)
- Locatie: In report definition (reports_new.html), niet in Settings
- Reden: Elk rapport kan andere tekst/tone hebben afhankelijk van klant
✅ Email Sending (Technical)
- Besluit: Graph API hergebruiken (geen SMTP)
- Redenen:
- Microsoft 365 Basic Authentication/SMTP wordt uitgefaseerd
- Geen SPF record aanpassingen nodig (DNS lookup limit van 10 al bereikt)
- Consistent met bestaande mail import functionaliteit
- Implementatie: Hergebruik bestaande Graph API client/token flow
📧 Settings > Reporting Section (NEW)
Doel
Centrale configuratie voor report branding en email settings. Deze settings gelden voor alle scheduled reports (tenzij per rapport overschreven).
UI Design - settings.html
Nieuwe tab/sectie: "Reporting" (naast General, Mail, Integrations, etc.)
┌─ Reporting Settings ─────────────────────────────────┐
│ │
│ ┌─ Email Settings ───────────────────────────────┐ │
│ │ │ │
│ │ From email address * │ │
│ │ [email@example.com________________________] │ │
│ │ The email address used to send reports. │ │
│ │ Must be a mailbox in your Microsoft 365 tenant. │ │
│ │ │ │
│ │ From name * │ │
│ │ [Acme Backup Services_________________] │ │
│ │ Display name shown in recipient's inbox. │ │
│ │ │ │
│ │ Reply-to address (optional) │ │
│ │ [support@example.com__________________] │ │
│ │ Where replies should go (leave empty to use │ │
│ │ from address). │ │
│ │ │ │
│ │ [Send test email] │ │
│ │ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌─ Report Branding ──────────────────────────────┐ │
│ │ │ │
│ │ Company name * │ │
│ │ [Acme Backup Services_________________] │ │
│ │ Displayed in report headers and footers. │ │
│ │ │ │
│ │ Company logo │ │
│ │ [Current: logo.png (45 KB)] │ │
│ │ [Choose file...] [Remove logo] │ │
│ │ Max 500 KB. PNG, JPG, or SVG. Recommended: │ │
│ │ 300x100px or similar landscape aspect ratio. │ │
│ │ │ │
│ │ Brand color (hex) │ │
│ │ [#] [1a73e8___] [Color picker] │ │
│ │ Primary color for HTML/PDF reports. │ │
│ │ │ │
│ │ ┌─ Preview ─────────────────────────────────┐ │ │
│ │ │ [LOGO] Acme Backup Services │ │ │
│ │ │ Backup Report - January 2026 │ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌─ Report Footer ────────────────────────────────┐ │
│ │ │ │
│ │ Contact information (optional) │ │
│ │ Phone: [+31 20 123 4567_______________] │ │
│ │ Email: [info@example.com______________] │ │
│ │ Website: [https://example.com__________] │ │
│ │ │ │
│ │ Footer text (optional) │ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ This is an automated report. For questions │ │ │
│ │ │ please contact our support team. │ │ │
│ │ │ │ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ │ Shown at bottom of all reports. │ │
│ │ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌─ Default Email Template ───────────────────────┐ │
│ │ │ │
│ │ Used when report has no custom template. │ │
│ │ Supports HTML and placeholders. │ │
│ │ │ │
│ │ Template (HTML): │ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ <div style="font-family: Arial;"> │ │ │
│ │ │ <img src="{logo_base64}" /> │ │ │
│ │ │ <p>Dear <strong>{customer_name}</strong> │ │ │
│ │ │ ... │ │ │
│ │ │ │ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Available placeholders: │ │
│ │ {customer_name}, {period}, {total_jobs}, │ │
│ │ {success_count}, {failed_count}, {success_rate},│ │
│ │ {company_name}, {logo_base64} │ │
│ │ │ │
│ │ ┌─ Live Preview ─────────────────────────────┐ │ │
│ │ │ [LOGO] Acme Backup Services │ │ │
│ │ │ │ │ │
│ │ │ Dear Customer Name, │ │ │
│ │ │ Attached is your report for January 2026. │ │ │
│ │ │ • Total jobs: 25 │ │ │
│ │ │ • Success rate: 96% │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ [Save Reporting Settings] │
│ │
└───────────────────────────────────────────────────────┘
Database Model - SystemSettings
Nieuwe velden toevoegen:
# Email settings
reporting_from_email = Column(String, nullable=True) # "reports@example.com"
reporting_from_name = Column(String, nullable=True) # "Acme Backup Services"
reporting_reply_to = Column(String, nullable=True) # "support@example.com"
# Branding
reporting_company_name = Column(String, nullable=True) # "Acme Backup Services"
reporting_logo_blob = Column(LargeBinary, nullable=True) # Logo image bytes
reporting_logo_filename = Column(String, nullable=True) # "logo.png"
reporting_logo_mime_type = Column(String, nullable=True) # "image/png"
reporting_brand_color = Column(String, nullable=True) # "#1a73e8"
# Footer
reporting_contact_phone = Column(String, nullable=True) # "+31 20 123 4567"
reporting_contact_email = Column(String, nullable=True) # "info@example.com"
reporting_contact_website = Column(String, nullable=True) # "https://example.com"
reporting_footer_text = Column(Text, nullable=True) # Custom footer text
# Email Template
reporting_default_email_template = Column(Text, nullable=True) # Default HTML email body
Total: 11 nieuwe velden
Database Migration
Bestand: migrations.py
def migrate_add_reporting_settings():
"""Add reporting configuration fields to system_settings table."""
# Check if columns already exist (idempotent)
# Add columns with ALTER TABLE
# Safe to run multiple times
Backend Routes - routes_settings.py
POST /settings (extend existing route):
Detect reporting form submission:
reporting_form_touched = any(k.startswith("reporting_") for k in request.form.keys())
if reporting_form_touched:
# Update reporting_* fields
# Handle logo upload (validate size, type)
# Validate email address (must be valid + in tenant?)
# Validate hex color format
db.session.commit()
POST /settings/reporting/test-email:
New route to send test email:
@main_bp.route("/settings/reporting/test-email", methods=["POST"])
@login_required
@roles_required("admin")
def settings_reporting_test_email():
"""Send test report email to validate settings and permissions."""
settings = _get_or_create_settings()
# Validate required fields
if not settings.reporting_from_email:
return jsonify({"error": "From email not configured"}), 400
# Check Graph API Mail.Send permission
try:
has_permission = check_graph_mail_send_permission(settings)
if not has_permission:
return jsonify({
"error": "Mail.Send permission not granted. Please add it in Azure Portal."
}), 403
except Exception as e:
return jsonify({"error": f"Permission check failed: {str(e)}"}), 500
# Send test email to admin
test_email_body = render_test_email_template(settings)
try:
send_report_email(
to_address=current_user.email,
subject=f"Test Email - {settings.reporting_company_name or 'Backup Reports'}",
body_html=test_email_body,
attachment_bytes=b"", # No attachment for test
attachment_name=""
)
return jsonify({"success": True, "message": f"Test email sent to {current_user.email}"}), 200
except Exception as e:
return jsonify({"error": f"Failed to send test email: {str(e)}"}), 500
Helper function:
def check_graph_mail_send_permission(settings) -> bool:
"""Check if Graph API has Mail.Send permission."""
access_token = _get_access_token(settings)
headers = _build_auth_headers(access_token)
# Try to get mailbox info - if this works, Mail.Send is likely granted
# Or parse token claims to check permissions
url = f"{GRAPH_BASE_URL}/users/{settings.reporting_from_email}"
response = requests.get(url, headers=headers)
if response.status_code == 403:
return False # No permission
elif response.status_code == 200:
return True # Has permission (simplified check)
else:
raise Exception(f"Permission check failed: HTTP {response.status_code}")
Logo Upload Handling
Validation:
- Max file size: 500 KB
- Allowed formats: PNG, JPG, JPEG, SVG
- Store as blob in database (not filesystem - easier backup/restore)
Preview:
- Show thumbnail in settings UI
- Option to remove/replace logo
Graph API Email Sending
Reuse existing Graph API setup:
from ..mail_importer import _get_access_token, _build_auth_headers, GRAPH_BASE_URL
def send_report_email(to_address, subject, body_html, attachment_bytes, attachment_name):
"""
Send report email via Microsoft Graph API.
Uses SendMail endpoint with attachment.
Requires Mail.Send permission (already have for mail import).
"""
settings = _get_or_create_settings()
access_token = _get_access_token(settings)
# Build email payload
message = {
"message": {
"subject": subject,
"body": {
"contentType": "HTML",
"content": body_html
},
"toRecipients": [{"emailAddress": {"address": to_address}}],
"from": {
"emailAddress": {
"address": settings.reporting_from_email,
"name": settings.reporting_from_name
}
},
"attachments": [{
"@odata.type": "#microsoft.graph.fileAttachment",
"name": attachment_name,
"contentType": "application/pdf",
"contentBytes": base64.b64encode(attachment_bytes).decode()
}]
}
}
if settings.reporting_reply_to:
message["message"]["replyTo"] = [
{"emailAddress": {"address": settings.reporting_reply_to}}
]
# POST to /users/{from_email}/sendMail
url = f"{GRAPH_BASE_URL}/users/{settings.reporting_from_email}/sendMail"
response = requests.post(url, headers=headers, json=message)
# Handle response
Report Model Extension
Email body template per report:
# In Report model, add field:
email_body_template = Column(Text, nullable=True) # Custom email body with placeholders
Placeholders supported:
{customer_name}- Customer name{period}- Report period (e.g. "January 2026" or "Last 7 days"){total_jobs}- Number of jobs in report{success_count}- Number of successful jobs{failed_count}- Number of failed jobs{success_rate}- Success percentage{company_name}- From reporting settings
Default template if empty:
Dear {customer_name},
Attached is your backup report for {period}.
Summary:
- Total jobs: {total_jobs}
- Successful: {success_count}
- Failed: {failed_count}
- Success rate: {success_rate}%
Best regards,
{company_name}
UI Update - reports_new.html
Add email body template field:
After the "Report content" section, add new section:
<hr class="my-4" />
<div class="fw-semibold mb-1">Email settings (for scheduled delivery)</div>
<div class="text-muted small mb-3">
Configure the email body for this report. Leave empty to use default template.
</div>
<div class="mb-3">
<label class="form-label">Email body template (optional)</label>
<textarea class="form-control font-monospace" id="rep_email_body" rows="10"
placeholder="Dear {customer_name}, Attached is your backup report for {period}..."></textarea>
<div class="form-text">
Available placeholders:
<code>{customer_name}</code>,
<code>{period}</code>,
<code>{total_jobs}</code>,
<code>{success_count}</code>,
<code>{failed_count}</code>,
<code>{success_rate}</code>,
<code>{company_name}</code>
</div>
</div>
<div class="alert alert-info">
<strong>Note:</strong> Email settings like "From address" and "Logo" are configured in
<a href="{{ url_for('main.settings', section='reporting') }}">Settings > Reporting</a>.
</div>
📂 Belangrijke Bestanden
containers/backupchecks/src/backend/app/
├── models.py # Report model (add period_type, relative_period, email_body)
│ # SystemSettings model (add reporting_* fields)
├── migrations.py # Add migration functions (reports + settings)
├── report_periods.py # NEW: Period calculator
└── main/
├── routes_reports.py # API routes (accept/process new fields)
└── routes_settings.py # Settings routes (add reporting section handling)
containers/backupchecks/src/templates/main/
├── reports.html # Overview (display relative periods)
├── reports_new.html # Create/Edit (new period selector + email body template)
└── settings.html # Settings (add Reporting section)
docs/
└── changelog-claude.md # Changelog entry
🎯 Implementatie Fases Samenvatting
Phase 1: Settings > Reporting (Start hier) ⭐
Focus: Basis configuratie voor report branding en email
- SystemSettings model uitbreiden (10 velden)
- Settings UI met Reporting sectie
- Logo upload functionaliteit
- Test email functie
- Schatting: Medium complexity
- Dependencies: Geen
Phase 2: Relative Periods
Focus: Dynamische periode selectie
- Report model uitbreiden (3 velden)
- Period calculator met timezone support (15 periods)
- UI updates (reports_new.html + reports.html)
- Email body template per rapport
- Schatting: Medium-High complexity
- Dependencies: Phase 1 settings (voor email body preview)
Phase 3: Email Sending (Toekomstig - Scheduling)
Focus: Automatisch versturen van reports
- Graph API email sender
- Template processor (placeholder replacement)
- Scheduling implementatie (wordt later besproken)
- Schatting: High complexity
- Dependencies: Phase 1 + Phase 2
✅ Alle Beslissingen - Scheduling & Email (Phase 3)
Scheduling Configuration
Frequentie opties:
- ✅ Weekly (elke week op bepaalde dag)
- ✅ Monthly (elke maand op bepaalde dag, bijv. 1e)
- ✅ Quarterly (elk kwartaal)
- ✅ Yearly (elk jaar op bepaalde datum)
- ❌ GEEN Daily (niet nodig)
- ❌ GEEN Custom cron (te complex)
Tijdstip:
- ✅ Per rapport configureerbaar (niet centraal)
- Format: HH:MM in ui_timezone
- Voorbeeld: "08:00" in Amsterdam timezone
Recipients (ontvangers):
- ✅ Per rapport: comma-separated email adressen
- Format:
user1@example.com, user2@example.com - GEEN per-customer emails (te complex voor MVP)
Delivery Failures:
- ✅ Max 3 retry attempts (met exponential backoff?)
- ✅ Log in audit log (event_type:
report_delivery_failed) - ✅ Error indicator bij scheduled report in UI
- ✅ Admin kan failed reports zien en manually retry
Generated Reports Opslag:
- ✅ Instelling in Settings: bewaren ja/nee + aantal dagen
- ✅ Rapport kan opnieuw gegenereerd worden (on-demand)
- ❌ Oude versies worden NIET bewaard (altijd verse data bij regenerate)
Email Body Template
Live Preview:
- ✅ Ja - toon live preview met sample data in UI
- Shows: sample customer name, period, statistics
- Updates in real-time as user types
Invalid Placeholders:
- ✅ Validation error bij opslaan (niet toegestaan)
- Toegestane placeholders:
{customer_name},{period},{total_jobs},{success_count},{failed_count},{success_rate},{company_name} - Foutmelding: "Invalid placeholder: {wrong_name}. Allowed: ..."
HTML Support:
- ✅ HTML toegestaan in email body template
- Reden: Professionele weergave met logo/opmaak → klanten vertrouwen legitimiteit
- Veiligheid: XSS-safe (sanitize user input)
- Toegestane tags:
<strong>,<em>,<u>,<br>,<p>,<ul>,<ol>,<li>,<a>,<img>(voor logo) - Voorbeeld default template:
<div style="font-family: Arial, sans-serif;"> <img src="{logo_base64}" alt="{company_name}" style="max-width: 200px; margin-bottom: 20px;" /> <p>Dear <strong>{customer_name}</strong>,</p> <p>Attached is your backup report for <strong>{period}</strong>.</p> <ul> <li>Total jobs: {total_jobs}</li> <li>Successful: {success_count}</li> <li>Failed: {failed_count}</li> <li>Success rate: {success_rate}%</li> </ul> <p>Best regards,<br/>{company_name}</p> </div>
Default Template:
- ✅ Ook configureerbaar in Settings > Reporting
- Field:
reporting_default_email_template(Text) - Used when report's
email_body_templateis NULL/empty - Includes placeholders + HTML
Logo & Branding
Logo in Email:
- ✅ Inline/embedded (base64 in HTML)
- Reden: Backupreports app is beveiligd, alleen toegankelijk vanaf bepaalde locaties
- Method:
<img src="data:image/png;base64,{base64_data}" /> - Placeholder in template:
{logo_base64}(automatically filled)
Multiple Logos:
- ❌ Nee - 1 logo voor alle reports (simpel, voldoende)
- Configured in Settings > Reporting
Dark Mode:
- ❌ Nee - standaard styling, geen dark mode support
- Reden: Reports als PDF attachment (PDF heeft geen dark mode)
Graph API Permissions
Mail.Send Permission:
- ⚠️ Nog niet toegekend - moet toegevoegd worden in Azure Portal
- Validatie bij opslaan:
- Optie 1: Check Graph API permissions via API call
- Optie 2: Test email field in Settings (admin vult eigen email in, send test)
- Keuze: Beide - check permissions + test email button
Mailbox Verschillend:
- ✅ Ja -
reporting_from_emailKAN anders zijn dangraph_mailbox - Reden: Klanten mogen NIET naar import mailbox mailen (wordt niet meer uitgelezen)
- Voorbeeld:
- Import mailbox:
backups@company.com(voor import) - Reporting mailbox:
reports@company.com(voor sending)
- Import mailbox:
Impersonation:
- ❌ Nee - altijd vanaf 1 centraal reporting mailadres
- ✅ Reply-to adres kan ingevuld worden
- Voorbeeld:
- From:
reports@company.com - Reply-To:
helpdesk@company.com
- From:
- Klanten replyen naar helpdesk, niet naar reports mailbox
🚀 Volgende Stappen
✅ Planning Compleet!
Alle vragen zijn beantwoord. Het TODO bevat nu:
- ✅ 15 relative period options met timezone support
- ✅ Settings > Reporting sectie (11 velden)
- ✅ Email template systeem met HTML + placeholders
- ✅ Logo upload en branding
- ✅ Graph API email sending (geen SMTP)
- ✅ Scheduling specificaties (voor Phase 3)
- ✅ 60+ test cases
Implementatie Start
Aanbevolen volgorde:
-
Start Phase 1: Settings > Reporting
- SystemSettings model + 11 velden
- Migratie functie
- Settings UI met nieuwe Reporting tab
- Logo upload handler
- Test email functie + Graph permission check
- Default email template editor
-
Dan Phase 2: Relative Periods
- Report model + 14 velden (periods + scheduling)
- Period calculator met timezone
- UI updates (period selector + email template)
- Template validation + sanitization
-
Later Phase 3: Scheduling (toekomstig)
- Scheduler daemon/cron
- Email sender via Graph API
- Retry logic + error handling
- Reports opslag
Klaar om te beginnen? We kunnen starten met Phase 1!
📝 Belangrijke Ontwerpbeslissingen
Technisch
- SPF/DNS: Bewust geen SMTP - Graph API voorkomt SPF lookup limiet (max 10 al bereikt)
- Backwards compatibility: Bestaande reports blijven werken (period_type="custom")
- Logo storage: Database blob (niet filesystem) voor eenvoudige backup/restore
- HTML sanitization: Gebruik bleach library voor XSS protection
- Logo embedding: Base64 inline in email (app is beveiligd, geen public URLs nodig)
User Experience
- Timezone: Volgt ui_timezone setting overal (consistent met rest van app)
- "To date" periodes: Eindigen altijd op gisteren 23:59 (volledige dagen, geen partial data)
- Email legitimiteit: HTML templates met logo → professioneel → klanten vertrouwen
- Reply-to scheiding: Reports vanaf reports@, replies naar helpdesk@ (mailbox scheiding)
- Validation: Strict placeholder validation (voorkomen errors bij sending)
Scheduling
- Frequenties: Weekly/Monthly/Quarterly/Yearly (geen daily - overkill)
- Retry logic: Max 3 attempts voor failed deliveries
- Error visibility: Admin ziet failed reports in UI + audit log
- Recipients: Per rapport (comma-separated) - geen customer-level emails in MVP
Scope Limitaties (MVP)
- ❌ Geen multiple logos per klant (1 logo voor alle reports)
- ❌ Geen dark mode email support (PDF attachment toch)
- ❌ Geen custom cron expressions (te complex)
- ❌ Geen per-customer email addresses (te complex)
- ❌ Geen report history storage (kan wel opnieuw genereren)
🎯 Success Criteria
Phase 1 Success
- Admin kan Settings > Reporting configureren
- Logo upload werkt (PNG/JPG/SVG, max 500KB)
- Test email verstuurd met alle settings toegepast
- Graph API Mail.Send permission check werkt
- Default email template configureerbaar met live preview
Phase 2 Success
- Admin kan relative period kiezen bij report maken
- 15 relative period options beschikbaar
- Period calculator werkt correct (timezone-aware, to yesterday)
- Reports lijst toont relative period badges
- Email body template per rapport met HTML + placeholders
- Template validation werkt (invalid placeholders rejected)
Phase 3 Success (Toekomstig)
- Reports kunnen gescheduled worden (weekly/monthly/quarterly/yearly)
- Scheduled reports versturen automatisch
- Failed deliveries hebben retry (max 3x)
- Admin ziet schedule status in UI
- Audit log bevat delivery events