1use rocket::http::Status;
17use rocket::response::Redirect;
18use rocket::serde::json::Json;
19use rocket::{Route, State};
20use rocket_dyn_templates::Template;
21use serde::Serialize;
22
23use crate::backend::{
24 DatabaseUrl, DbPool, MessageCounts, ReportSummaryRow, RerunOptions, TaskReportOptions,
25 category_rollup, category_total, from_address, progress_report, severity_total, task_messages,
26 task_report, what_rollup,
27};
28use crate::frontend::actor::{Actor, AdminReject, AdminSession, require_admin};
29use crate::frontend::concerns::{LiveReportLimiter, serve_report};
30use crate::frontend::params::{MAX_REPORT_OFFSET, MAX_REPORT_PAGE_SIZE, ReportParams};
31use crate::helpers::TaskStatus;
32use crate::jobs;
33use crate::models::{Corpus, Service, Task};
34
35#[derive(Debug, Serialize, schemars::JsonSchema)]
38pub struct StatusCountDto {
39 pub status: String,
42 pub tasks: i64,
44 pub percent: f64,
46}
47
48#[derive(Debug, Serialize, schemars::JsonSchema)]
53pub struct ServiceOverviewDto {
54 pub corpus: String,
56 pub service: String,
58 pub total: i64,
60 pub statuses: Vec<StatusCountDto>,
62}
63
64#[derive(Debug, Serialize, schemars::JsonSchema)]
67pub struct ReportRowDto {
68 pub name: String,
70 pub tasks: i64,
72 pub messages: i64,
74}
75
76#[derive(Debug, Serialize, schemars::JsonSchema)]
79pub struct CategoryReportDto {
80 pub severity: String,
82 pub total_tasks: i64,
84 pub total_messages: i64,
86 pub categories: Vec<ReportRowDto>,
88}
89
90#[derive(Debug, Serialize, schemars::JsonSchema)]
93pub struct WhatReportDto {
94 pub severity: String,
96 pub category: String,
98 pub total_tasks: i64,
100 pub total_messages: i64,
102 pub whats: Vec<ReportRowDto>,
104}
105
106#[derive(Debug, Serialize, schemars::JsonSchema)]
109pub struct MessageDto {
110 pub severity: String,
112 pub category: String,
114 pub what: String,
116 pub details: String,
118}
119
120#[derive(Debug, Serialize, schemars::JsonSchema)]
123pub struct MessageCountsDto {
124 pub info: i64,
126 pub warning: i64,
128 pub error: i64,
130 pub fatal: i64,
132 pub invalid: i64,
134 pub total: i64,
136}
137impl From<MessageCounts> for MessageCountsDto {
138 fn from(counts: MessageCounts) -> Self {
139 MessageCountsDto {
140 info: counts.info,
141 warning: counts.warning,
142 error: counts.error,
143 fatal: counts.fatal,
144 invalid: counts.invalid,
145 total: counts.total(),
146 }
147 }
148}
149
150#[derive(Debug, Serialize, schemars::JsonSchema)]
153pub struct DocumentReportDto {
154 pub corpus: String,
156 pub service: String,
158 pub name: String,
160 pub entry: String,
162 pub task_id: i64,
164 pub status: String,
166 pub status_code: i32,
168 pub messages: Vec<MessageDto>,
173 pub message_counts: MessageCountsDto,
175 pub messages_truncated: bool,
177 pub result_url: String,
179 pub preview_url: String,
181}
182
183fn is_rollup_severity(severity: &str) -> bool {
186 matches!(severity, "warning" | "error" | "fatal" | "invalid" | "info")
187}
188
189pub fn is_valid_rerun_severity(severity: &str, has_category: bool) -> bool {
198 if has_category {
199 matches!(severity, "warning" | "error" | "fatal" | "invalid" | "info")
200 } else {
201 matches!(
202 severity,
203 "no_problem" | "warning" | "error" | "fatal" | "invalid"
204 )
205 }
206}
207
208fn resolve(
210 corpus: &str,
211 service: &str,
212 connection: &mut diesel::PgConnection,
213) -> Result<(Corpus, Service), Status> {
214 let corpus = Corpus::find_by_name(corpus, connection).map_err(|_| Status::NotFound)?;
215 let service = Service::find_by_name(service, connection).map_err(|_| Status::NotFound)?;
216 Ok((corpus, service))
217}
218
219fn totals(row: Option<ReportSummaryRow>) -> (i64, i64) {
221 row.map_or((0, 0), |total| (total.task_count, total.message_count))
222}
223
224#[rocket_okapi::openapi(tag = "Reports")]
229#[get("/api/reports/<corpus>/<service>")]
230pub fn api_service_overview(
231 corpus: &str,
232 service: &str,
233 pool: &State<DbPool>,
234) -> Result<Json<ServiceOverviewDto>, Status> {
235 let mut connection = pool.get().map_err(|_| Status::ServiceUnavailable)?;
236 let (corpus, service) = resolve(corpus, service, &mut connection)?;
237 let stats = progress_report(&mut connection, corpus.id, service.id);
238 let statuses = TaskStatus::keys()
239 .into_iter()
240 .map(|status| {
241 let percent = stats
242 .get(&format!("{status}_percent"))
243 .copied()
244 .unwrap_or(0.0);
245 let tasks = stats.get(&status).copied().unwrap_or(0.0) as i64;
246 StatusCountDto {
247 status,
248 tasks,
249 percent,
250 }
251 })
252 .collect();
253 Ok(Json(ServiceOverviewDto {
254 corpus: corpus.name,
255 service: service.name,
256 total: stats.get("total").copied().unwrap_or(0.0) as i64,
257 statuses,
258 }))
259}
260
261#[rocket_okapi::openapi(tag = "Reports")]
264#[get("/api/reports/<corpus>/<service>/<severity>?<offset>&<page_size>")]
265pub fn api_category_report(
266 corpus: &str,
267 service: &str,
268 severity: &str,
269 offset: Option<i64>,
270 page_size: Option<i64>,
271 pool: &State<DbPool>,
272) -> Result<Json<CategoryReportDto>, Status> {
273 if !is_rollup_severity(severity) {
274 return Err(Status::BadRequest);
275 }
276 let mut connection = pool.get().map_err(|_| Status::ServiceUnavailable)?;
277 let (corpus, service) = resolve(corpus, service, &mut connection)?;
278 let limit = page_size.unwrap_or(100).clamp(1, MAX_REPORT_PAGE_SIZE);
279 let offset = offset.unwrap_or(0).clamp(0, MAX_REPORT_OFFSET);
280 let categories = category_rollup(
281 &mut connection,
282 corpus.id,
283 service.id,
284 severity,
285 limit,
286 offset,
287 )
288 .unwrap_or_default()
289 .into_iter()
290 .map(|row| ReportRowDto {
291 name: row.category,
292 tasks: row.task_count,
293 messages: row.message_count,
294 })
295 .collect();
296 let (total_tasks, total_messages) =
297 totals(severity_total(&mut connection, corpus.id, service.id, severity).unwrap_or_default());
298 Ok(Json(CategoryReportDto {
299 severity: severity.to_string(),
300 total_tasks,
301 total_messages,
302 categories,
303 }))
304}
305
306#[rocket_okapi::openapi(tag = "Reports")]
310#[get("/api/reports/<corpus>/<service>/<severity>/<category>?<offset>&<page_size>")]
311pub fn api_what_report(
312 corpus: &str,
313 service: &str,
314 severity: &str,
315 category: &str,
316 offset: Option<i64>,
317 page_size: Option<i64>,
318 pool: &State<DbPool>,
319) -> Result<Json<WhatReportDto>, Status> {
320 if !is_rollup_severity(severity) {
321 return Err(Status::BadRequest);
322 }
323 let mut connection = pool.get().map_err(|_| Status::ServiceUnavailable)?;
324 let (corpus, service) = resolve(corpus, service, &mut connection)?;
325 let limit = page_size.unwrap_or(100).clamp(1, MAX_REPORT_PAGE_SIZE);
326 let offset = offset.unwrap_or(0).clamp(0, MAX_REPORT_OFFSET);
327 let whats = what_rollup(
328 &mut connection,
329 corpus.id,
330 service.id,
331 severity,
332 category,
333 limit,
334 offset,
335 )
336 .unwrap_or_default()
337 .into_iter()
338 .map(|row| ReportRowDto {
339 name: row.what.unwrap_or_default(),
340 tasks: row.task_count,
341 messages: row.message_count,
342 })
343 .collect();
344 let (total_tasks, total_messages) = totals(
345 category_total(&mut connection, corpus.id, service.id, severity, category).unwrap_or_default(),
346 );
347 Ok(Json(WhatReportDto {
348 severity: severity.to_string(),
349 category: category.to_string(),
350 total_tasks,
351 total_messages,
352 whats,
353 }))
354}
355
356#[derive(Debug, Serialize, schemars::JsonSchema)]
359pub struct EntryRowDto {
360 pub name: String,
363 pub task_id: i64,
365 pub details: String,
367}
368
369#[derive(Debug, Serialize, schemars::JsonSchema)]
375pub struct EntryListDto {
376 pub corpus: String,
378 pub service: String,
380 pub severity: String,
382 pub category: String,
384 pub what: String,
386 pub offset: i64,
388 pub page_size: i64,
390 pub entries: Vec<EntryRowDto>,
392}
393
394#[rocket_okapi::openapi(tag = "Reports")]
399#[get("/api/reports/<corpus>/<service>/<severity>/<category>/<what>?<offset>&<page_size>")]
400#[allow(clippy::too_many_arguments)]
401pub fn api_entry_list(
402 corpus: &str,
403 service: &str,
404 severity: &str,
405 category: &str,
406 what: &str,
407 offset: Option<i64>,
408 page_size: Option<i64>,
409 pool: &State<DbPool>,
410) -> Result<Json<EntryListDto>, Status> {
411 if !is_rollup_severity(severity) {
412 return Err(Status::BadRequest);
413 }
414 let offset = offset.unwrap_or(0).max(0);
415 if offset > MAX_REPORT_OFFSET {
418 return Err(Status::BadRequest);
419 }
420 let mut connection = pool.get().map_err(|_| Status::ServiceUnavailable)?;
421 let (corpus_record, service_record) = resolve(corpus, service, &mut connection)?;
422 let page_size = page_size.unwrap_or(100).clamp(1, MAX_REPORT_PAGE_SIZE);
424 let rows = task_report(
427 &mut connection,
428 TaskReportOptions {
429 corpus: &corpus_record,
430 service: &service_record,
431 severity_opt: Some(severity.to_string()),
432 category_opt: Some(category.to_string()),
433 what_opt: Some(what.to_string()),
434 all_messages: false,
435 offset,
436 page_size,
437 },
438 );
439 let entries = rows
440 .iter()
441 .map(|row| EntryRowDto {
442 name: row.get("entry_name").cloned().unwrap_or_default(),
443 task_id: row
444 .get("entry_taskid")
445 .and_then(|id| id.parse().ok())
446 .unwrap_or(0),
447 details: row.get("details").cloned().unwrap_or_default(),
448 })
449 .collect();
450 Ok(Json(EntryListDto {
451 corpus: corpus_record.name,
452 service: service_record.name,
453 severity: severity.to_string(),
454 category: category.to_string(),
455 what: what.to_string(),
456 offset,
457 page_size,
458 entries,
459 }))
460}
461
462fn document_report(
466 corpus: &str,
467 service: &str,
468 name: &str,
469 connection: &mut diesel::PgConnection,
470) -> Result<DocumentReportDto, Status> {
471 let (corpus, service) = resolve(corpus, service, connection)?;
472 let task =
473 Task::find_by_name(name, &corpus, &service, connection).map_err(|_| Status::NotFound)?;
474 let status = TaskStatus::from_raw(task.status);
475 let (records, counts) = task_messages(connection, &task);
476 let messages: Vec<MessageDto> = records
477 .iter()
478 .map(|message| MessageDto {
479 severity: message.severity().to_string(),
480 category: message.category().to_string(),
481 what: message.what().to_string(),
482 details: message.details().to_string(),
483 })
484 .collect();
485 let messages_truncated = (messages.len() as i64) < counts.total();
486 Ok(DocumentReportDto {
487 corpus: corpus.name.clone(),
488 service: service.name.clone(),
489 name: name.to_string(),
490 entry: task.entry.trim_end().to_string(),
491 task_id: task.id,
492 status: status.to_key(),
493 status_code: status.raw(),
494 messages,
495 message_counts: counts.into(),
496 messages_truncated,
497 result_url: format!("/entry/{}/{}", service.name, task.id),
498 preview_url: format!("/preview/{}/{}/{}", corpus.name, service.name, name),
499 })
500}
501
502#[rocket_okapi::openapi(tag = "Reports")]
507#[get("/api/corpus/<corpus>/<service>/document/<name>")]
508pub fn api_document(
509 corpus: &str,
510 service: &str,
511 name: &str,
512 pool: &State<DbPool>,
513) -> Result<Json<DocumentReportDto>, Status> {
514 let mut connection = pool.get().map_err(|_| Status::ServiceUnavailable)?;
515 Ok(Json(document_report(
516 corpus,
517 service,
518 name,
519 &mut connection,
520 )?))
521}
522
523#[get("/document/<corpus>/<service>/<name>")]
530pub fn document_report_page(
531 corpus: &str,
532 service: &str,
533 name: &str,
534 pool: &State<DbPool>,
535) -> Result<(Status, Template), Status> {
536 let mut connection = pool.get().map_err(|_| Status::ServiceUnavailable)?;
537 match document_report(corpus, service, name, &mut connection) {
538 Ok(report) => {
539 let global = serde_json::json!({
540 "title": format!("{} — {}/{}", report.name, report.corpus, report.service),
541 "description": "Per-article conversion forensics",
542 });
543 Ok((
544 Status::Ok,
545 Template::render(
546 "document-report",
547 rocket_dyn_templates::context! { global, report },
548 ),
549 ))
550 },
551 Err(status) if status == Status::NotFound => {
554 let message =
555 format!("No article “{name}” in {corpus} / {service}. Check the paper id and try again.");
556 let global = serde_json::json!({
557 "title": format!("404 · {message}"),
558 "description": message.clone(),
559 });
560 Ok((
561 Status::NotFound,
562 Template::render(
563 "error",
564 rocket_dyn_templates::context! {
565 global,
566 status: 404,
567 message,
568 back_url: format!("/corpus/{corpus}/{service}"),
569 back_label: format!("Back to the {service} report"),
570 },
571 ),
572 ))
573 },
574 Err(status) => Err(status),
575 }
576}
577
578#[get("/document/<corpus>/<service>?<name>")]
586pub fn document_lookup_redirect(
587 corpus: &str,
588 service: &str,
589 name: Option<&str>,
590) -> Result<Redirect, Status> {
591 let trimmed = name.unwrap_or("").trim();
592 if trimmed.is_empty() {
593 return Err(Status::BadRequest);
594 }
595 Ok(Redirect::to(uri!(document_report_page(
596 corpus = corpus,
597 service = service,
598 name = trimmed
599 ))))
600}
601
602#[derive(Debug, Serialize, schemars::JsonSchema)]
604pub struct RerunAckDto {
605 pub corpus: String,
607 pub service: String,
609 pub actor: String,
611 pub description: String,
613}
614
615#[rocket_okapi::openapi(tag = "Reports")]
621#[post("/api/reports/<corpus>/<service>/rerun?<severity>&<category>&<what>&<description>")]
622#[allow(clippy::too_many_arguments)]
623pub fn rerun_report(
624 corpus: &str,
625 service: &str,
626 severity: Option<&str>,
627 category: Option<&str>,
628 what: Option<&str>,
629 description: Option<&str>,
630 actor: Actor,
631 database_url: &State<DatabaseUrl>,
632 _pool: &State<DbPool>,
633) -> Result<(Status, Json<RerunAckDto>), Status> {
634 if let Some(severity) = severity
635 && !is_valid_rerun_severity(severity, category.is_some())
636 {
637 return Err(Status::BadRequest);
638 }
639 let description = description.unwrap_or("rerun via API").to_string();
640 let mut backend = from_address(&database_url.0);
643 let corpus_record =
644 Corpus::find_by_name(corpus, &mut backend.connection).map_err(|_| Status::NotFound)?;
645 let service_record =
646 Service::find_by_name(service, &mut backend.connection).map_err(|_| Status::NotFound)?;
647 backend
648 .mark_rerun(RerunOptions {
649 corpus: &corpus_record,
650 service: &service_record,
651 severity_opt: severity.map(str::to_string),
652 category_opt: category.map(str::to_string),
653 what_opt: what.map(str::to_string),
654 description_opt: Some(description.clone()),
655 owner_opt: Some(actor.owner.clone()),
656 })
657 .map_err(|_| Status::InternalServerError)?;
658 tracing::info!(actor = %actor.owner, corpus, service, severity = ?severity, category = ?category, what = ?what, "rerun via API");
659 Ok((
663 Status::Accepted,
664 Json(RerunAckDto {
665 corpus: corpus.to_string(),
666 service: service.to_string(),
667 actor: actor.owner,
668 description,
669 }),
670 ))
671}
672
673#[derive(Serialize, schemars::JsonSchema)]
676pub struct RunControlDto {
677 pub corpus: String,
679 pub service: String,
681 pub action: String,
683 pub affected: usize,
685 pub actor: String,
687}
688
689fn api_run_control(
692 corpus: &str,
693 service: &str,
694 owner: &str,
695 pause: bool,
696 pool: &State<DbPool>,
697) -> Result<Json<RunControlDto>, Status> {
698 let mut connection = pool.get().map_err(|_| Status::ServiceUnavailable)?;
699 let affected =
700 crate::frontend::concerns::serve_pause_resume(&mut connection, corpus, service, owner, pause)?;
701 Ok(Json(RunControlDto {
702 corpus: corpus.to_string(),
703 service: service.to_string(),
704 action: if pause { "pause" } else { "resume" }.to_string(),
705 affected,
706 actor: owner.to_string(),
707 }))
708}
709
710#[rocket_okapi::openapi(tag = "Reports")]
715#[post("/api/reports/<corpus>/<service>/pause")]
716pub fn pause_run_api(
717 corpus: &str,
718 service: &str,
719 actor: Actor,
720 pool: &State<DbPool>,
721) -> Result<Json<RunControlDto>, Status> {
722 api_run_control(corpus, service, &actor.owner, true, pool)
723}
724
725#[rocket_okapi::openapi(tag = "Reports")]
730#[post("/api/reports/<corpus>/<service>/resume")]
731pub fn resume_run_api(
732 corpus: &str,
733 service: &str,
734 actor: Actor,
735 pool: &State<DbPool>,
736) -> Result<Json<RunControlDto>, Status> {
737 api_run_control(corpus, service, &actor.owner, false, pool)
738}
739
740#[derive(Serialize, schemars::JsonSchema)]
742pub struct GlobalRunControlDto {
743 pub action: String,
745 pub affected: usize,
747 pub actor: String,
749}
750
751fn api_run_control_all(
752 owner: &str,
753 pause: bool,
754 pool: &State<DbPool>,
755) -> Result<Json<GlobalRunControlDto>, Status> {
756 let mut connection = pool.get().map_err(|_| Status::ServiceUnavailable)?;
757 let affected = crate::frontend::concerns::serve_pause_resume_all(&mut connection, owner, pause)?;
758 Ok(Json(GlobalRunControlDto {
759 action: if pause { "pause" } else { "resume" }.to_string(),
760 affected,
761 actor: owner.to_string(),
762 }))
763}
764
765#[rocket_okapi::openapi(tag = "Reports")]
769#[post("/api/conversions/pause")]
770pub fn pause_all_api(
771 actor: Actor,
772 pool: &State<DbPool>,
773) -> Result<Json<GlobalRunControlDto>, Status> {
774 api_run_control_all(&actor.owner, true, pool)
775}
776
777#[rocket_okapi::openapi(tag = "Reports")]
780#[post("/api/conversions/resume")]
781pub fn resume_all_api(
782 actor: Actor,
783 pool: &State<DbPool>,
784) -> Result<Json<GlobalRunControlDto>, Status> {
785 api_run_control_all(&actor.owner, false, pool)
786}
787
788#[derive(Serialize, schemars::JsonSchema)]
791pub struct RefreshAckDto {
792 pub job: String,
794 pub poll: String,
796 pub actor: String,
798}
799
800#[rocket_okapi::openapi(tag = "Reports")]
807#[post("/api/reports/refresh")]
808pub fn refresh_reports(
809 actor: Actor,
810 pool: &State<DbPool>,
811) -> Result<(Status, Json<RefreshAckDto>), Status> {
812 let job_uuid = jobs::spawn_report_refresh(pool.inner().clone(), &actor.owner)
813 .map_err(|_| Status::InternalServerError)?;
814 Ok((
815 Status::Accepted,
816 Json(RefreshAckDto {
817 job: job_uuid.to_string(),
818 poll: format!("/api/jobs/{job_uuid}"),
819 actor: actor.owner,
820 }),
821 ))
822}
823
824#[allow(clippy::result_large_err)] #[post("/reports/refresh")]
830pub fn refresh_reports_human(
831 session: Option<AdminSession>,
832 pool: &State<DbPool>,
833) -> Result<rocket::response::Redirect, AdminReject> {
834 let session = require_admin(session)?;
835 let uuid = jobs::spawn_report_refresh(pool.inner().clone(), &session.owner)
836 .map_err(|_| Status::InternalServerError)?;
837 Ok(rocket::response::Redirect::to(format!("/jobs/{uuid}")))
838}
839
840#[post("/corpus/<corpus_name>/<service_name>/refresh")]
849pub fn refresh_report_scope(
850 corpus_name: String,
851 service_name: String,
852 session: Option<AdminSession>,
853 pool: &State<DbPool>,
854) -> Result<Status, Status> {
855 session.ok_or(Status::Unauthorized)?;
856 let mut connection = pooled(pool)?;
857 let corpus = Corpus::find_by_name(&corpus_name.to_lowercase(), &mut connection)
858 .map_err(|_| Status::NotFound)?;
859 let service = Service::find_by_name(&service_name.to_lowercase(), &mut connection)
860 .map_err(|_| Status::NotFound)?;
861 crate::backend::invalidate_scope(&mut connection, corpus.id, service.id)
862 .map_err(|_| Status::InternalServerError)?;
863 Ok(Status::NoContent)
864}
865
866#[derive(Debug, Serialize, schemars::JsonSchema)]
868pub struct ScopeRefreshAckDto {
869 pub corpus: String,
871 pub service: String,
873 pub actor: String,
875}
876
877#[rocket_okapi::openapi(tag = "Reports")]
885#[post("/api/reports/<corpus>/<service>/refresh")]
886pub fn refresh_report_scope_api(
887 corpus: &str,
888 service: &str,
889 actor: Actor,
890 pool: &State<DbPool>,
891) -> Result<Json<ScopeRefreshAckDto>, Status> {
892 let mut connection = pooled(pool)?;
893 let corpus_record =
894 Corpus::find_by_name(&corpus.to_lowercase(), &mut connection).map_err(|_| Status::NotFound)?;
895 let service_record = Service::find_by_name(&service.to_lowercase(), &mut connection)
896 .map_err(|_| Status::NotFound)?;
897 crate::backend::invalidate_scope(&mut connection, corpus_record.id, service_record.id)
898 .map_err(|_| Status::InternalServerError)?;
899 tracing::info!(actor = %actor.owner, corpus, service, "scoped report refresh via API");
900 Ok(Json(ScopeRefreshAckDto {
901 corpus: corpus_record.name,
902 service: service_record.name,
903 actor: actor.owner,
904 }))
905}
906
907fn pooled(pool: &State<DbPool>) -> Result<crate::backend::PooledConn, Status> {
917 pool.get().map_err(|_| Status::ServiceUnavailable)
918}
919
920#[get("/corpus/<corpus_name>/<service_name>")]
922pub fn top_service_report(
923 corpus_name: String,
924 service_name: String,
925 session: Option<AdminSession>,
926 pool: &State<DbPool>,
927) -> Result<Template, Status> {
928 let mut connection = pooled(pool)?;
929 serve_report(
930 &mut connection,
931 pool.inner(),
932 corpus_name,
933 service_name,
934 None,
935 None,
936 None,
937 None,
938 session.is_some(),
939 )
940}
941
942#[get("/corpus/<corpus_name>/<service_name>/<severity>")]
944pub fn severity_service_report(
945 corpus_name: String,
946 service_name: String,
947 severity: String,
948 session: Option<AdminSession>,
949 pool: &State<DbPool>,
950) -> Result<Template, Status> {
951 let mut connection = pooled(pool)?;
952 serve_report(
953 &mut connection,
954 pool.inner(),
955 corpus_name,
956 service_name,
957 Some(severity),
958 None,
959 None,
960 None,
961 session.is_some(),
962 )
963}
964
965#[allow(clippy::too_many_arguments)]
967#[get("/corpus/<corpus_name>/<service_name>/<severity>?<params..>")]
968pub async fn severity_service_report_all(
969 corpus_name: String,
970 service_name: String,
971 severity: String,
972 params: Option<ReportParams>,
973 session: Option<AdminSession>,
974 limiter: &State<LiveReportLimiter>,
975 pool: &State<DbPool>,
976) -> Result<Template, Status> {
977 let _permit = match params.as_ref().and_then(|p| p.all) {
981 Some(true) => Some(limiter.acquire().await?),
982 _ => None,
983 };
984 let mut connection = pooled(pool)?;
985 serve_report(
986 &mut connection,
987 pool.inner(),
988 corpus_name,
989 service_name,
990 Some(severity),
991 None,
992 None,
993 params,
994 session.is_some(),
995 )
996}
997
998#[get("/corpus/<corpus_name>/<service_name>/<severity>/<category>")]
1000pub fn category_service_report(
1001 corpus_name: String,
1002 service_name: String,
1003 severity: String,
1004 category: String,
1005 session: Option<AdminSession>,
1006 pool: &State<DbPool>,
1007) -> Result<Template, Status> {
1008 let mut connection = pooled(pool)?;
1009 serve_report(
1010 &mut connection,
1011 pool.inner(),
1012 corpus_name,
1013 service_name,
1014 Some(severity),
1015 Some(category),
1016 None,
1017 None,
1018 session.is_some(),
1019 )
1020}
1021
1022#[allow(clippy::too_many_arguments)]
1024#[get("/corpus/<corpus_name>/<service_name>/<severity>/<category>?<params..>")]
1025pub async fn category_service_report_all(
1026 corpus_name: String,
1027 service_name: String,
1028 severity: String,
1029 category: String,
1030 params: Option<ReportParams>,
1031 session: Option<AdminSession>,
1032 limiter: &State<LiveReportLimiter>,
1033 pool: &State<DbPool>,
1034) -> Result<Template, Status> {
1035 let _permit = match params.as_ref().and_then(|p| p.all) {
1038 Some(true) => Some(limiter.acquire().await?),
1039 _ => None,
1040 };
1041 let mut connection = pooled(pool)?;
1042 serve_report(
1043 &mut connection,
1044 pool.inner(),
1045 corpus_name,
1046 service_name,
1047 Some(severity),
1048 Some(category),
1049 None,
1050 params,
1051 session.is_some(),
1052 )
1053}
1054
1055#[get("/corpus/<corpus_name>/<service_name>/<severity>/<category>/<what>")]
1057pub fn what_service_report(
1058 corpus_name: String,
1059 service_name: String,
1060 severity: String,
1061 category: String,
1062 what: String,
1063 session: Option<AdminSession>,
1064 pool: &State<DbPool>,
1065) -> Result<Template, Status> {
1066 let mut connection = pooled(pool)?;
1067 serve_report(
1068 &mut connection,
1069 pool.inner(),
1070 corpus_name,
1071 service_name,
1072 Some(severity),
1073 Some(category),
1074 Some(what),
1075 None,
1076 session.is_some(),
1077 )
1078}
1079
1080#[allow(clippy::too_many_arguments)]
1082#[get("/corpus/<corpus_name>/<service_name>/<severity>/<category>/<what>?<params..>")]
1083pub fn what_service_report_all(
1084 corpus_name: String,
1085 service_name: String,
1086 severity: String,
1087 category: String,
1088 what: String,
1089 params: Option<ReportParams>,
1090 session: Option<AdminSession>,
1091 pool: &State<DbPool>,
1092) -> Result<Template, Status> {
1093 let mut connection = pooled(pool)?;
1094 serve_report(
1095 &mut connection,
1096 pool.inner(),
1097 corpus_name,
1098 service_name,
1099 Some(severity),
1100 Some(category),
1101 Some(what),
1102 params,
1103 session.is_some(),
1104 )
1105}
1106
1107pub fn routes() -> Vec<Route> {
1109 routes![
1113 refresh_reports_human,
1114 refresh_report_scope,
1115 top_service_report,
1116 severity_service_report,
1117 severity_service_report_all,
1118 category_service_report,
1119 category_service_report_all,
1120 what_service_report,
1121 what_service_report_all,
1122 document_report_page,
1123 document_lookup_redirect
1124 ]
1125}
1126
1127#[cfg(test)]
1128mod tests {
1129 use super::is_valid_rerun_severity;
1130
1131 #[test]
1136 fn rerun_severity_is_context_aware() {
1137 for sev in ["no_problem", "warning", "error", "fatal", "invalid"] {
1139 assert!(
1140 is_valid_rerun_severity(sev, false),
1141 "{sev} is a valid no-category rerun severity"
1142 );
1143 }
1144 assert!(
1145 !is_valid_rerun_severity("info", false),
1146 "info is not a task status"
1147 );
1148 assert!(
1149 !is_valid_rerun_severity("todo", false),
1150 "todo isn't a rerun scope"
1151 );
1152 assert!(!is_valid_rerun_severity("bogus", false));
1153
1154 for sev in ["warning", "error", "fatal", "invalid", "info"] {
1156 assert!(
1157 is_valid_rerun_severity(sev, true),
1158 "{sev} is a valid with-category rerun severity"
1159 );
1160 }
1161 assert!(
1162 !is_valid_rerun_severity("no_problem", true),
1163 "no_problem tasks carry no messages, so it's invalid with a category"
1164 );
1165 assert!(!is_valid_rerun_severity("bogus", true));
1166 }
1167}