use serde_json::{Value, json};
pub async fn list_recent_errors(limit: u64) -> Result<Value, String> {
let limit = (limit as usize).min(100);
let errors = super::bug_report::aggregate_errors(limit);
let summaries: Vec<Value> = errors
.iter()
.map(|e| {
json!({
"fingerprint": e.record.fingerprint,
"crate_target": e.record.crate_target,
"crate_version": e.record.crate_version,
"summary": e.record.summary(),
"occurrences": e.occurrences,
"timestamp_secs": e.record.timestamp_secs,
"os": e.record.os,
"arch": e.record.arch,
})
})
.collect();
Ok(json!({
"errors": summaries,
"total": summaries.len(),
"limit": limit,
}))
}
pub async fn preview_bug_report(fingerprint: &str) -> Result<Value, String> {
let errors = super::bug_report::aggregate_errors(500);
let found = errors
.into_iter()
.find(|e| e.record.fingerprint == fingerprint)
.ok_or_else(|| {
format!(
"fingerprint `{fingerprint}` not found in local error stores; \
run list_recent_errors to see available fingerprints"
)
})?;
let preview = super::bug_report::build_preview(&found);
let changes: Vec<Value> = preview
.scrub_changes
.iter()
.map(|c| json!({ "pattern": c.pattern, "hint": c.hint }))
.collect();
Ok(json!({
"fingerprint": preview.fingerprint,
"title": preview.title,
"body": preview.body,
"labels": preview.labels,
"scrub_changes": changes,
"note": "This is the exact content that will be filed. Call report_bug with confirm:true to file.",
}))
}
pub async fn report_bug(fingerprint: &str, confirm: bool) -> Result<Value, String> {
let errors = super::bug_report::aggregate_errors(500);
let found = errors
.into_iter()
.find(|e| e.record.fingerprint == fingerprint)
.ok_or_else(|| format!("fingerprint `{fingerprint}` not found; run list_recent_errors"))?;
let preview = super::bug_report::build_preview(&found);
if !confirm {
let changes: Vec<Value> = preview
.scrub_changes
.iter()
.map(|c| json!({ "pattern": c.pattern, "hint": c.hint }))
.collect();
return Ok(json!({
"filed": false,
"note": "confirm:false — preview only. Call with confirm:true to file.",
"preview": {
"fingerprint": preview.fingerprint,
"title": preview.title,
"body": preview.body,
"labels": preview.labels,
"scrub_changes": changes,
}
}));
}
let guard = super::bug_report::RateLimitGuard::production();
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let rl_decision = guard.check(fingerprint, now_secs);
if !rl_decision.is_allowed() {
return Ok(json!({
"filed": false,
"rate_limited": true,
"note": rl_decision.block_reason(),
}));
}
let fp_owned = fingerprint.to_string();
let provider = super::bug_report::ResolvedProvider;
let result =
tokio::task::spawn_blocking(move || super::bug_report::file_issue(&preview, &provider))
.await
.map_err(|e| format!("internal error: spawn_blocking failed: {e}"))?;
match result {
Ok(filing) => {
guard.record_filed(&fp_owned, now_secs);
Ok(json!({
"filed": filing.filed,
"deduped": filing.deduped,
"issue_url": filing.issue_url,
"issue_number": filing.issue_number,
}))
}
Err(super::bug_report::GithubFilingError::NoToken) => Ok(json!({
"filed": false,
"note": super::bug_report::GithubFilingError::NoToken.to_string(),
})),
Err(e) => Err(format!("GitHub filing failed: {e}")),
}
}