use std::sync::mpsc;
use std::time::Instant;
use crate::app::{self, App, Screen};
use crate::containers;
use crate::event::AppEvent;
use crate::file_browser;
use crate::providers;
use crate::ssh_config;
use crate::tui;
use crate::vault_ssh;
pub(crate) fn handle_tick(
app: &mut App,
anim: &mut crate::animation::AnimationState,
vault_signing: bool,
last_config_check: &mut Instant,
) {
app.tick_status();
app.tick_toast();
let provider_syncing = !app.providers.syncing.is_empty();
let tunnels_animating =
matches!(app.top_page, crate::app::TopPage::Tunnels) && !app.tunnels.active.is_empty();
if anim.has_checking_hosts(app)
|| vault_signing
|| provider_syncing
|| anim.has_reachable_hosts(app)
|| tunnels_animating
{
anim.tick_spinner();
}
if vault_signing {
if let Some(ref mut status) = app.status_center.status {
if status.sticky && !status.is_error() {
let frame = crate::animation::SPINNER_FRAMES
[anim.spinner_tick as usize % crate::animation::SPINNER_FRAMES.len()];
if let Some(updated) = crate::replace_spinner_frame(&status.text, frame) {
status.text = updated;
}
}
}
}
if provider_syncing {
if let Some(ref mut status) = app.status_center.status {
let frame = crate::animation::SPINNER_FRAMES
[anim.spinner_tick as usize % crate::animation::SPINNER_FRAMES.len()];
if let Some(updated) = crate::replace_spinner_frame(&status.text, frame) {
status.text = updated;
status.created_at = std::time::Instant::now();
}
}
}
if last_config_check.elapsed() >= std::time::Duration::from_secs(4) {
app.check_config_changed();
*last_config_check = Instant::now();
}
let exited = app.poll_tunnels();
for (_alias, msg, is_error) in exited {
if is_error {
app.notify_background_error(msg);
} else {
app.notify_background(msg);
}
}
}
pub(crate) fn handle_ping_result(
app: &mut App,
alias: String,
rtt_ms: Option<u32>,
generation: u64,
) {
if generation == app.ping.generation {
let status = app::classify_ping(rtt_ms, app.ping.slow_threshold_ms);
let now = Instant::now();
log::debug!(
"ping-result: {} → {:?} (rtt={:?}ms, gen={})",
alias,
status,
rtt_ms,
generation
);
app.ping.status.insert(alias.clone(), status.clone());
app.ping.last_checked.insert(alias.clone(), now);
app::propagate_ping_to_dependents(
&app.hosts_state.list,
&mut app.ping.status,
&alias,
&status,
);
let mut propagated = 0usize;
for h in &app.hosts_state.list {
if h.proxy_jump == alias {
app.ping.last_checked.insert(h.alias.clone(), now);
propagated += 1;
}
}
if propagated > 0 {
log::debug!(
"ping-result: propagated bastion {} status+timestamp to {} dependent(s)",
alias,
propagated
);
}
if app.ping.filter_down_only {
app.apply_filter();
}
if app.hosts_state.sort_mode == app::SortMode::Status {
app.apply_sort();
}
if !app.ping.status.is_empty()
&& app
.ping
.status
.values()
.all(|s| !matches!(s, app::PingStatus::Checking))
{
app.ping.checked_at = Some(Instant::now());
}
}
}
pub(crate) fn handle_sync_progress(app: &mut App, provider: String, message: String) {
if app.providers.syncing.contains_key(&provider) && app.providers.sync_done.is_empty() {
let name = providers::provider_display_name(&provider);
let spinner = crate::animation::SPINNER_FRAMES[0];
app.notify_background(crate::messages::provider_progress(spinner, name, &message));
}
}
pub(crate) fn handle_sync_complete(
app: &mut App,
provider: String,
hosts: Vec<crate::providers::ProviderHost>,
last_config_check: &mut Instant,
) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let display_name = providers::provider_display_name(&provider);
let before_aliases = app.snapshot_alias_set();
let (_msg, is_err, total, added, updated, stale) =
app.apply_sync_result(&provider, hosts, false);
if is_err {
app.providers.sync_history.insert(
provider.clone(),
app::SyncRecord {
timestamp: now,
message: format!("{}: sync failed", display_name),
is_error: true,
},
);
app.providers.sync_had_errors = true;
} else {
let label = if total == 1 { "server" } else { "servers" };
let message = format!(
"{} {}{}",
total,
label,
crate::format_sync_diff(added, updated, stale)
);
app.providers.sync_history.insert(
provider.clone(),
app::SyncRecord {
timestamp: now,
message,
is_error: false,
},
);
app.providers.batch_added += added;
app.providers.batch_updated += updated;
app.providers.batch_stale += stale;
}
app.providers.syncing.remove(&provider);
app.providers.sync_done.push(display_name.to_string());
crate::set_sync_summary(app);
*last_config_check = Instant::now();
app.queue_new_aliases_since(&before_aliases);
}
pub(crate) fn handle_sync_partial(
app: &mut App,
provider: String,
hosts: Vec<crate::providers::ProviderHost>,
failures: usize,
total: usize,
last_config_check: &mut Instant,
) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let display_name = providers::provider_display_name(provider.as_str());
let before_aliases = app.snapshot_alias_set();
let (msg, is_err, synced, added, updated, stale) =
app.apply_sync_result(&provider, hosts, true);
if is_err {
app.providers.sync_history.insert(
provider.clone(),
app::SyncRecord {
timestamp: now,
message: msg,
is_error: true,
},
);
} else {
let label = if synced == 1 { "server" } else { "servers" };
app.providers.sync_history.insert(
provider.clone(),
app::SyncRecord {
timestamp: now,
message: format!(
"{} {}{} ({} of {} failed)",
synced,
label,
crate::format_sync_diff(added, updated, stale),
failures,
total
),
is_error: true,
},
);
app.providers.batch_added += added;
app.providers.batch_updated += updated;
app.providers.batch_stale += stale;
}
app.providers.sync_had_errors = true;
app.providers.syncing.remove(&provider);
app.providers.sync_done.push(display_name.to_string());
crate::set_sync_summary(app);
*last_config_check = Instant::now();
app.queue_new_aliases_since(&before_aliases);
}
pub(crate) fn handle_sync_error(
app: &mut App,
provider: String,
message: String,
last_config_check: &mut Instant,
) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let display_name = providers::provider_display_name(provider.as_str());
app.providers.sync_history.insert(
provider.clone(),
app::SyncRecord {
timestamp: now,
message: message.clone(),
is_error: true,
},
);
app.providers.sync_had_errors = true;
app.providers.syncing.remove(&provider);
app.providers.sync_done.push(display_name.to_string());
crate::set_sync_summary(app);
*last_config_check = Instant::now();
}
pub(crate) fn handle_update_available(app: &mut App, version: String, headline: Option<String>) {
app.update.available = Some(version);
app.update.headline = headline;
}
pub(crate) fn handle_file_browser_listing(
app: &mut App,
alias: String,
path: String,
entries: Result<Vec<crate::file_browser::FileEntry>, String>,
terminal: &mut tui::Tui,
) {
let mut record_connection = false;
if let Some(ref mut fb) = app.file_browser {
if fb.alias == alias {
fb.remote_loading = false;
match entries {
Ok(listing) => {
if !fb.connection_recorded {
fb.connection_recorded = true;
record_connection = true;
}
if fb.remote_path.is_empty() || fb.remote_path != path {
fb.remote_path = path;
}
fb.remote_entries = listing;
fb.remote_error = None;
fb.remote_list_state = ratatui::widgets::ListState::default();
fb.remote_list_state.select(Some(0));
}
Err(msg) => {
if fb.remote_path.is_empty() {
fb.remote_path = path;
}
fb.remote_error = Some(msg);
fb.remote_entries.clear();
}
}
}
}
if record_connection {
app.history.record(&alias);
app.apply_sort();
}
terminal.force_redraw();
}
pub(crate) fn handle_scp_complete(
app: &mut App,
alias: String,
success: bool,
message: String,
events_tx: &mpsc::Sender<AppEvent>,
terminal: &mut tui::Tui,
) {
let mut refresh_remote: Option<(
String,
Option<String>,
String,
bool,
file_browser::BrowserSort,
)> = None;
let matched = if let Some(ref mut fb) = app.file_browser {
if fb.alias == alias {
fb.transferring = None;
if success {
app.history.record(&alias);
app.hosts_state.render_cache.invalidate();
fb.local_selected.clear();
fb.remote_selected.clear();
match file_browser::list_local(&fb.local_path, fb.show_hidden, fb.sort) {
Ok(entries) => {
fb.local_entries = entries;
fb.local_error = None;
}
Err(e) => {
fb.local_entries = Vec::new();
fb.local_error = Some(e.to_string());
}
}
fb.local_list_state.select(Some(0));
if !fb.remote_path.is_empty() {
fb.remote_loading = true;
fb.remote_entries.clear();
fb.remote_error = None;
fb.remote_list_state = ratatui::widgets::ListState::default();
refresh_remote = Some((
fb.alias.clone(),
fb.askpass.clone(),
fb.remote_path.clone(),
fb.show_hidden,
fb.sort,
));
}
} else {
fb.transfer_error = Some(message.clone());
}
true
} else {
false
}
} else {
false
};
if matched && success {
app.notify_background(crate::messages::TRANSFER_COMPLETE);
app.apply_sort();
}
if let Some((fb_alias, askpass_fb, path, show_hidden, sort)) = refresh_remote {
let has_tunnel = app.tunnels.active.contains_key(&fb_alias);
let ctx = crate::ssh_context::OwnedSshContext {
alias: fb_alias,
config_path: app.reload.config_path.clone(),
askpass: askpass_fb,
bw_session: app.bw_session.clone(),
has_tunnel,
};
let tx = events_tx.clone();
file_browser::spawn_remote_listing(ctx, path, show_hidden, sort, move |a, p, e| {
let _ = tx.send(AppEvent::FileBrowserListing {
alias: a,
path: p,
entries: e,
});
});
}
crate::askpass::cleanup_marker(&alias);
terminal.force_redraw();
}
pub(crate) fn handle_snippet_host_done(
app: &mut App,
run_id: u64,
alias: String,
stdout: String,
stderr: String,
exit_code: Option<i32>,
) {
if exit_code == Some(0) {
app.history.record(&alias);
app.apply_sort();
}
if let Some(ref mut state) = app.snippets.output {
if state.run_id == run_id {
state.results.push(app::SnippetHostOutput {
alias,
stdout,
stderr,
exit_code,
});
}
}
}
pub(crate) fn handle_snippet_progress(app: &mut App, run_id: u64, completed: usize, total: usize) {
if let Some(ref mut state) = app.snippets.output {
if state.run_id == run_id {
state.completed = completed;
state.total = total;
}
}
}
pub(crate) fn handle_snippet_all_done(app: &mut App, run_id: u64) {
if let Some(ref mut state) = app.snippets.output {
if state.run_id == run_id {
state.all_done = true;
}
}
}
pub(crate) fn handle_container_listing(
app: &mut App,
alias: String,
result: Result<containers::ContainerListing, containers::ContainerError>,
events_tx: &mpsc::Sender<AppEvent>,
) {
if !app.hosts_state.list.iter().any(|h| h.alias == alias) {
log::debug!(
"[purple] container_listing dropped: alias={} no longer in config",
alias
);
crate::askpass::cleanup_marker(&alias);
app.containers_overview.auto_list_in_flight.remove(&alias);
drive_refresh_batch(app, &alias, events_tx);
return;
}
match &result {
Ok(listing) => {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
app.container_cache.insert(
alias.clone(),
containers::ContainerCacheEntry {
timestamp: now,
runtime: listing.runtime,
engine_version: listing.engine_version.clone(),
containers: listing.containers.clone(),
},
);
containers::save_container_cache(&app.container_cache);
crate::handler::containers_overview::prefetch_inspect_for_listing(
app,
&alias,
listing.runtime,
&listing.containers,
events_tx,
);
}
Err(e) => {
if let Some(rt) = e.runtime {
if let Some(entry) = app.container_cache.get_mut(&alias) {
entry.runtime = rt;
}
}
}
}
if let Some(ref mut state) = app.container_state {
if state.alias == alias {
match result {
Ok(listing) => {
state.runtime = Some(listing.runtime);
state.containers = listing.containers;
state.loading = false;
state.error = None;
if let Some(sel) = state.list_state.selected() {
if sel >= state.containers.len() && !state.containers.is_empty() {
state.list_state.select(Some(0));
}
} else if !state.containers.is_empty() {
state.list_state.select(Some(0));
}
}
Err(e) => {
if let Some(rt) = e.runtime {
state.runtime = Some(rt);
}
state.loading = false;
state.error = Some(e.message);
}
}
}
}
crate::askpass::cleanup_marker(&alias);
app.containers_overview.auto_list_in_flight.remove(&alias);
drive_refresh_batch(app, &alias, events_tx);
}
pub(crate) fn drive_refresh_batch(app: &mut App, alias: &str, events_tx: &mpsc::Sender<AppEvent>) {
let Some(batch) = app.containers_overview.refresh_batch.as_mut() else {
return;
};
if !batch.in_flight_aliases.remove(alias) {
return;
}
batch.in_flight = batch.in_flight.saturating_sub(1);
batch.completed += 1;
let next = batch.queue.pop_front();
let total = batch.total;
let completed = batch.completed;
let queue_remaining = batch.queue.len();
let spawned = next.is_some();
if let Some(item) = next {
batch.in_flight += 1;
batch.in_flight_aliases.insert(item.alias.clone());
let config_path = app.reload.config_path.clone();
let bw_session = app.bw_session.clone();
let ctx = crate::ssh_context::OwnedSshContext {
alias: item.alias,
config_path,
askpass: item.askpass,
bw_session,
has_tunnel: item.has_tunnel,
};
let tx = events_tx.clone();
containers::spawn_container_listing(ctx, item.cached_runtime, move |a, r| {
let _ = tx.send(AppEvent::ContainerListing {
alias: a,
result: r,
});
});
}
let still_in_flight = app
.containers_overview
.refresh_batch
.as_ref()
.map(|b| b.in_flight)
.unwrap_or(0);
log::debug!(
"[purple] refresh_batch: alias={} done={}/{} in_flight={} queue={} spawned_next={}",
alias,
completed,
total,
still_in_flight,
queue_remaining,
spawned
);
if queue_remaining == 0 && still_in_flight == 0 {
app.containers_overview.refresh_batch = None;
app.status_center.status = None;
app.notify(crate::messages::container_refresh_complete(total));
} else {
app.notify_progress(crate::messages::container_refresh_progress(
completed, total,
));
}
}
pub(crate) fn handle_container_inspect_complete(
app: &mut App,
alias: String,
container_id: String,
result: Result<containers::ContainerInspect, String>,
) {
if !app.hosts_state.list.iter().any(|h| h.alias == alias) {
log::debug!(
"[purple] container_inspect_complete dropped: alias={} no longer in config",
alias
);
app.containers_overview
.inspect_cache
.in_flight
.remove(&container_id);
return;
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
log::debug!(
"[purple] container_inspect_complete: alias={} id={} ok={}",
alias,
container_id,
result.is_ok()
);
app.containers_overview.inspect_cache.entries.insert(
container_id.clone(),
crate::app::InspectCacheEntry {
timestamp: now,
result,
},
);
app.containers_overview
.inspect_cache
.in_flight
.remove(&container_id);
}
pub(crate) fn handle_container_logs_tail_complete(
app: &mut App,
alias: String,
container_id: String,
result: Result<Vec<String>, String>,
) {
if !app.hosts_state.list.iter().any(|h| h.alias == alias) {
log::debug!(
"[purple] container_logs_tail_complete dropped: alias={} no longer in config",
alias
);
app.containers_overview
.logs_cache
.in_flight
.remove(&container_id);
return;
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
log::debug!(
"[purple] container_logs_tail_complete: alias={} id={} ok={}",
alias,
container_id,
result.is_ok()
);
app.containers_overview.logs_cache.entries.insert(
container_id.clone(),
crate::app::LogsCacheEntry {
timestamp: now,
result,
},
);
app.containers_overview
.logs_cache
.in_flight
.remove(&container_id);
}
pub(crate) fn handle_container_logs_complete(
app: &mut App,
alias: String,
container_id: String,
container_name: String,
result: Result<Vec<String>, String>,
) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
if let Screen::ContainerLogs {
alias: scr_alias,
container_id: scr_id,
body,
error,
fetched_at,
scroll,
last_render_height,
search,
..
} = &mut app.screen
{
if scr_alias == &alias && scr_id == &container_id {
match result {
Ok(lines) => {
log::debug!(
"[purple] container_logs_complete: {} lines for alias={} id={}",
lines.len(),
alias,
container_id
);
*body = lines;
*error = None;
*fetched_at = now;
*scroll = crate::handler::container_logs::tail_scroll(
body.len(),
*last_render_height,
);
if let Some(s) = search.as_mut() {
crate::handler::container_logs::refresh_search(body, s);
log::debug!(
"[purple] container_logs: search refreshed query={:?} matches={}",
s.query,
s.matches.len()
);
crate::handler::container_logs::recenter_on_match(
body.len(),
*last_render_height,
s,
scroll,
);
}
}
Err(e) => {
log::warn!(
"[external] container_logs_complete: alias={} id={} error={}",
alias,
container_id,
e
);
body.clear();
*error = Some(e);
*fetched_at = now;
*scroll = 0;
}
}
return;
}
}
log::debug!(
"[purple] container_logs_complete dropped (overlay closed): alias={} id={} name={}",
alias,
container_id,
container_name
);
}
pub(crate) fn handle_container_action_complete(
app: &mut App,
alias: String,
action: containers::ContainerAction,
result: Result<(), String>,
events_tx: &mpsc::Sender<AppEvent>,
) {
let should_refresh = if let Some(ref mut state) = app.container_state {
if state.alias == alias {
state.action_in_progress = None;
match result {
Ok(()) => {
state.loading = true;
Some((state.alias.clone(), state.askpass.clone(), state.runtime))
}
Err(e) => {
state.error = Some(e);
None
}
}
} else {
None
}
} else {
None
};
if let Some((refresh_alias, askpass, cached_runtime)) = should_refresh {
app.notify(crate::messages::container_action_complete(action.as_str()));
let has_tunnel = app.tunnels.active.contains_key(&refresh_alias);
app.containers_overview
.auto_list_in_flight
.insert(refresh_alias.clone());
let ctx = crate::ssh_context::OwnedSshContext {
alias: refresh_alias,
config_path: app.reload.config_path.clone(),
askpass,
bw_session: app.bw_session.clone(),
has_tunnel,
};
let tx = events_tx.clone();
containers::spawn_container_listing(ctx, cached_runtime, move |a, r| {
let _ = tx.send(AppEvent::ContainerListing {
alias: a,
result: r,
});
});
}
crate::askpass::cleanup_marker(&alias);
}
pub(crate) fn handle_vault_sign_result(
app: &mut App,
alias: String,
existing_cert_file: String,
success: bool,
message: String,
) {
if success {
let mut host_missing = false;
if crate::should_write_certificate_file(&existing_cert_file) {
if let Ok(cert_path) = vault_ssh::cert_path_for(&alias) {
let updated = app
.hosts_state
.ssh_config
.set_host_certificate_file(&alias, &cert_path.to_string_lossy());
if !updated {
host_missing = true;
}
}
}
app.refresh_cert_cache(&alias);
if host_missing {
app.notify_error(crate::messages::vault_cert_saved_host_gone(&alias));
} else {
app.notify(crate::messages::vault_signed(&alias));
}
} else {
app.notify_error(crate::messages::vault_sign_failed(&alias, &message));
}
}
pub(crate) fn handle_vault_sign_progress(
app: &mut App,
alias: String,
done: usize,
total: usize,
spinner_tick: u64,
) {
const ALIAS_BUDGET: usize = 40;
let display_alias: String = if alias.chars().count() > ALIAS_BUDGET {
let cut: String = alias.chars().take(ALIAS_BUDGET - 1).collect();
format!("{}\u{2026}", cut)
} else {
alias.clone()
};
let spinner = crate::animation::SPINNER_FRAMES
[spinner_tick as usize % crate::animation::SPINNER_FRAMES.len()];
app.notify_progress(crate::messages::vault_signing_progress(
spinner,
done,
total,
&display_alias,
));
}
pub(crate) fn handle_vault_sign_all_done(
app: &mut App,
signed: u32,
failed: u32,
skipped: u32,
cancelled: bool,
aborted_message: Option<String>,
first_error: Option<String>,
) -> std::ops::ControlFlow<()> {
app.vault.signing_cancel = None;
if let Some(handle) = app.vault.sign_thread.take() {
log::debug!("[purple] vault sign thread: joining");
let _ = handle.join();
log::info!(
"[purple] vault sign thread: joined (signed={} failed={} skipped={} cancelled={})",
signed,
failed,
skipped,
cancelled
);
}
if let Some(msg) = aborted_message {
app.notify_sticky_error(msg);
return std::ops::ControlFlow::Break(()); }
if cancelled {
let msg = crate::messages::vault_signing_cancelled_summary(
signed,
failed,
first_error.as_deref(),
);
if failed > 0 {
app.notify_sticky_error(msg);
} else {
app.notify_info(msg);
}
return std::ops::ControlFlow::Break(()); }
let summary_msg =
crate::format_vault_sign_summary(signed, failed, skipped, first_error.as_deref());
if signed > 0 {
if app.is_form_open() {
app.pending_vault_config_write = true;
if failed > 0 {
app.notify_sticky_error(summary_msg);
} else {
app.notify_info(summary_msg);
}
} else if app.external_config_changed() {
let reapply: Vec<(String, String)> = app
.hosts_state
.ssh_config
.host_entries()
.into_iter()
.filter_map(|h| {
if h.vault_ssh.is_some()
&& crate::should_write_certificate_file(&h.certificate_file)
{
vault_ssh::cert_path_for(&h.alias)
.ok()
.map(|p| (h.alias.clone(), p.to_string_lossy().into_owned()))
} else {
None
}
})
.collect();
match ssh_config::model::SshConfigFile::parse(&app.reload.config_path) {
Ok(fresh) => {
app.hosts_state.ssh_config = fresh;
let mut reapplied = 0usize;
for (alias, cert_path) in &reapply {
let entry = app
.hosts_state
.ssh_config
.host_entries()
.into_iter()
.find(|h| &h.alias == alias);
if let Some(entry) = entry {
if crate::should_write_certificate_file(&entry.certificate_file)
&& app
.hosts_state
.ssh_config
.set_host_certificate_file(alias, cert_path)
{
reapplied += 1;
}
}
}
if reapplied > 0 {
if let Err(e) = app.hosts_state.ssh_config.write() {
app.notify_sticky_error(crate::messages::vault_config_reapply_failed(
signed as usize,
&e,
));
} else {
app.update_last_modified();
app.reload_hosts();
if failed > 0 {
app.notify_sticky_error(
crate::messages::vault_external_edits_merged(
&summary_msg,
reapplied,
),
);
} else {
app.notify_info(crate::messages::vault_external_edits_merged(
&summary_msg,
reapplied,
));
}
}
} else {
app.reload_hosts();
app.notify_sticky_error(crate::messages::vault_external_edits_no_write(
&summary_msg,
));
}
}
Err(e) => {
app.notify_sticky_error(crate::messages::vault_reparse_failed(
signed as usize,
&e,
));
}
}
} else if let Err(e) = app.hosts_state.ssh_config.write() {
app.notify_sticky_error(crate::messages::vault_config_update_failed(
signed as usize,
&e,
));
} else {
app.update_last_modified();
app.reload_hosts();
if failed > 0 {
app.notify_sticky_error(summary_msg);
} else {
app.notify_info(summary_msg);
}
}
} else if failed > 0 {
app.notify_sticky_error(summary_msg);
} else {
app.notify_info(summary_msg);
}
std::ops::ControlFlow::Continue(()) }
pub(crate) fn handle_cert_check_result(
app: &mut App,
alias: String,
status: vault_ssh::CertStatus,
) {
app.vault.cert_checks_in_flight.remove(&alias);
let mtime = crate::tui_loop::current_cert_mtime(&alias, app);
app.vault
.cert_cache
.insert(alias, (Instant::now(), status, mtime));
}
pub(crate) fn handle_cert_check_error(app: &mut App, alias: String, message: String) {
app.vault.cert_checks_in_flight.remove(&alias);
app.vault.cert_cache.insert(
alias.clone(),
(
Instant::now(),
vault_ssh::CertStatus::Invalid(message.clone()),
None,
),
);
app.notify_background_error(crate::messages::vault_cert_check_failed(&alias, &message));
}