diff --git a/lib/towerops/preseem/insights.ex b/lib/towerops/preseem/insights.ex
index 782f3d78..00ec1468 100644
--- a/lib/towerops/preseem/insights.ex
+++ b/lib/towerops/preseem/insights.ex
@@ -29,7 +29,16 @@ defmodule Towerops.Preseem.Insights do
query = if type, do: where(query, type: ^type), else: query
query = if urgency, do: where(query, urgency: ^urgency), else: query
- query = if source, do: where(query, source: ^source), else: query
+
+ # `source: "ai"` is a virtual filter — surfaces every insight that has
+ # been LLM-enriched, regardless of which underlying integration produced
+ # it. Real source values (preseem/snmp/gaiia/system) match the column.
+ query =
+ case source do
+ nil -> query
+ "ai" -> where(query, [i], not is_nil(i.llm_enriched_at))
+ s -> where(query, source: ^s)
+ end
query
|> preload([:preseem_access_point, :device])
diff --git a/lib/towerops_web/live/insights_live/index.html.heex b/lib/towerops_web/live/insights_live/index.html.heex
index 56330be7..0292cef9 100644
--- a/lib/towerops_web/live/insights_live/index.html.heex
+++ b/lib/towerops_web/live/insights_live/index.html.heex
@@ -69,7 +69,7 @@
{t("All")}
<.link
- :for={source <- ~w(preseem gaiia snmp system)}
+ :for={source <- ~w(preseem gaiia snmp system ai)}
id={"filter-source-#{source}"}
patch={
~p"/insights?#{build_filter_params(%{status: @filter_status, urgency: @filter_urgency}, %{source: source})}"
@@ -258,6 +258,13 @@
{insight.source}
+ <%!-- AI badge — present on any LLM-enriched insight --%>
+ <%= if insight.llm_enriched_at do %>
+
+ AI
+
+ <% end %>
+
<%!-- Type badge --%>
{String.replace(insight.type, "_", " ")}
diff --git a/test/towerops/preseem/insights_test.exs b/test/towerops/preseem/insights_test.exs
index 3f9f6577..1ac6d8e0 100644
--- a/test/towerops/preseem/insights_test.exs
+++ b/test/towerops/preseem/insights_test.exs
@@ -354,6 +354,55 @@ defmodule Towerops.Preseem.InsightsTest do
assert hd(snmp_insights).title == "SNMP insight"
end
+ test "source: \"ai\" returns LLM-enriched insights regardless of underlying source",
+ %{org: org, access_point: ap} do
+ # Two unenriched insights from two different sources
+ {:ok, _preseem_unenriched} =
+ insert_insight(org, ap, %{
+ type: "qoe_degradation",
+ urgency: "warning",
+ title: "Preseem (no LLM)"
+ })
+
+ {:ok, _snmp_unenriched} =
+ insert_insight_with_source(org, %{
+ type: "snmp_cpu_high",
+ urgency: "warning",
+ title: "SNMP (no LLM)",
+ source: "snmp"
+ })
+
+ # Two enriched insights from two different sources — these should match.
+ {:ok, _preseem_enriched} =
+ insert_insight(org, ap, %{
+ type: "qoe_degradation",
+ urgency: "warning",
+ title: "Preseem (enriched)",
+ llm_summary: "summary",
+ llm_model: "deepseek-v4-pro",
+ llm_enriched_at: Towerops.Time.now()
+ })
+
+ {:ok, _snmp_enriched} =
+ insert_insight_with_source(org, %{
+ type: "snmp_cpu_high",
+ urgency: "warning",
+ title: "SNMP (enriched)",
+ source: "snmp",
+ llm_summary: "summary",
+ llm_model: "deepseek-v4-pro",
+ llm_enriched_at: Towerops.Time.now()
+ })
+
+ ai_insights = Insights.list_insights(org.id, source: "ai")
+ titles = Enum.map(ai_insights, & &1.title)
+
+ assert "Preseem (enriched)" in titles
+ assert "SNMP (enriched)" in titles
+ refute "Preseem (no LLM)" in titles
+ refute "SNMP (no LLM)" in titles
+ end
+
test "returns all sources when no source filter", %{org: org, access_point: ap} do
insert_insight(org, ap, %{type: "qoe_degradation", urgency: "warning", title: "Preseem"})