Wire backup, notification, report export, and file watcher modules into UI

This commit is contained in:
2026-02-27 21:03:19 +02:00
parent 668206e5e5
commit cddbe5b2e2
3 changed files with 407 additions and 5 deletions

View File

@@ -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()
}