Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions bin/hctl/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1162,6 +1162,101 @@ fn print_pretty(resp: &Response) {
if *activated { " (activated)" } else { "" }
);
}
Response::PackageList(rows) => {
for p in rows {
println!(
"{}\t{}\t{}\t{} feature(s)\t{} site(s){}",
p.id,
p.slug,
p.pretty_price(),
p.features.forced_count(),
p.active_count,
if p.enabled { "" } else { "\t(hidden)" }
);
}
}
Response::PackageGet(p) | Response::PackageCreate(p) | Response::PackageUpdate(p) => {
println!("id: {}", p.id);
println!("name: {}", p.name);
println!("slug: {}", p.slug);
println!("price: {}", p.pretty_price());
println!("offered: {}", if p.enabled { "yes" } else { "no" });
println!("in use: {} site(s)", p.active_count);
// "leave" is not "off" — print every feature so the operator can
// see which ones this package has no opinion about.
println!("features:");
println!(" wp_auto_update: {}", p.features.wp_auto_update);
println!(" integrity_scan: {}", p.features.integrity_scan);
println!(" monitoring: {}", p.features.monitoring);
println!(" hardening: {}", p.features.hardening);
println!(" backup_cadence: {}", p.features.backup_cadence);
}
Response::PackageDelete => {
println!("✓ package deleted (existing activations keep their price, unenforced)")
}
Response::PackageActivations(rows) => {
if rows.is_empty() {
println!("(no packages on this hosting)");
}
for a in rows {
println!(
"{}\t{}\t{}\t{}{}",
a.id,
a.state,
if a.package_name.is_empty() {
"(definition deleted)"
} else {
a.package_name.as_str()
},
a.pretty_price(),
match a.next_billing_at {
Some(ts) => format!("\tnext reminder: {ts}"),
None => String::new(),
}
);
}
}
Response::PackageActivate(a) => {
println!("✓ package active (activation {})", a.id);
println!(" price: {}", a.pretty_price());
if let Some(ts) = a.next_billing_at {
println!(" next reminder: {ts}");
}
}
Response::PackageCancel(a) => {
println!("✓ package cancelled (activation {} kept as history)", a.id);
}
Response::PackageEnforceTick { corrected } => {
if *corrected == 0 {
println!("✓ nothing had drifted");
} else {
println!("✓ re-asserted {corrected} paid feature(s) that had been switched off");
}
}
Response::CareReportPreview(m) | Response::CareReportSend(m) => {
// The body is the point — print it verbatim, because the whole
// reason preview exists is to read exactly what the customer
// gets. Everything else goes above it as a short header.
println!("period: {} → {}", m.period_start, m.period_end);
println!("cadence: {}", m.cadence);
println!(
"to: {}",
if m.to.is_empty() {
"(none — this site has no owner e-mail, so nothing can be sent)"
} else {
m.to.as_str()
}
);
if m.entirely_unmeasured {
println!(
"warning: not one metric could be measured — the scheduled send skips \
a report like this (is this the node that owns the site?)"
);
}
println!("subject: {}", m.subject);
println!();
print!("{}", m.body);
}
Response::HostingImportPanelPlan(plan) => {
println!(
"Import plan — source {} {} ({} site(s)):",
Expand Down
57 changes: 57 additions & 0 deletions bin/hyperion-agent/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,63 @@ async fn main() -> anyhow::Result<()> {
}
});
}
// Care-package drift enforcement — hourly. Re-asserts the features
// every ACTIVE package promises, so a paid capability someone switched
// off comes back instead of quietly staying off until the customer
// notices. Cheap when idle: it reads state and only writes where
// something actually moved, and returns immediately on a node with no
// activations at all.
{
let pk = svc.clone();
tokio::spawn(async move {
// 7-minute offset — clear of the vuln (4 min) and integrity
// (6 min) sweeps, which are the two ticks that hammer wp-cli.
tokio::time::sleep(std::time::Duration::from_secs(420)).await;
let mut interval = tokio::time::interval(std::time::Duration::from_secs(3600));
interval.tick().await;
loop {
match pk.package_enforce_tick().await {
Ok(n) if n > 0 => tracing::info!(corrected = n, "care package enforce tick"),
Ok(_) => tracing::debug!("care package enforce tick: nothing drifted"),
Err(e) => tracing::warn!(error=%e, "care package enforce tick failed"),
}
interval.tick().await;
}
});
}
// Care reports — hourly check of which LOCAL hostings are due their
// periodic report to the customer. The sibling of the drift tick and
// the other half of the same feature: that one keeps the paid work
// happening, this one is the only part the person paying ever sees.
//
// Runs on EVERY node, and must: every metric behind a report (bans,
// usage buckets, monitor samples, backup runs, the audit log, the
// stored integrity scan) lives on the node that owns the hosting. The
// same sweep on the master would truthfully report "not measured" for
// a worker's site — honest, and worthless.
//
// Hourly is the CHECK cadence, not the send cadence: the tick sends
// only once a full weekly/monthly/quarterly period has elapsed since
// the last report, and it returns immediately on a node with no
// activations at all.
{
let cr = svc.clone();
tokio::spawn(async move {
// 9-minute offset — clear of the package drift tick (7 min),
// which walks the same activation rows.
tokio::time::sleep(std::time::Duration::from_secs(540)).await;
let mut interval = tokio::time::interval(std::time::Duration::from_secs(3600));
interval.tick().await;
loop {
match cr.care_report_tick().await {
Ok(n) if n > 0 => tracing::info!(sent = n, "care report tick"),
Ok(_) => tracing::debug!("care report tick: nothing due"),
Err(e) => tracing::warn!(error=%e, "care report tick failed"),
}
interval.tick().await;
}
});
}
// Scheduled backups — checks hourly which LOCAL hostings are due per their
// backup_cadence (off by default) and runs backup_now for each. Runs on
// every node since backups are node-local.
Expand Down
1 change: 1 addition & 0 deletions bin/hyperion-web/src/handlers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub mod me;
pub mod migration;
pub mod monitoring;
pub mod notifications;
pub mod packages;
pub mod profile;
pub mod profiles;
pub mod roles;
Expand Down
Loading