Watch
0
0
Fork
You've already forked hyperhive
0

docs: notification flows, their API and the notifications page

This commit is contained in:
atlas 2026-10-24 04:05:05 +02:00 • committed by mara
commit 7795da44f8
9 changed files with 140 additions and 59 deletions

View file

@ -82,6 +82,11 @@ The swarm's control plane. The swarm UI and `swarmctl` are its clients.
`review-classifier-settings` KV bucket, which this daemon alone reads and
writes (`GET`/`PUT /api/review-classifier/settings`) →
[`docs/swarm/review-classifier.md`](../docs/swarm/review-classifier.md#swarm-level-settings).
- **Notification flows** — which of the notification router's built-in flows are
switched off, and the custom flows, as one document in the `notify-flows`
KV bucket, which this daemon alone writes (`GET`/`PUT /api/notify/flows`);
the router reads it →
[`docs/swarm/notify-router.md`](../docs/swarm/notify-router.md#flows).
- **Operator profiles** — each operator's name and the pronouns on their
forge profile, for every member of authelia's operator group, as one list
under the one key of the `operator-profiles` KV bucket. This daemon alone

View file

@ -201,8 +201,7 @@ pub(crate) async fn put_flows(
let (current, revision) = stored(&store).await.map_err(|e| queue_problem(&e))?;
// An unreadable document is replaced only when it is not a version
// this build does not know: a newer build's document is kept for it.
if let Stored::Unreadable(why @ DecodeError::UnknownVersion(_)) = &current
{
if let Stored::Unreadable(why @ DecodeError::UnknownVersion(_)) = &current {
return Err(unreadable(why));
}
if put.revision.is_some() && put.revision != revision {
@ -269,16 +268,12 @@ mod tests {
}
}
fn body(
revision: Option<u64>,
disabled: &[&str],
flows: Vec<Flow>,
) -> Result<Json<PutFlows>, JsonRejection> {
Ok(Json(PutFlows {
fn body(revision: Option<u64>, disabled: &[&str], flows: Vec<Flow>) -> Json<PutFlows> {
Json(PutFlows {
revision,
disabled_builtins: disabled.iter().map(|s| (*s).to_owned()).collect(),
flows,
}))
})
}
#[tokio::test]
@ -295,7 +290,7 @@ mod tests {
let Json(saved) = put_flows(
State(state.clone()),
body(None, &["unassigned"], vec![custom("triage")]),
Ok(body(None, &["unassigned"], vec![custom("triage")])),
)
.await
.expect("put");
@ -307,18 +302,22 @@ mod tests {
// The stored bytes are the document the router decodes.
let store = open(&client).await.expect("bucket");
let bytes = store.get(notify_flows::KEY).await.expect("read").expect("stored");
let bytes = store
.get(notify_flows::KEY)
.await
.expect("read")
.expect("stored");
let doc = Document::decode(&bytes).expect("the router reads it");
assert_eq!(doc.active().0.len(), 3, "2 built-ins and the custom flow");
// A refused save changes nothing: a flow with a cycle-free orphan.
// A refused save changes nothing: an orphan node, and a built-in that does not exist.
let mut orphan = custom("orphan");
orphan.edges.clear();
let refused = put_flows(State(state.clone()), body(None, &[], vec![orphan]))
let refused = put_flows(State(state.clone()), Ok(body(None, &[], vec![orphan])))
.await
.expect_err("an orphan node");
assert_eq!(refused.status, Some(StatusCode::BAD_REQUEST), "{refused:?}");
let unknown = put_flows(State(state.clone()), body(None, &["nope"], vec![]))
let unknown = put_flows(State(state.clone()), Ok(body(None, &["nope"], vec![])))
.await
.expect_err("no such built-in");
assert_eq!(unknown.status, Some(StatusCode::BAD_REQUEST), "{unknown:?}");
@ -326,14 +325,14 @@ mod tests {
assert_eq!(after, saved);
// A stale revision is refused; the current one lands.
let stale = put_flows(
let outdated = put_flows(
State(state.clone()),
body(Some(revision + 100), &[], vec![]),
Ok(body(Some(revision + 100), &[], vec![])),
)
.await
.expect_err("stale");
assert_eq!(stale.status, Some(StatusCode::CONFLICT), "{stale:?}");
let Json(next) = put_flows(State(state.clone()), body(Some(revision), &[], vec![]))
assert_eq!(outdated.status, Some(StatusCode::CONFLICT), "{outdated:?}");
let Json(next) = put_flows(State(state.clone()), Ok(body(Some(revision), &[], vec![])))
.await
.expect("current revision");
assert!(next.revision > Some(revision));
@ -350,16 +349,25 @@ mod tests {
let store = open(&client).await.expect("bucket");
store
.put(notify_flows::KEY, br#"{"version":2,"flows":[]}"#.to_vec().into())
.put(
notify_flows::KEY,
br#"{"version":2,"flows":[]}"#.to_vec().into(),
)
.await
.expect("seed");
let got = get_flows(State(state.clone())).await.expect_err("version 2");
let got = get_flows(State(state.clone()))
.await
.expect_err("version 2");
assert_eq!(got.status, Some(StatusCode::CONFLICT), "{got:?}");
let refused = put_flows(State(state.clone()), body(None, &[], vec![]))
let refused = put_flows(State(state.clone()), Ok(body(None, &[], vec![])))
.await
.expect_err("version 2 is kept");
assert_eq!(refused.status, Some(StatusCode::CONFLICT), "{refused:?}");
let kept = store.get(notify_flows::KEY).await.expect("read").expect("stored");
let kept = store
.get(notify_flows::KEY)
.await
.expect("read")
.expect("stored");
assert_eq!(&kept[..], br#"{"version":2,"flows":[]}"#);
// Control: garbage that names no version is replaced by a save.
@ -367,7 +375,7 @@ mod tests {
.put(notify_flows::KEY, b"garbage".to_vec().into())
.await
.expect("seed");
let _ = put_flows(State(state.clone()), body(None, &["closed"], vec![]))
let _ = put_flows(State(state.clone()), Ok(body(None, &["closed"], vec![])))
.await
.expect("replaces garbage");
let Json(view) = get_flows(State(state)).await.expect("readable again");

View file

@ -31,8 +31,8 @@ use crate::{
agent_control, agent_icon, agent_input, agent_limits, agent_login, agent_permissions,
agent_renewal, agent_state_stream, agent_status, agents_md, alerts, archetypes, auth, builder,
claude_login, config_pr, forge, forge_account, github_account, inference, issue_report,
linked_accounts, matrix_account, message_stream, notify_flows, pulls, queue_identity, review_classifier,
schedules, status, subagent_term, term_stream, wanted, webhook,
linked_accounts, matrix_account, message_stream, notify_flows, pulls, queue_identity,
review_classifier, schedules, status, subagent_term, term_stream, wanted, webhook,
};
/// Where the daemon binds, overridable via `SWARM_CONTROLLER_SOCKET`.