Refresh activity list when change in time modifies it

This commit is contained in:
Albert Armea 2025-12-30 10:00:24 -05:00
parent 8e754d7c77
commit a17fb5104d
3 changed files with 55 additions and 14 deletions

View file

@ -9,6 +9,7 @@ use shepherd_config::{Entry, Policy};
use shepherd_host_api::{HostCapabilities, HostSessionHandle}; use shepherd_host_api::{HostCapabilities, HostSessionHandle};
use shepherd_store::{AuditEvent, AuditEventType, Store}; use shepherd_store::{AuditEvent, AuditEventType, Store};
use shepherd_util::{EntryId, MonotonicInstant, SessionId}; use shepherd_util::{EntryId, MonotonicInstant, SessionId};
use std::collections::HashSet;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tracing::{debug, info}; use tracing::{debug, info};
@ -35,6 +36,8 @@ pub struct CoreEngine {
store: Arc<dyn Store>, store: Arc<dyn Store>,
capabilities: HostCapabilities, capabilities: HostCapabilities,
current_session: Option<ActiveSession>, current_session: Option<ActiveSession>,
/// Tracks which entries were enabled on the last tick, to detect availability changes
last_availability_set: HashSet<EntryId>,
} }
impl CoreEngine { impl CoreEngine {
@ -59,6 +62,7 @@ impl CoreEngine {
store, store,
capabilities, capabilities,
current_session: None, current_session: None,
last_availability_set: HashSet::new(),
} }
} }
@ -300,10 +304,29 @@ impl CoreEngine {
} }
} }
/// Tick the engine - check for warnings and expiry /// Tick the engine - check for warnings, expiry, and availability changes
pub fn tick(&mut self, now_mono: MonotonicInstant) -> Vec<CoreEvent> { pub fn tick(&mut self, now_mono: MonotonicInstant, now: DateTime<Local>) -> Vec<CoreEvent> {
let mut events = Vec::new(); let mut events = Vec::new();
// Check if the set of available entries has changed
let current_availability: HashSet<EntryId> = self
.policy
.entries
.iter()
.filter(|e| self.evaluate_entry(e, now).enabled)
.map(|e| e.id.clone())
.collect();
if current_availability != self.last_availability_set {
debug!(
previous = ?self.last_availability_set,
current = ?current_availability,
"Entry availability set changed"
);
self.last_availability_set = current_availability;
events.push(CoreEvent::AvailabilitySetChanged);
}
let session = match &mut self.current_session { let session = match &mut self.current_session {
Some(s) => s, Some(s) => s,
None => return events, None => return events,
@ -676,19 +699,23 @@ mod tests {
engine.start_session(plan, now, now_mono); engine.start_session(plan, now, now_mono);
} }
// No warnings initially // No warnings initially (first tick may emit AvailabilitySetChanged)
let events = engine.tick(now_mono); let events = engine.tick(now_mono, now);
assert!(events.is_empty()); // Filter to just warning events for this test
let warning_events: Vec<_> = events.iter().filter(|e| matches!(e, CoreEvent::Warning { .. })).collect();
assert!(warning_events.is_empty());
// At 70 seconds (10 seconds past warning threshold), warning should fire // At 70 seconds (10 seconds past warning threshold), warning should fire
let later = now_mono + Duration::from_secs(70); let later = now_mono + Duration::from_secs(70);
let events = engine.tick(later); let events = engine.tick(later, now);
assert_eq!(events.len(), 1); let warning_events: Vec<_> = events.iter().filter(|e| matches!(e, CoreEvent::Warning { .. })).collect();
assert!(matches!(events[0], CoreEvent::Warning { threshold_seconds: 60, .. })); assert_eq!(warning_events.len(), 1);
assert!(matches!(warning_events[0], CoreEvent::Warning { threshold_seconds: 60, .. }));
// Warning shouldn't fire twice // Warning shouldn't fire twice
let events = engine.tick(later); let events = engine.tick(later, now);
assert!(events.is_empty()); let warning_events: Vec<_> = events.iter().filter(|e| matches!(e, CoreEvent::Warning { .. })).collect();
assert!(warning_events.is_empty());
} }
#[test] #[test]
@ -739,8 +766,10 @@ mod tests {
// At 61 seconds, should be expired // At 61 seconds, should be expired
let later = now_mono + Duration::from_secs(61); let later = now_mono + Duration::from_secs(61);
let events = engine.tick(later); let events = engine.tick(later, now);
assert_eq!(events.len(), 1); // Filter to just expiry events for this test
assert!(matches!(events[0], CoreEvent::ExpireDue { .. })); let expiry_events: Vec<_> = events.iter().filter(|e| matches!(e, CoreEvent::ExpireDue { .. })).collect();
assert_eq!(expiry_events.len(), 1);
assert!(matches!(expiry_events[0], CoreEvent::ExpireDue { .. }));
} }
} }

View file

@ -17,6 +17,9 @@ pub enum CoreEvent {
deadline: Option<DateTime<Local>>, deadline: Option<DateTime<Local>>,
}, },
/// The set of available entries has changed (e.g., due to time window boundaries)
AvailabilitySetChanged,
/// Warning threshold reached /// Warning threshold reached
Warning { Warning {
session_id: SessionId, session_id: SessionId,

View file

@ -178,7 +178,7 @@ impl Service {
let events = { let events = {
let mut engine = engine.lock().await; let mut engine = engine.lock().await;
engine.tick(now_mono) engine.tick(now_mono, now)
}; };
for event in events { for event in events {
@ -308,6 +308,15 @@ impl Service {
enabled: *enabled, enabled: *enabled,
})); }));
} }
CoreEvent::AvailabilitySetChanged => {
// Time-based availability change - broadcast updated state
let state = {
let engine = engine.lock().await;
engine.get_state()
};
ipc.broadcast_event(Event::new(EventPayload::StateChanged(state)));
}
} }
} }