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.
This commit is contained in:
@@ -23,6 +23,7 @@ constexpr const char* PATH_SCHEDULE = "/schedule";
|
||||
struct HttpResponse {
|
||||
long status = 0; // HTTP status code; 0 if the request never completed
|
||||
std::string body; // raw response body (any status)
|
||||
std::string etag; // response ETag header, if any
|
||||
bool transportOk = false; // true when the request completed without a transport error
|
||||
};
|
||||
|
||||
@@ -31,6 +32,9 @@ HttpResponse httpGet(const std::string& path);
|
||||
HttpResponse httpPost(const std::string& path, const std::string& jsonBody);
|
||||
HttpResponse httpPut(const std::string& path, const std::string& jsonBody);
|
||||
|
||||
// GET with an optional If-None-Match header. Captures the response ETag.
|
||||
HttpResponse httpGetConditional(const std::string& path, const std::string& ifNoneMatch);
|
||||
|
||||
// ─── GET /frequencies ──────────────────────────────────────────────────
|
||||
|
||||
struct Frequency {
|
||||
@@ -176,4 +180,54 @@ bool parseScheduleJson(const std::string& body, ScheduleResponse& out);
|
||||
bool fetchSchedule(const std::string& email, const std::string& license_key,
|
||||
ScheduleResponse& out);
|
||||
|
||||
// ─── GET /licenses/{key}/schedule (Phase 3, conditional) ───────────────
|
||||
|
||||
// One Block of the target payload. `day` is "" for daily schedules and
|
||||
// "mon".."sun" for weekly schedules. Times are wall-clock minutes.
|
||||
struct TargetBlock {
|
||||
std::string id;
|
||||
std::string day; // "" for daily
|
||||
int start_minutes = 0;
|
||||
int end_minutes = 0;
|
||||
bool overnight = false;
|
||||
int duration_minutes = 0;
|
||||
bool dynamic_audio = true;
|
||||
std::vector<std::string> proclamation_ids;
|
||||
};
|
||||
|
||||
// The target schedule payload (version/type/daily/weekly).
|
||||
struct SchedulePayload {
|
||||
std::string version;
|
||||
// "daily", "weekly", or "" for no schedule (type null).
|
||||
std::string type;
|
||||
std::vector<TargetBlock> daily;
|
||||
// Weekly blocks keyed by day (mon..sun); empty for daily/none.
|
||||
std::vector<TargetBlock> weekly;
|
||||
// Legacy `blocks` flattening carried for backward compatibility.
|
||||
std::vector<ScheduleBlock> legacy_blocks;
|
||||
|
||||
bool hasSchedule() const { return type == "daily" || type == "weekly"; }
|
||||
};
|
||||
|
||||
// Parse the target payload (with version/type/daily/weekly). `blocks` (legacy)
|
||||
// is also read so older responses still populate `legacy_blocks`. Returns false
|
||||
// on malformed input.
|
||||
bool parseSchedulePayloadJson(const std::string& body, SchedulePayload& out);
|
||||
|
||||
// Result of a conditional schedule fetch.
|
||||
enum class ScheduleFetchStatus {
|
||||
Ok, // 200; payload replaced
|
||||
NotModified, // 304; caller keeps its cached payload
|
||||
NotFound, // 404 unknown license
|
||||
NetworkError, // transport/other failure
|
||||
};
|
||||
|
||||
// Fetch a License Key's schedule with conditional GET. When `etag` is
|
||||
// non-empty it is sent as If-None-Match. On success `out` is replaced and
|
||||
// `outEtag` receives the response ETag. Never throws.
|
||||
ScheduleFetchStatus fetchScheduleForLicense(const std::string& license_key,
|
||||
const std::string& etag,
|
||||
SchedulePayload& out,
|
||||
std::string& outEtag);
|
||||
|
||||
} // namespace radiantwave::api
|
||||
|
||||
Reference in New Issue
Block a user