#include "schedule/schedule_manager.h" #include "db/db_keys.h" #include "network/api_client.h" #include "network/schedule_cache.h" #include namespace radiantwave::schedule { ScheduleManager::ScheduleManager() : fetch_([](const std::string& licenseKey, const std::string& etag, api::SchedulePayload& out, std::string& outEtag) { return api::fetchScheduleForLicense(licenseKey, etag, out, outEtag); }) { } ScheduleManager::~ScheduleManager() { shutdown(); } bool ScheduleManager::hasSchedule() const { std::lock_guard lock(mutex_); return payload_.hasSchedule(); } bool ScheduleManager::enabled() const { std::lock_guard lock(mutex_); return enabled_; } Decision ScheduleManager::activeBlock(int minutes, Weekday weekday) const { std::lock_guard lock(mutex_); if (!enabled_) { return Decision{}; } return evaluate(payload_, minutes, weekday, nullptr); } void ScheduleManager::setFetchForTesting(FetchFn fetch) { std::lock_guard lock(mutex_); fetch_ = std::move(fetch); } void ScheduleManager::applyPayload(api::SchedulePayload payload, const std::string& etag) { { std::lock_guard lock(mutex_); payload_ = std::move(payload); etag_ = etag; } // Cache the last good payload for offline use (atomic write). radiantwave::sync::schedule_cache::write(payload_, etag_); if (onChanged_) onChanged_(); } void ScheduleManager::loadCache() { api::SchedulePayload cached; std::string cachedEtag; if (radiantwave::sync::schedule_cache::read(cached, cachedEtag)) { std::lock_guard lock(mutex_); payload_ = std::move(cached); etag_ = std::move(cachedEtag); if (onChanged_) onChanged_(); } } void ScheduleManager::refreshOnce() { std::string licenseKey; std::string etag; FetchFn fetch; { std::lock_guard lock(mutex_); licenseKey = licenseKey_; etag = etag_; fetch = fetch_; } if (licenseKey.empty()) return; api::SchedulePayload payload; std::string newEtag; const api::ScheduleFetchStatus status = fetch(licenseKey, etag, payload, newEtag); if (status == api::ScheduleFetchStatus::Ok) { applyPayload(std::move(payload), newEtag); } // NotModified / NotFound / NetworkError: keep the current cache. } void ScheduleManager::start(db::DatabaseManager& db, const std::string& timezone, std::function onChanged) { std::lock_guard lock(mutex_); if (started_) return; started_ = true; db_ = &db; timezone_ = timezone; onChanged_ = std::move(onChanged); licenseKey_ = db.getKV(db::KV_LICENSE_KEY); enabled_ = true; // config supplies the toggle via setEnabled() // Offline first: load any cached payload immediately. loadCache(); stop_.store(false); worker_ = std::thread(&ScheduleManager::run, this); requested_.store(true); cv_.notify_one(); } void ScheduleManager::shutdown() { { std::lock_guard lock(mutex_); if (!started_) return; stop_.store(true); } cv_.notify_one(); if (worker_.joinable()) { worker_.join(); } std::lock_guard lock(mutex_); started_ = false; } void ScheduleManager::requestRefresh() { { std::lock_guard lock(mutex_); if (!started_) return; if (db_) { licenseKey_ = db_->getKV(db::KV_LICENSE_KEY); } requested_.store(true); } cv_.notify_one(); } void ScheduleManager::setEnabled(bool enabled) { std::lock_guard lock(mutex_); enabled_ = enabled; } void ScheduleManager::setPayloadForTesting(api::SchedulePayload payload, const std::string& etag) { std::lock_guard lock(mutex_); payload_ = std::move(payload); etag_ = etag; } void ScheduleManager::run() { while (true) { { std::unique_lock lock(mutex_); cv_.wait_for(lock, REFRESH_INTERVAL, [this] { return stop_.load() || requested_.load(); }); if (stop_.load()) return; requested_.store(false); } refreshOnce(); } } } // namespace radiantwave::schedule