Add incoming webhooks, fix stale service worker and push re-registration

- Webhooks for automations: per-job secret, Gitea/GitHub HMAC signatures,
  payload appended to the instructions; settings on the automations page
- Service worker gets a new file name per build (proxies that cache *.js
  served a stale worker, which broke updates and Android installation);
  /sw.js is a kill switch that removes old workers
- Push: devices re-register their subscription with the server on start and
  before a test; the test reports why a push service rejected a message

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-09 00:38:04 +01:00
1 parent 6dffcb501a
commit 10d4b64289
16 files changed
+267 -18

No files matched your search

+15 -3
View File
@@ -161,6 +161,7 @@ impl Store {
("sessions", "owner_id", "ALTER TABLE sessions ADD COLUMN owner_id TEXT"),
("usage", "user_id", "ALTER TABLE usage ADD COLUMN user_id TEXT"),
("users", "monthly_quota", "ALTER TABLE users ADD COLUMN monthly_quota REAL"),
("jobs", "webhook_secret", "ALTER TABLE jobs ADD COLUMN webhook_secret TEXT"),
] {
let exists: bool = conn
.prepare(&format!("SELECT 1 FROM pragma_table_info('{table}') WHERE name = ?1"))?
@@ -975,13 +976,14 @@ impl Store {
last_session_id: r.get(5)?,
last_status: r.get(6)?,
created_at: r.get(7)?,
webhook: r.get::<_, Option<String>>(8)?.is_some(),
})
}
pub fn jobs(&self, owner: Option<&str>) -> Result<Vec<crate::jobs::Job>> {
let conn = self.conn.lock().unwrap();
let mut stmt = conn.prepare(
"SELECT id, owner_id, path, enabled, last_run, last_session_id, last_status, created_at FROM jobs
"SELECT id, owner_id, path, enabled, last_run, last_session_id, last_status, created_at, webhook_secret FROM jobs
WHERE ?1 IS NULL OR owner_id = ?1 ORDER BY created_at",
)?;
let rows = stmt.query_map([owner], Self::job_row)?;
@@ -991,7 +993,7 @@ impl Store {
pub fn job(&self, id: &str) -> Result<Option<crate::jobs::Job>> {
let conn = self.conn.lock().unwrap();
Ok(conn
.query_row("SELECT id, owner_id, path, enabled, last_run, last_session_id, last_status, created_at FROM jobs WHERE id = ?1", [id], Self::job_row)
.query_row("SELECT id, owner_id, path, enabled, last_run, last_session_id, last_status, created_at, webhook_secret FROM jobs WHERE id = ?1", [id], Self::job_row)
.optional()?)
}
@@ -1007,7 +1009,7 @@ impl Store {
}
let conn = self.conn.lock().unwrap();
Ok(conn.query_row(
"SELECT id, owner_id, path, enabled, last_run, last_session_id, last_status, created_at FROM jobs WHERE path = ?1",
"SELECT id, owner_id, path, enabled, last_run, last_session_id, last_status, created_at, webhook_secret FROM jobs WHERE path = ?1",
[path],
Self::job_row,
)?)
@@ -1039,6 +1041,16 @@ impl Store {
Ok(())
}
/// Set (Some) or remove (None) the webhook secret of a job.
pub fn job_set_webhook(&self, id: &str, secret: Option<&str>) -> Result<()> {
self.conn.lock().unwrap().execute("UPDATE jobs SET webhook_secret = ?2 WHERE id = ?1", rusqlite::params![id, secret])?;
Ok(())
}
pub fn job_webhook_secret(&self, id: &str) -> Result<Option<String>> {
Ok(self.conn.lock().unwrap().query_row("SELECT webhook_secret FROM jobs WHERE id = ?1", [id], |r| r.get(0)).optional()?.flatten())
}
pub fn job_delete(&self, id: &str) -> Result<()> {
self.conn.lock().unwrap().execute("DELETE FROM jobs WHERE id = ?1", [id])?;
Ok(())