feat: Complete Phase 1 backend services and WebSocket/NATS bridge
Implements all remaining backend infrastructure for Corrosion platform.
Backend Services (5 new):
- license.rs: License validation, activation, check-in with NATS token generation
- map_manager.rs: Map upload/rotation with SHA-256 checksums, circular advancement
- health_checker.rs: Post-wipe verification with retry loop and backoff
- backup_manager.rs: Tar.gz backups with retention policy (last 10), recursive upload
- scheduler.rs: Tokio-cron integration for scheduled wipes with NATS events
WipeEngine Orchestration (wipe_engine.rs):
- execute_wipe(): Master orchestrator managing full lifecycle
- execute_pre_wipe(): Countdown warnings, backups, player kicks
- execute_wipe_actions(): Map/plugin deletion, seed rotation, Steam updates
- execute_post_wipe_verification(): Health checks with restart attempts
- execute_rollback(): Failure recovery with backup restore
- JSONB execution logs, NATS status events, service composition pattern
WebSocket/NATS Bridge (ws.rs):
- JWT authentication via query parameter
- License-scoped NATS subscriptions (corrosion.{license_id}.*)
- Bi-directional: NATS→WebSocket event forwarding, WebSocket→NATS publishing
- Axum 0.8 with ws feature, auto Ping/Pong handling
Panel Adapter Fixes:
- AMP/Pterodactyl/Companion adapters fully wired
- RCON command execution, file operations, Steam update triggers
Fixes:
- Added ws feature to Axum dependency
- Fixed Message::Text() type conversions (String→Utf8Bytes via .into())
- Fixed BackupInfo FromRow derive
- Fixed recursive async with Box::pin pattern
- Fixed async JobScheduler::new() constructor
- Removed manual WebSocket Ping/Pong handler
Compilation: 0 errors, 327 warnings (unused vars/functions)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,25 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use reqwest::Client;
|
||||
use serde::Serialize;
|
||||
|
||||
const PUSHBULLET_API_URL: &str = "https://api.pushbullet.com/v2/pushes";
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct NotePush {
|
||||
#[serde(rename = "type")]
|
||||
push_type: String,
|
||||
title: String,
|
||||
body: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct LinkPush {
|
||||
#[serde(rename = "type")]
|
||||
push_type: String,
|
||||
title: String,
|
||||
body: String,
|
||||
url: String,
|
||||
}
|
||||
|
||||
/// Pushbullet notification service.
|
||||
///
|
||||
@@ -6,25 +27,122 @@ use anyhow::Result;
|
||||
/// API. Used as a secondary notification channel alongside Discord for
|
||||
/// critical alerts that need to reach admins on their mobile devices.
|
||||
pub struct PushbulletNotifier {
|
||||
// TODO: Add fields:
|
||||
// - api_key: String
|
||||
// - default_device_iden: Option<String> (target specific device, or all)
|
||||
http: Client,
|
||||
api_key: String,
|
||||
}
|
||||
|
||||
impl PushbulletNotifier {
|
||||
pub fn new(api_key: String) -> Self {
|
||||
Self {
|
||||
http: Client::new(),
|
||||
api_key,
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a text notification (note type push).
|
||||
pub async fn send_notification(&self, _title: &str, _body: &str) -> Result<()> {
|
||||
// TODO: POST to https://api.pushbullet.com/v2/pushes
|
||||
// TODO: Body: { type: "note", title, body }
|
||||
// TODO: Auth: Access-Token header with api_key
|
||||
todo!()
|
||||
pub async fn send_notification(&self, title: &str, body: &str) -> Result<()> {
|
||||
let payload = NotePush {
|
||||
push_type: "note".to_string(),
|
||||
title: title.to_string(),
|
||||
body: body.to_string(),
|
||||
};
|
||||
|
||||
let response = self
|
||||
.http
|
||||
.post(PUSHBULLET_API_URL)
|
||||
.header("Access-Token", &self.api_key)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send Pushbullet notification")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
tracing::error!("Pushbullet push failed: {} — {}", status, body);
|
||||
anyhow::bail!("Pushbullet returned {status}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a link notification with a clickable URL.
|
||||
pub async fn send_link(&self, _title: &str, _body: &str, _url: &str) -> Result<()> {
|
||||
// TODO: POST to https://api.pushbullet.com/v2/pushes
|
||||
// TODO: Body: { type: "link", title, body, url }
|
||||
// TODO: Auth: Access-Token header with api_key
|
||||
todo!()
|
||||
pub async fn send_link(&self, title: &str, body: &str, url: &str) -> Result<()> {
|
||||
let payload = LinkPush {
|
||||
push_type: "link".to_string(),
|
||||
title: title.to_string(),
|
||||
body: body.to_string(),
|
||||
url: url.to_string(),
|
||||
};
|
||||
|
||||
let response = self
|
||||
.http
|
||||
.post(PUSHBULLET_API_URL)
|
||||
.header("Access-Token", &self.api_key)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send Pushbullet link push")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
tracing::error!("Pushbullet push failed: {} — {}", status, body);
|
||||
anyhow::bail!("Pushbullet returned {status}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a wipe-starting notification.
|
||||
pub async fn send_wipe_start(&self, server_name: &str, wipe_type: &str) -> Result<()> {
|
||||
self.send_notification(
|
||||
&format!("🔄 {} — Wipe Starting", server_name),
|
||||
&format!("{} wipe is beginning. Server will be offline briefly.", wipe_type),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Send a wipe-completed notification.
|
||||
pub async fn send_wipe_complete(&self, server_name: &str, wipe_type: &str) -> Result<()> {
|
||||
self.send_notification(
|
||||
&format!("✅ {} — Wipe Complete", server_name),
|
||||
&format!("{} wipe completed. Server is back online.", wipe_type),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Send a wipe-failed notification.
|
||||
pub async fn send_wipe_failed(&self, server_name: &str, error: &str) -> Result<()> {
|
||||
self.send_notification(
|
||||
&format!("❌ {} — Wipe Failed", server_name),
|
||||
&format!("Wipe failed: {}", error),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Send a crash alert.
|
||||
pub async fn send_crash_alert(
|
||||
&self,
|
||||
server_name: &str,
|
||||
crash_count: u32,
|
||||
auto_recovered: bool,
|
||||
) -> Result<()> {
|
||||
let title = if auto_recovered {
|
||||
format!("⚠️ {} — Crash Recovered", server_name)
|
||||
} else {
|
||||
format!("🔴 {} — Crash — Manual Action Needed", server_name)
|
||||
};
|
||||
|
||||
let body = if auto_recovered {
|
||||
format!("Server crashed and was auto-restarted (attempt {}).", crash_count)
|
||||
} else {
|
||||
format!(
|
||||
"Server crashed {} times. Auto-recovery exhausted. Check the server.",
|
||||
crash_count
|
||||
)
|
||||
};
|
||||
|
||||
self.send_notification(&title, &body).await
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user