{# FI-3 / FI-6: feedback-timeline fragment — lazy-loaded into #insights-timeline. Context: rows — list of {period, , count} dicts (FI-1/FI-5's StateDB.feedback_timeline output), or [] for the empty path. is "signal_type" when dimension="signal" else "source". granularity — active bucket size ("week" | "day"); drives the toggle. dimension — active group-by ("signal" | "source"); drives the second toggle and the category column header/value (FI-6). weeks — the bounded lookback window (int) surfaced in a .field-note. error — True when the read raised (generic notice, no detail leaked). Presentation (CU-2): a STACKED Chart.js bar — labels are the sorted distinct periods, one dataset per category (signal_type or source) carrying per-period counts, stacked so each bar shows the per-period total broken down by category. The chart is built client-side by site.js from the JSON data island below (palette colors from site.css tokens). The per-(period, category) breakdown table beneath the chart is the exact-values + graceful-degradation fallback (kept when the Chart.js CDN is blocked). All content via Jinja autoescape and |tojson only — never |safe. Both toggles preserve each other's state: every granularity link carries the current dimension and every dimension link carries the current granularity, so switching one axis never silently resets the other. #} {% set _cat_header = "Source" if dimension == "source" else "Signal" %}

Feedback timeline

{# Granularity toggle: each link re-requests this fragment at a different bucket size, preserving the current dimension, and swaps itself back into #insights-timeline. #}

{% for g in ["week", "day"] %} {{ g }} {% endfor %}

{# Dimension toggle (FI-6): group the per-period breakdown by signal or source, preserving the current granularity. #}

{% for d in ["signal", "source"] %} by {{ d }} {% endfor %}

{% if error %}

Could not load the feedback timeline — check the server logs.

{% elif rows %} {# Aggregate the flat (period, , count) rows into per-period totals for the chart, and collect the distinct categories for the table columns. The category value is row.signal_type when dimension="signal" else row.source (FI-6) — read via the dimension-matched key the backend emits; the table cells below use .get on (period, category) so a missing pair never raises. Jinja namespaces are used so loop-local mutations persist. #} {% set _periods = [] %} {% set _totals = {} %} {% set _by_cell = {} %} {% set _cats = [] %} {% for r in rows %} {% set _cat = r.signal_type if dimension == "signal" else r.source %} {% if r.period not in _periods %}{% set _ = _periods.append(r.period) %}{% endif %} {% if _cat not in _cats %}{% set _ = _cats.append(_cat) %}{% endif %} {% set _prev = _totals.get(r.period, 0) + (r.count | int) %} {% set _ = _totals.update({r.period: _prev}) %} {% set _ = _by_cell.update({(r.period, _cat): (r.count | int)}) %} {% endfor %} {# CU-2: build a STACKED-bar payload — one dataset per category, each carrying per-period counts (0 where a (period, category) pair is absent). The JSON island feeds site.js, which builds a stacked Chart.js bar; the per-(period, category) breakdown table below remains the exact-values + degradation fallback. #} {% set _datasets = [] %} {% for c in _cats %} {% set _series = [] %} {% for p in _periods %} {% set _ = _series.append(_by_cell.get((p, c), 0)) %} {% endfor %} {% set _ = _datasets.append({"label": c, "data": _series}) %} {% endfor %}

Feedback events per {{ granularity }} over the last {{ weeks }} weeks (bars are stacked by {{ "source" if dimension == "source" else "signal type" }}; each segment is one {{ "source" if dimension == "source" else "signal type" }}'s share of that {{ granularity }}'s total).

{% for c in _cats %}{% endfor %} {% for p in _periods %} {% for c in _cats %} {% set _cell = _by_cell.get((p, c)) %} {% endfor %} {% endfor %}
Counts by {{ _cat_header | lower }} per {{ granularity }}.
Period{{ c }}Total
{{ p }}{% if _cell %}{{ _cell }}{% else %}—{% endif %}{{ _totals.get(p, 0) }}
{% else %}

No feedback events yet — give feedback (save / dismiss / rate) on discoveries to populate the timeline. Showing the last {{ weeks }} weeks.

{% endif %}