Wire backup, notification, report export, and file watcher modules into UI
This commit is contained in:
@@ -2,7 +2,10 @@ use adw::prelude::*;
|
||||
use gtk::gio;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::config::APP_ID;
|
||||
use crate::core::database::Database;
|
||||
use crate::core::notification;
|
||||
use crate::core::report;
|
||||
use crate::core::security;
|
||||
use super::widgets;
|
||||
|
||||
@@ -16,8 +19,19 @@ pub fn build_security_report_page(db: &Rc<Database>) -> adw::NavigationPage {
|
||||
let initial_content = build_report_content(db);
|
||||
content_stack.add_named(&initial_content, Some("content"));
|
||||
|
||||
// Header bar with scan button
|
||||
// Header bar with scan and export buttons
|
||||
let header = adw::HeaderBar::new();
|
||||
|
||||
// Export button
|
||||
let export_button = gtk::Button::builder()
|
||||
.label("Export")
|
||||
.tooltip_text("Save this report as HTML, JSON, or CSV")
|
||||
.build();
|
||||
export_button.update_property(&[
|
||||
gtk::accessible::Property::Label("Export security report as HTML, JSON, or CSV"),
|
||||
]);
|
||||
|
||||
// Scan button
|
||||
let scan_button = gtk::Button::builder()
|
||||
.label("Scan All")
|
||||
.tooltip_text("Scan all AppImages for vulnerabilities")
|
||||
@@ -56,19 +70,126 @@ pub fn build_security_report_page(db: &Rc<Database>) -> adw::NavigationPage {
|
||||
}
|
||||
stack_refresh.add_named(&new_content, Some("content"));
|
||||
stack_refresh.set_visible_child_name("content");
|
||||
|
||||
// Send desktop notifications for new CVE findings if enabled
|
||||
let settings = gio::Settings::new(APP_ID);
|
||||
if settings.boolean("security-notifications") {
|
||||
let threshold = settings.string("security-notification-threshold").to_string();
|
||||
glib::spawn_future_local(async move {
|
||||
let _ = gio::spawn_blocking(move || {
|
||||
let bg_db = Database::open().expect("Failed to open database");
|
||||
notification::check_and_notify(&bg_db, &threshold);
|
||||
})
|
||||
.await;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
header.pack_end(&scan_button);
|
||||
header.pack_end(&export_button);
|
||||
|
||||
// Toast overlay wraps the toolbar for feedback
|
||||
let toast_overlay = adw::ToastOverlay::new();
|
||||
|
||||
let toolbar = adw::ToolbarView::new();
|
||||
toolbar.add_top_bar(&header);
|
||||
toolbar.set_content(Some(&content_stack));
|
||||
toast_overlay.set_child(Some(&toolbar));
|
||||
|
||||
// Wire export button
|
||||
let db_export = db.clone();
|
||||
let toast_ref = toast_overlay.clone();
|
||||
export_button.connect_clicked(move |btn| {
|
||||
let html_filter = gtk::FileFilter::new();
|
||||
html_filter.set_name(Some("HTML (*.html)"));
|
||||
html_filter.add_pattern("*.html");
|
||||
html_filter.add_pattern("*.htm");
|
||||
|
||||
let json_filter = gtk::FileFilter::new();
|
||||
json_filter.set_name(Some("JSON (*.json)"));
|
||||
json_filter.add_pattern("*.json");
|
||||
|
||||
let csv_filter = gtk::FileFilter::new();
|
||||
csv_filter.set_name(Some("CSV (*.csv)"));
|
||||
csv_filter.add_pattern("*.csv");
|
||||
|
||||
let filters = gio::ListStore::new::<gtk::FileFilter>();
|
||||
filters.append(&html_filter);
|
||||
filters.append(&json_filter);
|
||||
filters.append(&csv_filter);
|
||||
|
||||
let dialog = gtk::FileDialog::builder()
|
||||
.title("Export Security Report")
|
||||
.initial_name("driftwood-security-report.html")
|
||||
.filters(&filters)
|
||||
.default_filter(&html_filter)
|
||||
.modal(true)
|
||||
.build();
|
||||
|
||||
let btn_clone = btn.clone();
|
||||
let db_for_save = db_export.clone();
|
||||
let toast_for_save = toast_ref.clone();
|
||||
let window = btn.root().and_downcast::<gtk::Window>();
|
||||
|
||||
dialog.save(window.as_ref(), None::<&gio::Cancellable>, move |result| {
|
||||
let Ok(file) = result else { return };
|
||||
let Some(path) = file.path() else { return };
|
||||
|
||||
// Detect format from extension
|
||||
let ext = path.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("html")
|
||||
.to_lowercase();
|
||||
|
||||
let format = match ext.as_str() {
|
||||
"json" => report::ReportFormat::Json,
|
||||
"csv" => report::ReportFormat::Csv,
|
||||
_ => report::ReportFormat::Html,
|
||||
};
|
||||
|
||||
btn_clone.set_sensitive(false);
|
||||
btn_clone.set_label("Exporting...");
|
||||
let btn_done = btn_clone.clone();
|
||||
let toast_done = toast_for_save.clone();
|
||||
let db_bg = db_for_save.clone();
|
||||
|
||||
glib::spawn_future_local(async move {
|
||||
let path_clone = path.clone();
|
||||
let result = gio::spawn_blocking(move || {
|
||||
let bg_db = Database::open().expect("Failed to open database");
|
||||
// Use the records from the bg_db since Rc<Database> isn't Send
|
||||
let _ = db_bg; // acknowledge capture but use bg_db
|
||||
let report_data = report::build_report(&bg_db, None);
|
||||
let content = report::render(&report_data, format);
|
||||
std::fs::write(&path_clone, content)
|
||||
})
|
||||
.await;
|
||||
|
||||
btn_done.set_sensitive(true);
|
||||
btn_done.set_label("Export");
|
||||
|
||||
match result {
|
||||
Ok(Ok(())) => {
|
||||
let filename = path.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("report");
|
||||
toast_done.add_toast(
|
||||
adw::Toast::new(&format!("Report saved as {}", filename)),
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
toast_done.add_toast(adw::Toast::new("Failed to export report"));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
adw::NavigationPage::builder()
|
||||
.title("Security Report")
|
||||
.tag("security-report")
|
||||
.child(&toolbar)
|
||||
.child(&toast_overlay)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user