Refresh activity list when change in time modifies it
This commit is contained in:
parent
8e754d7c77
commit
a17fb5104d
3 changed files with 55 additions and 14 deletions
|
|
@ -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 { .. }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue