huskies: merge 1061

This commit is contained in:
dave
2026-05-14 20:08:09 +00:00
parent 54d9737428
commit 5678f2a556
11 changed files with 752 additions and 82 deletions
+113
View File
@@ -107,6 +107,115 @@ pub(crate) async fn open_project_root(
}
}
/// One-shot migration: read existing JSON store files and insert their rows
/// into the SQLite tables created by the migration scripts, then rename the
/// originals to `*.migrated` so this function is a no-op on the next startup.
///
/// Runs after `db::init()` so the tables already exist and the shared pool is
/// available. Errors are logged but never fatal.
async fn migrate_json_stores_to_sqlite(huskies_dir: &Path) {
let pool = match crate::db::get_shared_pool() {
Some(p) => p,
None => {
crate::slog!("[db-migrate] Shared pool not available; skipping JSON migration");
return;
}
};
// ── event_triggers.json ───────────────────────────────────────────────────
let et_path = huskies_dir.join("event_triggers.json");
if et_path.exists() {
match std::fs::read_to_string(&et_path) {
Ok(s) => {
let triggers: Vec<crate::service::event_triggers::EventTrigger> =
serde_json::from_str(&s).unwrap_or_default();
for t in triggers {
let predicate_json = serde_json::to_string(&t.predicate).unwrap_or_default();
let action_json = serde_json::to_string(&t.action).unwrap_or_default();
let mode = match t.mode {
crate::service::event_triggers::FireMode::Once => "once",
crate::service::event_triggers::FireMode::Persistent => "persistent",
};
let created_at = t.created_at.to_rfc3339();
let _ = sqlx::query(
"INSERT OR IGNORE INTO event_triggers \
(id, predicate_json, action_json, mode, created_at) \
VALUES (?1, ?2, ?3, ?4, ?5)",
)
.bind(&t.id)
.bind(&predicate_json)
.bind(&action_json)
.bind(mode)
.bind(&created_at)
.execute(pool)
.await;
}
let migrated = huskies_dir.join("event_triggers.json.migrated");
let _ = std::fs::rename(&et_path, &migrated);
crate::slog!("[db-migrate] Migrated event_triggers.json → SQLite");
}
Err(e) => crate::slog!("[db-migrate] Could not read event_triggers.json: {e}"),
}
}
// ── timers.json ───────────────────────────────────────────────────────────
let timers_path = huskies_dir.join("timers.json");
if timers_path.exists() {
match std::fs::read_to_string(&timers_path) {
Ok(s) => {
let entries: Vec<crate::service::timer::TimerEntry> =
serde_json::from_str(&s).unwrap_or_default();
for e in entries {
let _ = sqlx::query(
"INSERT OR IGNORE INTO timers (story_id, scheduled_at) \
VALUES (?1, ?2)",
)
.bind(&e.story_id)
.bind(e.scheduled_at.to_rfc3339())
.execute(pool)
.await;
}
let migrated = huskies_dir.join("timers.json.migrated");
let _ = std::fs::rename(&timers_path, &migrated);
crate::slog!("[db-migrate] Migrated timers.json → SQLite");
}
Err(e) => crate::slog!("[db-migrate] Could not read timers.json: {e}"),
}
}
// ── scheduled_timers.json ─────────────────────────────────────────────────
let st_path = huskies_dir.join("scheduled_timers.json");
if st_path.exists() {
match std::fs::read_to_string(&st_path) {
Ok(s) => {
use crate::service::timer::scheduled::ScheduledTimer;
let entries: Vec<ScheduledTimer> = serde_json::from_str(&s).unwrap_or_default();
for t in entries {
let action_json = serde_json::to_string(&t.action).unwrap_or_default();
let mode_json = serde_json::to_string(&t.mode).unwrap_or_default();
let _ = sqlx::query(
"INSERT OR IGNORE INTO scheduled_timers \
(id, label, fire_at, action_json, mode_json, created_at) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
)
.bind(&t.id)
.bind(&t.label)
.bind(t.fire_at.to_rfc3339())
.bind(&action_json)
.bind(&mode_json)
.bind(t.created_at.to_rfc3339())
.execute(pool)
.await;
}
let migrated = huskies_dir.join("scheduled_timers.json.migrated");
let _ = std::fs::rename(&st_path, &migrated);
crate::slog!("[db-migrate] Migrated scheduled_timers.json → SQLite");
}
Err(e) => crate::slog!("[db-migrate] Could not read scheduled_timers.json: {e}"),
}
}
}
/// Set up the server log file, node identity keypair, pipeline DB, and CRDT state.
pub(crate) async fn init_subsystems(app_state: &Arc<SessionState>, cwd: &Path) {
// Enable persistent server log file now that the project root is known.
@@ -146,6 +255,10 @@ pub(crate) async fn init_subsystems(app_state: &Arc<SessionState>, cwd: &Path) {
if let Some(ref db_path) = pipeline_db_path {
if let Err(e) = db::init(db_path).await {
crate::slog!("[db] Failed to initialise pipeline.db: {e}");
} else {
// One-shot migration: move any existing JSON store files into SQLite.
let huskies_dir = db_path.parent().unwrap_or(db_path);
migrate_json_stores_to_sqlite(huskies_dir).await;
}
if let Err(e) = crdt_state::init(db_path).await {
crate::slog!("[crdt] Failed to initialise CRDT state layer: {e}");