Files
radiantwave/src/schedule/schedule_manager.cpp
T
NeilClack 509c925480 Kiosk: schedule client (conditional GET + atomic cache) and engine
Adds api_client support for GET /licenses/{key}/schedule with If-None-Match
and target-payload parsing (version/type/daily/weekly + legacy blocks), an
atomic on-disk schedule cache, a pure schedule engine (half-open blocks,
midnight self-wrap for daily, Sunday->Monday spillover for weekly, invalid
blocks skipped with a warning), and a background ScheduleManager that loads
the cache offline and pulls on boot and every 5 minutes. Adds engine/cache
tests against the shared docs/fixtures, including 304, corrupt cache, and
offline cases.
2026-09-29 10:34:40 -05:00

161 lines
4.4 KiB
C++

#include "schedule/schedule_manager.h"
#include "db/db_keys.h"
#include "network/api_client.h"
#include "network/schedule_cache.h"
#include <utility>
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<std::mutex> lock(mutex_);
return payload_.hasSchedule();
}
bool ScheduleManager::enabled() const {
std::lock_guard<std::mutex> lock(mutex_);
return enabled_;
}
Decision ScheduleManager::activeBlock(int minutes, Weekday weekday) const {
std::lock_guard<std::mutex> lock(mutex_);
if (!enabled_) {
return Decision{};
}
return evaluate(payload_, minutes, weekday, nullptr);
}
void ScheduleManager::setFetchForTesting(FetchFn fetch) {
std::lock_guard<std::mutex> lock(mutex_);
fetch_ = std::move(fetch);
}
void ScheduleManager::applyPayload(api::SchedulePayload payload, const std::string& etag) {
{
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<void()> onChanged) {
std::lock_guard<std::mutex> 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<std::mutex> lock(mutex_);
if (!started_) return;
stop_.store(true);
}
cv_.notify_one();
if (worker_.joinable()) {
worker_.join();
}
std::lock_guard<std::mutex> lock(mutex_);
started_ = false;
}
void ScheduleManager::requestRefresh() {
{
std::lock_guard<std::mutex> 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<std::mutex> lock(mutex_);
enabled_ = enabled;
}
void ScheduleManager::setPayloadForTesting(api::SchedulePayload payload,
const std::string& etag) {
std::lock_guard<std::mutex> lock(mutex_);
payload_ = std::move(payload);
etag_ = etag;
}
void ScheduleManager::run() {
while (true) {
{
std::unique_lock<std::mutex> 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