diff --git a/api/src/email_templates/change_tracker_email.html.jinja2 b/api/src/email_templates/change_tracker_email.html.jinja2 new file mode 100644 index 000000000..607ed8322 --- /dev/null +++ b/api/src/email_templates/change_tracker_email.html.jinja2 @@ -0,0 +1,970 @@ + +{#- + GTFS Diff Email Notification — Jinja2 Template + ═══════════════════════════════════════════════ + + REQUIRED CONTEXT VARIABLES + ─────────────────────────── + notification_date : str – Formatted date shown in the email (e.g. "June 5, 2026") + subscriptions_url : str – URL to the user's subscriptions management page + feeds : list[dict] – One or more feed diffs; schema per item below + + PER-FEED DICT (maps to GTFS Diff v2 schema — gtfs-diff-schema.json) + ───────────────────────────────────────────────────────────────────── + feed_id str Feed ID used for anchor links (e.g. "mdb-2126") + feed_name str Human-readable agency/feed name + feed_url str URL to the full diff report page + unsubscribe_url str Feed-specific unsubscribe URL + notification_date str Per-feed update date, formatted (e.g. "June 5, 2026") + base_feed_version str Base feed identifier (metadata.base_feed.source) + new_feed_version str New feed identifier (metadata.new_feed.source) + total_changes int summary.total_changes + files_modified_count int summary.files_modified_count + + breaking_changes list Analytical alerts: breaking changes (omit or [] if none) + .type str Alert type label (e.g. "REMOVED ENTITY") + .file str Affected GTFS file (e.g. "trips.txt") + .description str Human-readable description + + suspicious_changes list Analytical alerts: suspicious changes (omit or [] if none) + .type str Alert type label (e.g. "LARGE ENTITY COUNT DELTA") + .file str Affected GTFS file + .description str Human-readable description + + diff_files list Per-file row-change stats (from file_diffs[*].stats) + .file_name str GTFS file name (e.g. "calendar.txt") + .rows_added_count int|None stats.rows_added_count – None renders as "—" + .rows_deleted_count int|None stats.rows_deleted_count + .rows_modified_count int|None stats.rows_modified_count + + features_changed list GTFS feature-level changes (omit or [] if none) + .name str Feature name (e.g. "Stops Wheelchair Accessibility") + .change str "Added" | "Removed" | "Changed" + + validation_changes list Validation report diff entries (omit or [] if none) + .change_type str "New" | "Fixed" | "Occurrences" + .notice_code str Validator notice code + .severity str "WARNING" | "ERROR" | "INFO" + .older_count int|None Count in base report; None for "New" entries + .latest_count int|None Count in new report; None for "Fixed" entries + + REQUIRED CUSTOM FILTER + ─────────────────────── + thousands_sep(n) – Formats an integer with thousands separator; + returns "—" for None/null. + Python example: + def thousands_sep(n): + return "{:,}".format(int(n)) if n is not None else "—" + env.filters["thousands_sep"] = thousands_sep +-#} +{#- ─── Macros ──────────────────────────────────────────────────────────────── #} + +{%- macro severity_badge(severity) -%} + {%- if severity == "WARNING" -%} + WARNING + {%- elif severity == "ERROR" -%} + ERROR + {%- elif severity == "INFO" -%} + INFO + {%- else -%} + {{ severity }} + {%- endif -%} +{%- endmacro -%} + +{%- macro num(value) -%} + {%- if value is not none and value is defined -%}{{ value | thousands_sep }}{%- else -%}—{%- endif -%} +{%- endmacro -%} + +{#- Validation row: "New" and worsening "Occurrences" are red; "Fixed" and improving "Occurrences" are green #} +{%- macro validation_row_is_bad(change) -%} + {%- if change.change_type == "New" -%}true + {%- elif change.change_type == "Fixed" -%}false + {%- elif change.change_type == "Occurrences" and change.latest_count is not none and change.older_count is not none and change.latest_count > change.older_count -%}true + {%- else -%}false + {%- endif -%} +{%- endmacro -%} + +{%- macro feature_change_color(change) -%} + {%- if change == "Added" -%}#2e7d32 + {%- elif change == "Removed" -%}#d32f2f + {%- else -%}#ed6c02 + {%- endif -%} +{%- endmacro -%} + +{%- macro feature_row_bg(change) -%} + {%- if change == "Added" -%}#eef6ef + {%- elif change == "Removed" -%}#fdeeee + {%- else -%}#fff3e0 + {%- endif -%} +{%- endmacro -%} + +{#- ─── Document ───────────────────────────────────────────────────────────── #} + + + + + + + + + {%- if feeds | length == 1 -%} + GTFS Feed Update — {{ feeds[0].feed_name }} + {%- else -%} + GTFS Feed Updates — {{ feeds | length }} feeds + {%- endif -%} + + + + + + +
+ {%- if feeds | length == 1 -%} + {%- set f = feeds[0] -%} + {{ f.feed_name }} GTFS feed updated — {{ f.total_changes | thousands_sep }} changes across {{ f.files_modified_count }} file{{ "s" if f.files_modified_count != 1 }}{% if f.breaking_changes %}, {{ f.breaking_changes | length }} breaking change{{ "s" if f.breaking_changes | length != 1 }}{% endif %}{% if f.suspicious_changes %} and {{ f.suspicious_changes | length }} suspicious change{{ "s" if f.suspicious_changes | length != 1 }}{% endif %} detected. + {%- else -%} + {{ feeds | length }} GTFS feed updates — {% for f in feeds %}{{ f.feed_name }}{% if not loop.last %}, {% endif %}{% endfor %}. + {%- endif -%} +
+ + + + + +
+ + + + + + + + {# ============ TABLE OF CONTENTS (multiple feeds only) ============ #} + {% if feeds | length > 1 %} + + + + {% endif %} + + {# ============ FEED SECTIONS (loop) ============ #} + {% for feed in feeds %} + + {# Inter-feed divider (not before the first feed, and not when TOC provides separation) #} + {% if not loop.first %} + + + + {% endif %} + + {# Anchor target for TOC links #} + + + + + + + + + + {# ============ BREAKING CHANGES (conditional) ============ #} + {% if feed.breaking_changes %} + + + + {% endif %} + + {# ============ SUSPICIOUS CHANGES (conditional) ============ #} + {% if feed.suspicious_changes %} + + + + {% endif %} + + + + + + + {# ============ FEATURES CHANGED (conditional) ============ #} + {% if feed.features_changed %} + + + + {% endif %} + + {# ============ VALIDATION REPORT DIFF (conditional) ============ #} + {% if feed.validation_changes %} + + + + {% endif %} + + + + + + + {% endfor %}{# end feeds loop #} + + + + + + + + + + + + +
+ +