One schedule for every charger, and a clock on the server to keep it
The charger's own cloud schedule is one window inside one box: charge between these hours, every day, and that is the whole vocabulary. A third tab on Charging holds a list instead — each line an action, a time, the days it repeats on and the chargers it acts on — and one list covers the whole account rather than each charger hiding its own. The clock is the server's. A schedule that only fires while a tab is open is a reminder, so a ticker sweeps every enabled task and fires whichever minute has come. It sends by handing a synthesised request to the same control endpoint the page's buttons use, so a scheduled command goes through the same cascade, ownership gate, rate limit and audit trail — what the owner cannot press by hand, the scheduler cannot send for them. A task names its chargers, or names none, which means all of them and keeps meaning that for a charger imported next year. Times are stored as a wall clock plus the zone they were written in, so 23:00 stays 23:00 wherever the server sits. One action per task: a charging window is the two tasks that open and close it, which is how it is read back, edited and switched off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8e4c22cfcf
commit
5a4515978f
@@ -462,6 +462,30 @@ const DESIRED = {
|
||||
// defined through the API, so it is declared here like the audit trail's.
|
||||
F.autodate("created", true, false),
|
||||
],
|
||||
// The home-charger scheduler: a user's own list of charging tasks, one list
|
||||
// covering every charger they own. The charger's own cloud schedule holds one
|
||||
// window per box; this holds as many tasks as they like, each naming its own
|
||||
// chargers, days and action. Run by the ticker in the API Server
|
||||
// (internal/api/chargingtasks_run.go).
|
||||
charging_tasks: [
|
||||
F.text("name", true),
|
||||
// The home_chargers rows this task acts on. A list of ids rather than a
|
||||
// relation because empty has to mean "every charger I own" — a standing wish
|
||||
// that keeps covering chargers imported later.
|
||||
F.json("chargers", 2000),
|
||||
F.select("action", ["start", "stop", "limit", "boost"], true),
|
||||
F.number("amps"), // the ceiling, for the "limit" action
|
||||
// 24-hour "HH:MM", read in the IANA zone the task was written in.
|
||||
F.text("time", true),
|
||||
F.text("zone"),
|
||||
F.json("days", 200), // 0=Sunday … 6=Saturday; empty means every day
|
||||
F.bool("enabled"),
|
||||
F.text("last_run"), // RFC3339, UTC — also the guard against a double firing
|
||||
F.text("last_result"),
|
||||
// Owner. Non-cascading, like a charger's.
|
||||
F.relation("owner", "users", false, false),
|
||||
F.autodate("created", true, false),
|
||||
],
|
||||
// Server-wide settings as a single record, keyed "global". Today it holds
|
||||
// pluginSettings: the top (L1) layer of the integration cascade — every
|
||||
// plugin's enable state, its global config, and the registration of any
|
||||
@@ -549,6 +573,11 @@ const INDEXES = {
|
||||
// A charger is looked up by its owner, and by serial when checking whether the
|
||||
// account it came from has already been imported.
|
||||
home_chargers: ["CREATE INDEX `idx_home_chargers_owner_serial` ON `home_chargers` (`owner`, `serial`)"],
|
||||
// The runner sweeps every enabled task on every tick, and the page reads one
|
||||
// owner's; both go through these two columns.
|
||||
charging_tasks: [
|
||||
"CREATE INDEX `idx_charging_tasks_owner_enabled` ON `charging_tasks` (`owner`, `enabled`)",
|
||||
],
|
||||
// Audit is queried "this charger's events, newest first" and "this user's events".
|
||||
control_audit: [
|
||||
"CREATE INDEX `idx_control_audit_serial_created` ON `control_audit` (`serial`, `created`)",
|
||||
@@ -586,6 +615,7 @@ async function main() {
|
||||
"reminders",
|
||||
"control_audit",
|
||||
"home_chargers",
|
||||
"charging_tasks",
|
||||
]) {
|
||||
if (collections.some((c) => c.name === name)) continue;
|
||||
await createCollection(token, name, DESIRED[name], format, idByName);
|
||||
@@ -614,6 +644,7 @@ async function main() {
|
||||
"reminders",
|
||||
"control_audit",
|
||||
"home_chargers",
|
||||
"charging_tasks",
|
||||
]) {
|
||||
await reconcileFields(token, name, DESIRED[name], format, idByName);
|
||||
}
|
||||
@@ -622,7 +653,8 @@ async function main() {
|
||||
"\nDone. Collections ready: app_settings, organizations, users, cars,\n" +
|
||||
"service_records,\n" +
|
||||
"technical_checks, parts, car_shares, fuel_entries, charging_sessions,\n" +
|
||||
"maintenance_entries, car_documents, reminders, control_audit, home_chargers.",
|
||||
"maintenance_entries, car_documents, reminders, control_audit, home_chargers,\n" +
|
||||
"charging_tasks.",
|
||||
);
|
||||
console.log(
|
||||
"Note: the legacy `sessions` collection is no longer used (auth moved to PocketBase\n" +
|
||||
|
||||
Reference in New Issue
Block a user