Skip to main content

cortex/frontend/
render.rs

1//! Report-rendering layer: the thin presentation proxy over [`crate::backend::task_report`] that
2//! the HTML report screens use.
3//!
4//! The category/`what` aggregate grains are served by the `report_summary` rollup (an indexed
5//! lookup, kept fresh on the run-completion path), so the former Redis **cache** that shielded the
6//! expensive live aggregation is gone — Redis is no longer a hard dependency of the frontend. This
7//! module (formerly `frontend::cached`, renamed once the cache was removed) now only translates
8//! request params into a [`TaskReportOptions`], delegates, and records the pagination/`report_time`
9//! globals the report templates expect.
10use crate::backend::{TaskReportOptions, task_report as backend_task_report};
11use crate::frontend::params::ReportParams;
12use crate::models::{Corpus, Service};
13use diesel::PgConnection;
14use std::collections::HashMap;
15
16/// Renders a task report, filling in the pagination + provenance globals the templates consume.
17/// Reads over the caller-supplied (pooled) `connection` — no per-request fresh
18/// `Backend::default()`.
19#[allow(clippy::too_many_arguments)]
20pub fn task_report(
21  connection: &mut PgConnection,
22  global: &mut HashMap<String, String>,
23  corpus: &Corpus,
24  service: &Service,
25  severity: Option<String>,
26  category: Option<String>,
27  what: Option<String>,
28  params: &Option<ReportParams>,
29) -> Vec<HashMap<String, String>> {
30  let all_messages = params.as_ref().and_then(|p| p.all).unwrap_or(false);
31  // Clamp the paginate params: an unbounded `page_size` would `LIMIT` a whole entry list into one
32  // render, and a deep `offset` is a multi-second scan-and-discard — both reachable from the public
33  // report screens via the query string. One source of truth in `params` (P-4; also bounds the
34  // agent report endpoints).
35  let offset = params
36    .as_ref()
37    .and_then(|p| p.offset)
38    .unwrap_or(0)
39    .clamp(0, crate::frontend::params::MAX_REPORT_OFFSET);
40  let page_size = params
41    .as_ref()
42    .and_then(|p| p.page_size)
43    .unwrap_or(100)
44    .clamp(1, crate::frontend::params::MAX_REPORT_PAGE_SIZE);
45
46  let fetched_report = backend_task_report(
47    connection,
48    TaskReportOptions {
49      corpus,
50      service,
51      severity_opt: severity,
52      category_opt: category,
53      what_opt: what,
54      all_messages,
55      offset,
56      page_size,
57    },
58  );
59
60  // Pagination + provenance globals for the templates.
61  let from_offset = offset;
62  let to_offset = offset + page_size;
63  global.insert("from_offset".to_string(), from_offset.to_string());
64  if from_offset >= page_size {
65    global.insert("offset_min_false".to_string(), "true".to_string());
66    global.insert(
67      "prev_offset".to_string(),
68      (from_offset - page_size).to_string(),
69    );
70  }
71  // A full page of *data* rows implies there may be another page. Aggregate reports append summary
72  // rows (`total`, `no_messages`) that are not part of the paged set, so exclude them from the
73  // count — otherwise the "next" control would over-signal.
74  let data_rows = fetched_report
75    .iter()
76    .filter(|row| {
77      let name = row.get("name").map(String::as_str).unwrap_or("");
78      name != "total" && name != "no_messages"
79    })
80    .count();
81  if data_rows >= page_size as usize {
82    global.insert("offset_max_false".to_string(), "true".to_string());
83  }
84  global.insert(
85    "next_offset".to_string(),
86    (from_offset + page_size).to_string(),
87  );
88  global.insert("offset".to_string(), offset.to_string());
89  global.insert("page_size".to_string(), page_size.to_string());
90  global.insert("to_offset".to_string(), to_offset.to_string());
91  global.insert(
92    "report_time".to_string(),
93    crate::frontend::helpers::report_timestamp(),
94  );
95
96  fetched_report
97}