hive: remove the read-only schedules page and the old schedule store
Scheduled prompts live at swarm level (swarm-controller's /api/schedules, `swarmctl schedule`, the swarm UI's /schedules page). The hive dashboard's SCH3DUL3S tab only listed the rows left in hive-c0re's `scheduled_prompts` table so operators could copy them over; the operator confirmed everything is migrated. Removed: - the SCH3DUL3S tab, its pane, schedules.js, its CSS (including the `.rqe-source*` rules, whose only emitter was schedules.js) and the hub tile's "schedules" entry - hive-c0re's GET /api/schedules and the `scheduled_prompts` store (with its v1 column migration and test) - hive-sh4re's `schedule` wire types, which only that route used - docs describing the tab, the route and the two tables The rebuild-queue cancel route that shared the module moves to dashboard/rebuild_queue.rs under an OpenAPI tag of its own. The two sqlite tables are not dropped: nothing opens them any more, and persistence.md says they can be dropped by hand. Closes #4916
This commit is contained in:
parent
2f05e2db28
commit
78412b3d3b
22 changed files with 84 additions and 979 deletions
|
|
@ -1,10 +1,8 @@
|
|||
//! Sqlite-backed host-side stores (broker, read-only scheduled prompts,
|
||||
//! build logs, power intent) plus the shared connection open/migration
|
||||
//! helper (`db`). Each submodule is re-exported at the crate root, so
|
||||
//! Sqlite-backed host-side stores (broker, build logs, power intent)
|
||||
//! plus the shared connection open/migration helper (`db`). Each submodule is re-exported at the crate root, so
|
||||
//! `crate::broker::…` etc. keep working unchanged.
|
||||
|
||||
pub mod broker;
|
||||
pub mod build_logs;
|
||||
pub mod db;
|
||||
pub mod power;
|
||||
pub mod scheduled_prompts;
|
||||
|
|
|
|||
|
|
@ -5,9 +5,8 @@
|
|||
//!
|
||||
//! Stored as the `agent_power` table in the coordinator DB
|
||||
//! (`/var/lib/hyperhive/db/broker.sqlite`, one tiny row per agent) —
|
||||
//! same one-file-many-modules pattern as `broker` /
|
||||
//! `scheduled_prompts`, each with its own connection. Intent persists
|
||||
//! across hive-c0re restarts; in-flight
|
||||
//! same one-file-many-modules pattern as `broker`, each with its own
|
||||
//! connection. Intent persists across hive-c0re restarts; in-flight
|
||||
//! queue work deliberately does not. Setting `wanted` is never a
|
||||
//! queued node: operator/intent actions update the row synchronously
|
||||
//! at request time, then submit the DAG whose terminal `Reconcile`
|
||||
|
|
@ -72,7 +71,7 @@ pub struct PowerStore {
|
|||
impl PowerStore {
|
||||
/// Open (a connection to) the shared coordinator DB and ensure the
|
||||
/// `agent_power` table exists. `db_path` is the same sqlite file
|
||||
/// the broker / scheduled-prompts stores open.
|
||||
/// the broker store opens.
|
||||
pub fn open(db_path: &Path) -> Result<Self> {
|
||||
let conn = crate::db::open(db_path, "agent_power")?;
|
||||
conn.execute_batch(SCHEMA)
|
||||
|
|
|
|||
|
|
@ -1,225 +0,0 @@
|
|||
//! Scheduled prompts left in the hive's sqlite: the rows the dashboard's
|
||||
//! read-only schedule list shows so the operator can recreate them with
|
||||
//! `swarmctl schedule` (`docs/swarm/schedules.md`). Nothing on the hive
|
||||
//! writes or fires these rows.
|
||||
//!
|
||||
//! Schema: `docs/agent-lifecycle/persistence.md::/var/lib/hyperhive/db/broker.sqlite`
|
||||
//! (the `scheduled_prompts` / `scheduled_prompt_targets` table bullets).
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
use rusqlite::{Connection, params};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::db::Migration;
|
||||
|
||||
const SCHEMA: &str = r"
|
||||
CREATE TABLE IF NOT EXISTS scheduled_prompts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
owner TEXT NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
interval_seconds INTEGER,
|
||||
next_fire_at_unix INTEGER NOT NULL,
|
||||
created_at_unix INTEGER NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
cancelled_at_unix INTEGER,
|
||||
description TEXT,
|
||||
paused_at_unix INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scheduled_prompt_targets (
|
||||
schedule_id INTEGER NOT NULL,
|
||||
target TEXT NOT NULL,
|
||||
cancelled_at_unix INTEGER,
|
||||
last_fired_at_unix INTEGER,
|
||||
last_result TEXT,
|
||||
PRIMARY KEY (schedule_id, target),
|
||||
FOREIGN KEY (schedule_id) REFERENCES scheduled_prompts(id) ON DELETE CASCADE
|
||||
);
|
||||
";
|
||||
|
||||
/// One scheduled-prompt row + its target set, as `list` returns it.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Schedule {
|
||||
pub id: i64,
|
||||
/// `"operator"` or the name of the agent whose approved request
|
||||
/// created the row.
|
||||
pub owner: String,
|
||||
pub body: String,
|
||||
/// `None` = one-shot. `Some(n)` = recurring every `n` seconds.
|
||||
pub interval_seconds: Option<u64>,
|
||||
pub next_fire_at_unix: DateTime<Utc>,
|
||||
pub created_at_unix: DateTime<Utc>,
|
||||
pub source: ScheduleSource,
|
||||
/// Set when the whole schedule was cancelled.
|
||||
pub cancelled_at_unix: Option<DateTime<Utc>>,
|
||||
pub description: Option<String>,
|
||||
pub paused_at_unix: Option<DateTime<Utc>>,
|
||||
pub targets: Vec<ScheduleTarget>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ScheduleTarget {
|
||||
pub target: String,
|
||||
pub cancelled_at_unix: Option<DateTime<Utc>>,
|
||||
pub last_fired_at_unix: Option<DateTime<Utc>>,
|
||||
pub last_result: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum ScheduleSource {
|
||||
Operator,
|
||||
Approval { id: i64 },
|
||||
}
|
||||
|
||||
impl ScheduleSource {
|
||||
fn from_db_string(s: &str) -> Self {
|
||||
if let Some(rest) = s.strip_prefix("approval:")
|
||||
&& let Ok(id) = rest.parse::<i64>()
|
||||
{
|
||||
return ScheduleSource::Approval { id };
|
||||
}
|
||||
// Unknown / "operator" / corrupt → operator (best the row
|
||||
// can do without an unparseable-source variant).
|
||||
ScheduleSource::Operator
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ScheduledPrompts {
|
||||
conn: Mutex<Connection>,
|
||||
}
|
||||
|
||||
impl ScheduledPrompts {
|
||||
pub fn open(path: &Path) -> Result<Self> {
|
||||
let conn = crate::db::open(path, "scheduled_prompts")?;
|
||||
conn.execute_batch(SCHEMA)
|
||||
.context("apply scheduled_prompts schema")?;
|
||||
// The per-migration `adds_column` guard skips the ALTER on a legacy
|
||||
// DB that already has the column.
|
||||
crate::db::apply_versioned_migrations(
|
||||
&conn,
|
||||
"scheduled_prompts",
|
||||
&[
|
||||
// v1: add paused_at_unix.
|
||||
Migration {
|
||||
sql: "ALTER TABLE scheduled_prompts ADD COLUMN paused_at_unix INTEGER",
|
||||
adds_column: Some(("scheduled_prompts", "paused_at_unix")),
|
||||
},
|
||||
],
|
||||
)?;
|
||||
Ok(Self {
|
||||
conn: Mutex::new(conn),
|
||||
})
|
||||
}
|
||||
|
||||
/// Every row, cancelled ones included, ordered by next fire time.
|
||||
pub fn list(&self) -> Result<Vec<Schedule>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, owner, body, interval_seconds, next_fire_at_unix,
|
||||
created_at_unix, source, cancelled_at_unix, description,
|
||||
paused_at_unix
|
||||
FROM scheduled_prompts
|
||||
ORDER BY next_fire_at_unix ASC, id ASC",
|
||||
)?;
|
||||
let rows = stmt.query_map([], row_to_schedule_header)?;
|
||||
let mut out = Vec::new();
|
||||
for row in rows {
|
||||
let mut s = row?;
|
||||
s.targets = load_targets(&conn, s.id)?;
|
||||
out.push(s);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
fn row_to_schedule_header(row: &rusqlite::Row) -> rusqlite::Result<Schedule> {
|
||||
let interval: Option<i64> = row.get(3)?;
|
||||
let source_str: String = row.get(6)?;
|
||||
Ok(Schedule {
|
||||
id: row.get(0)?,
|
||||
owner: row.get(1)?,
|
||||
body: row.get(2)?,
|
||||
interval_seconds: interval.and_then(|i| u64::try_from(i).ok()),
|
||||
next_fire_at_unix: hive_sh4re::wire_time::from_secs(row.get(4)?),
|
||||
created_at_unix: hive_sh4re::wire_time::from_secs(row.get(5)?),
|
||||
source: ScheduleSource::from_db_string(&source_str),
|
||||
cancelled_at_unix: row
|
||||
.get::<_, Option<i64>>(7)?
|
||||
.map(hive_sh4re::wire_time::from_secs),
|
||||
description: row.get(8)?,
|
||||
paused_at_unix: row
|
||||
.get::<_, Option<i64>>(9)?
|
||||
.map(hive_sh4re::wire_time::from_secs),
|
||||
targets: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn load_targets(conn: &Connection, schedule_id: i64) -> Result<Vec<ScheduleTarget>> {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT target, cancelled_at_unix, last_fired_at_unix, last_result
|
||||
FROM scheduled_prompt_targets
|
||||
WHERE schedule_id = ?1
|
||||
ORDER BY target ASC",
|
||||
)?;
|
||||
let rows = stmt.query_map(params![schedule_id], |row| {
|
||||
Ok(ScheduleTarget {
|
||||
target: row.get(0)?,
|
||||
cancelled_at_unix: row
|
||||
.get::<_, Option<i64>>(1)?
|
||||
.map(hive_sh4re::wire_time::from_secs),
|
||||
last_fired_at_unix: row
|
||||
.get::<_, Option<i64>>(2)?
|
||||
.map(hive_sh4re::wire_time::from_secs),
|
||||
last_result: row.get(3)?,
|
||||
})
|
||||
})?;
|
||||
let mut out = Vec::new();
|
||||
for row in rows {
|
||||
out.push(row?);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn list_returns_rows_with_their_targets() {
|
||||
let dir = TempDir::new().expect("tempdir");
|
||||
let path = dir.path().join("schedules.sqlite");
|
||||
let db = ScheduledPrompts::open(&path).expect("open");
|
||||
let raw = Connection::open(&path).expect("raw open");
|
||||
raw.execute_batch(
|
||||
"INSERT INTO scheduled_prompts
|
||||
(id, owner, body, interval_seconds, next_fire_at_unix,
|
||||
created_at_unix, source, description)
|
||||
VALUES (1, 'operator', 'later', NULL, 200, 10, 'operator', NULL),
|
||||
(2, 'iris', 'hourly', 3600, 100, 10, 'approval:7', 'tick');
|
||||
INSERT INTO scheduled_prompt_targets (schedule_id, target, cancelled_at_unix)
|
||||
VALUES (1, 'bob', NULL), (2, 'iris', NULL), (2, 'alice', 50);",
|
||||
)
|
||||
.expect("seed");
|
||||
|
||||
let rows = db.list().expect("list");
|
||||
assert_eq!(rows.iter().map(|s| s.id).collect::<Vec<_>>(), vec![2, 1]);
|
||||
let hourly = &rows[0];
|
||||
assert_eq!(hourly.interval_seconds, Some(3600));
|
||||
assert_eq!(hourly.source, ScheduleSource::Approval { id: 7 });
|
||||
assert_eq!(hourly.description.as_deref(), Some("tick"));
|
||||
let targets: Vec<(&str, bool)> = hourly
|
||||
.targets
|
||||
.iter()
|
||||
.map(|t| (t.target.as_str(), t.cancelled_at_unix.is_some()))
|
||||
.collect();
|
||||
assert_eq!(targets, vec![("alice", true), ("iris", false)]);
|
||||
assert_eq!(rows[1].source, ScheduleSource::Operator);
|
||||
assert_eq!(rows[1].interval_seconds, None);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue