mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-30 04:09:26 -07:00
* add `auto_check_updates` config value * add `auto_check_updates` to dist config * add `Update` `NotificationType` * implement update checker and worker * add endpoint, add to documentation, add worker * clone update_status_lock Arc * fmt * add more tests * remove todo * add to docs * frontend update notice * improve name in documentation * add user-agent to update check request * add update check request timeout * openapi trait bound * do not enable `auto_check_updates` by default * remove redundant documentation * surface fetch of update status error * fail on version with pre-release for now, add additional test cases * Update configuration.md --------- Co-authored-by: Markus Unterwaditzer <markus-tarpit+git@unterwaditzer.net>
This commit is contained in:
@@ -60,6 +60,8 @@ pub struct Config {
|
||||
pub ntfy_url: Option<String>,
|
||||
/// Vector containing the types of enabled notifications
|
||||
pub enabled_notifications: Vec<NotificationType>,
|
||||
/// Whether Rayhunter should periodically check GitHub for new releases
|
||||
pub auto_check_updates: bool,
|
||||
/// Vector containing the list of enabled analyzers
|
||||
pub analyzers: AnalyzerConfig,
|
||||
/// Minimum disk space required to start a recording
|
||||
@@ -134,6 +136,7 @@ impl Default for Config {
|
||||
analyzers: AnalyzerConfig::default(),
|
||||
ntfy_url: None,
|
||||
enabled_notifications: vec![NotificationType::Warning, NotificationType::LowBattery],
|
||||
auto_check_updates: true,
|
||||
min_space_to_start_recording_mb: 1,
|
||||
min_space_to_continue_recording_mb: 1,
|
||||
gps_mode: GpsMode::Disabled,
|
||||
|
||||
@@ -12,6 +12,7 @@ pub mod pcap;
|
||||
pub mod qmdl_store;
|
||||
pub mod server;
|
||||
pub mod stats;
|
||||
pub mod update;
|
||||
pub mod webdav;
|
||||
|
||||
#[cfg(feature = "apidocs")]
|
||||
@@ -34,6 +35,7 @@ use utoipa::OpenApi;
|
||||
server::get_zip,
|
||||
stats::get_system_stats,
|
||||
stats::get_qmdl_manifest,
|
||||
stats::get_update_status,
|
||||
stats::get_log,
|
||||
diag::start_recording,
|
||||
diag::stop_recording,
|
||||
|
||||
@@ -12,6 +12,7 @@ mod pcap;
|
||||
mod qmdl_store;
|
||||
mod server;
|
||||
mod stats;
|
||||
mod update;
|
||||
mod webdav;
|
||||
|
||||
use std::net::SocketAddr;
|
||||
@@ -29,7 +30,8 @@ use crate::server::{
|
||||
ServerState, debug_set_display_state, get_config, get_qmdl, get_time, get_wifi_status, get_zip,
|
||||
scan_wifi, serve_static, set_config, set_time_offset, test_notification,
|
||||
};
|
||||
use crate::stats::{get_qmdl_manifest, get_system_stats};
|
||||
use crate::stats::{get_qmdl_manifest, get_system_stats, get_update_status};
|
||||
use crate::update::{UpdateStatus, run_update_check_worker};
|
||||
use crate::webdav::run_webdav_upload_worker;
|
||||
use wifi_station::WifiStatus;
|
||||
|
||||
@@ -63,6 +65,7 @@ fn get_router() -> AppRouter {
|
||||
.route("/api/qmdl/{name}", get(get_qmdl))
|
||||
.route("/api/zip/{name}", get(get_zip))
|
||||
.route("/api/system-stats", get(get_system_stats))
|
||||
.route("/api/update-status", get(get_update_status))
|
||||
.route("/api/qmdl-manifest", get(get_qmdl_manifest))
|
||||
.route("/api/log", get(get_log))
|
||||
.route("/api/start-recording", post(start_recording))
|
||||
@@ -217,6 +220,7 @@ async fn run_with_config(
|
||||
let _shutdown_guard = shutdown_token.clone().drop_guard();
|
||||
|
||||
let notification_service = NotificationService::new(config.ntfy_url.clone());
|
||||
let update_status_lock = Arc::new(RwLock::new(UpdateStatus::default()));
|
||||
|
||||
if !config.debug_mode {
|
||||
info!("Starting Diag Thread");
|
||||
@@ -258,6 +262,16 @@ async fn run_with_config(
|
||||
diag_tx.clone(),
|
||||
shutdown_token.clone(),
|
||||
);
|
||||
|
||||
if config.auto_check_updates {
|
||||
run_update_check_worker(
|
||||
&task_tracker,
|
||||
shutdown_token.clone(),
|
||||
update_status_lock.clone(),
|
||||
notification_service.new_handler(),
|
||||
config.enabled_notifications.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let analysis_status_lock = Arc::new(RwLock::new(analysis_status));
|
||||
@@ -339,6 +353,7 @@ async fn run_with_config(
|
||||
wifi_status,
|
||||
wifi_scan_lock: tokio::sync::Mutex::new(()),
|
||||
gps_state: Arc::new(tokio::sync::RwLock::new(initial_gps)),
|
||||
update_status_lock: update_status_lock.clone(),
|
||||
});
|
||||
run_server(&task_tracker, state, shutdown_token.clone()).await;
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ pub enum NotificationError {
|
||||
pub enum NotificationType {
|
||||
Warning,
|
||||
LowBattery,
|
||||
Update,
|
||||
}
|
||||
|
||||
pub struct Notification {
|
||||
|
||||
@@ -29,6 +29,7 @@ use crate::gps::GpsData;
|
||||
use crate::notifications::DEFAULT_NOTIFICATION_TIMEOUT;
|
||||
use crate::pcap::{generate_pcap_data, load_gps_records_for_entry};
|
||||
use crate::qmdl_store::RecordingStore;
|
||||
use crate::update::UpdateStatus;
|
||||
|
||||
pub struct ServerState {
|
||||
pub config_path: String,
|
||||
@@ -42,6 +43,7 @@ pub struct ServerState {
|
||||
pub wifi_status: Arc<RwLock<wifi_station::WifiStatus>>,
|
||||
pub wifi_scan_lock: tokio::sync::Mutex<()>,
|
||||
pub gps_state: Arc<RwLock<Option<GpsData>>>,
|
||||
pub update_status_lock: Arc<RwLock<UpdateStatus>>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
@@ -580,6 +582,7 @@ mod tests {
|
||||
wifi_status: Arc::new(RwLock::new(wifi_station::WifiStatus::default())),
|
||||
wifi_scan_lock: tokio::sync::Mutex::new(()),
|
||||
gps_state: Arc::new(RwLock::new(None)),
|
||||
update_status_lock: Arc::new(RwLock::new(UpdateStatus::default())),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::sync::Arc;
|
||||
use crate::battery::get_battery_status;
|
||||
use crate::error::RayhunterError;
|
||||
use crate::server::ServerState;
|
||||
use crate::update::UpdateStatus;
|
||||
use crate::{battery::BatteryState, qmdl_store::ManifestEntry};
|
||||
|
||||
use axum::Json;
|
||||
@@ -220,6 +221,20 @@ pub async fn get_qmdl_manifest(
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
get,
|
||||
path = "/api/update-status",
|
||||
tag = "Statistics",
|
||||
responses(
|
||||
(status = StatusCode::OK, description = "Success", body = UpdateStatus)
|
||||
),
|
||||
summary = "Rayhunter update status",
|
||||
description = "Check for available updates for Rayhunter."
|
||||
))]
|
||||
pub async fn get_update_status(State(state): State<Arc<ServerState>>) -> Json<UpdateStatus> {
|
||||
Json(state.update_status_lock.read().await.clone())
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "apidocs", utoipa::path(
|
||||
get,
|
||||
path = "/api/log",
|
||||
|
||||
274
daemon/src/update.rs
Normal file
274
daemon/src/update.rs
Normal file
@@ -0,0 +1,274 @@
|
||||
use chrono::{DateTime, Local};
|
||||
use log::{error, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::select;
|
||||
use tokio::sync::{RwLock, mpsc::Sender};
|
||||
use tokio::time;
|
||||
use tokio::time::{Duration, MissedTickBehavior};
|
||||
use tokio_util::{sync::CancellationToken, task::TaskTracker};
|
||||
|
||||
use crate::notifications::{Notification, NotificationType};
|
||||
|
||||
const UPDATE_CHECK_INTERVAL: Duration = Duration::from_secs(6 * 60 * 60);
|
||||
const GITHUB_LATEST_RELEASE_URL: &str =
|
||||
"https://api.github.com/repos/EFForg/rayhunter/releases/latest";
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[cfg_attr(feature = "apidocs", derive(utoipa::ToSchema))]
|
||||
pub struct UpdateStatus {
|
||||
pub current_version: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub latest_version: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub latest_release_url: Option<String>,
|
||||
pub update_available: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[cfg_attr(feature = "apidocs", schema(value_type = Option<String>, format = "date-time"))]
|
||||
pub last_checked: Option<DateTime<Local>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_error: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for UpdateStatus {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
current_version: get_current_version(),
|
||||
// To-be-populated by update check worker
|
||||
latest_version: None,
|
||||
latest_release_url: None,
|
||||
update_available: false,
|
||||
last_checked: None,
|
||||
last_error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GitHubReleaseResponse {
|
||||
tag_name: String,
|
||||
html_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
|
||||
struct VersionParts {
|
||||
major: u64,
|
||||
minor: u64,
|
||||
patch: u64,
|
||||
}
|
||||
|
||||
fn get_current_version() -> String {
|
||||
// See https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates
|
||||
env!("CARGO_PKG_VERSION").to_owned()
|
||||
}
|
||||
|
||||
fn parse_release_tagname(version: &str) -> Option<(VersionParts, String)> {
|
||||
// Trim whitespace and leading `v`, if any
|
||||
let trimmed_version = version.trim().trim_start_matches('v');
|
||||
let mut parts = trimmed_version.split('.');
|
||||
|
||||
// Fail on versions with pre-release metadata: https://github.com/EFForg/rayhunter/pull/1054#issuecomment-4528407281
|
||||
let major = parts.next()?.parse::<u64>().ok()?;
|
||||
let minor = parts.next()?.parse::<u64>().ok()?;
|
||||
let patch = parts.next()?.parse::<u64>().ok()?;
|
||||
// Expect only major.minor.patch format
|
||||
if parts.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let version = format!("{}.{}.{}", major, minor, patch);
|
||||
Some((
|
||||
VersionParts {
|
||||
major,
|
||||
minor,
|
||||
patch,
|
||||
},
|
||||
version.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn format_update_message(current_version: &str, latest_version: &str, release_url: &str) -> String {
|
||||
format!(
|
||||
"Rayhunter {current_version} is installed, but {latest_version} is available. Open {release_url} to download the update."
|
||||
)
|
||||
}
|
||||
|
||||
async fn refresh_update_status(
|
||||
status_lock: &Arc<RwLock<UpdateStatus>>,
|
||||
http_client: &reqwest::Client,
|
||||
) -> Result<Option<(String, String)>, String> {
|
||||
let response = http_client
|
||||
.get(GITHUB_LATEST_RELEASE_URL)
|
||||
.timeout(Duration::from_secs(5))
|
||||
.header(reqwest::header::USER_AGENT, "rayhunter-update-checker")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| format!("failed to query GitHub releases: {err}"))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!(
|
||||
"GitHub release check returned {}",
|
||||
response.status()
|
||||
));
|
||||
}
|
||||
|
||||
let response_text = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|err| format!("failed to read GitHub release response: {err}"))?;
|
||||
let release: GitHubReleaseResponse = serde_json::from_str(&response_text)
|
||||
.map_err(|err| format!("failed to parse GitHub release response: {err}"))?;
|
||||
|
||||
let current_version = get_current_version();
|
||||
let (current_version_parts, current_version) = parse_release_tagname(¤t_version)
|
||||
.ok_or_else(|| format!("failed to parse current version {current_version}"))?;
|
||||
let (latest_version_parts, latest_version) = parse_release_tagname(&release.tag_name)
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"failed to parse latest release version {}",
|
||||
release.tag_name
|
||||
)
|
||||
})?;
|
||||
|
||||
let update_available = latest_version_parts > current_version_parts;
|
||||
{
|
||||
let mut status = status_lock.write().await;
|
||||
status.current_version = current_version;
|
||||
status.latest_version = Some(latest_version.to_owned());
|
||||
status.latest_release_url = Some(release.html_url.to_owned());
|
||||
status.update_available = update_available;
|
||||
status.last_checked = Some(Local::now());
|
||||
status.last_error = None;
|
||||
}
|
||||
|
||||
if update_available {
|
||||
Ok(Some((latest_version, release.html_url)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_update_check_worker(
|
||||
task_tracker: &TaskTracker,
|
||||
shutdown_token: CancellationToken,
|
||||
update_status_lock: Arc<RwLock<UpdateStatus>>,
|
||||
notification_sender: Sender<Notification>,
|
||||
enabled_notifications: Vec<NotificationType>,
|
||||
) {
|
||||
task_tracker.spawn(async move {
|
||||
let http_client = match reqwest::Client::builder().build() {
|
||||
Ok(client) => client,
|
||||
Err(err) => {
|
||||
error!("failed to create update check client: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut interval = time::interval(UPDATE_CHECK_INTERVAL);
|
||||
interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||
|
||||
// Keep track of last notified version
|
||||
let mut last_notified_version: Option<String> = None;
|
||||
|
||||
loop {
|
||||
if shutdown_token.is_cancelled() {
|
||||
break;
|
||||
}
|
||||
|
||||
match refresh_update_status(&update_status_lock, &http_client).await {
|
||||
Ok(Some((latest_version, latest_release_url))) => {
|
||||
if last_notified_version.as_deref() != Some(latest_version.as_str()) {
|
||||
let current_version =
|
||||
update_status_lock.read().await.current_version.clone();
|
||||
let message = format_update_message(
|
||||
¤t_version,
|
||||
&latest_version,
|
||||
&latest_release_url,
|
||||
);
|
||||
if enabled_notifications.contains(&NotificationType::Update) {
|
||||
if let Err(err) = notification_sender
|
||||
.send(Notification::new(NotificationType::Update, message, None))
|
||||
.await
|
||||
{
|
||||
error!("failed to enqueue update notification: {err}");
|
||||
} else {
|
||||
info!("notified about Rayhunter update {latest_version}");
|
||||
}
|
||||
}
|
||||
last_notified_version = Some(latest_version);
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
last_notified_version = None;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("update check failed: {err}");
|
||||
let mut status = update_status_lock.write().await;
|
||||
status.last_error = Some(err);
|
||||
status.last_checked = Some(Local::now());
|
||||
}
|
||||
}
|
||||
|
||||
select! {
|
||||
_ = shutdown_token.cancelled() => break,
|
||||
_ = interval.tick() => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse_release_tagname;
|
||||
|
||||
#[test]
|
||||
fn parses_simple_versions() {
|
||||
let (parts, version) = parse_release_tagname("0.11.1").unwrap();
|
||||
assert_eq!(parts.major, 0);
|
||||
assert_eq!(parts.minor, 11);
|
||||
assert_eq!(parts.patch, 1);
|
||||
assert_eq!(version, "0.11.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_none_for_invalid_versions() {
|
||||
assert!(parse_release_tagname("invalid").is_none());
|
||||
assert!(parse_release_tagname("v1.2").is_none());
|
||||
assert!(parse_release_tagname("v1.2.x").is_none());
|
||||
assert!(parse_release_tagname("v1.2.3.4").is_none());
|
||||
assert!(parse_release_tagname("v1.2.-3").is_none());
|
||||
assert!(parse_release_tagname("v1.2.3-beta").is_none());
|
||||
assert!(parse_release_tagname("v1.2.3-beta.1").is_none());
|
||||
assert!(parse_release_tagname("1.2").is_none());
|
||||
assert!(parse_release_tagname("1.2.x").is_none());
|
||||
assert!(parse_release_tagname("1.2.3.4").is_none());
|
||||
assert!(parse_release_tagname("1.2.-3").is_none());
|
||||
assert!(parse_release_tagname("1.2.3-beta").is_none());
|
||||
assert!(parse_release_tagname("1.2.3-beta.1").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compares_versions_numerically() {
|
||||
let (newer_version_parts, newer_version) = parse_release_tagname("v0.11.2").unwrap();
|
||||
let (older_version_parts, older_version) = parse_release_tagname("v0.11.1").unwrap();
|
||||
assert!(newer_version_parts > older_version_parts);
|
||||
assert_eq!(newer_version, "0.11.2");
|
||||
assert_eq!(older_version, "0.11.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compares_major_minor_patch_correctly() {
|
||||
let (v1_parts, v1) = parse_release_tagname("v1.0.0").unwrap();
|
||||
let (v2_parts, v2) = parse_release_tagname("v1.0.1").unwrap();
|
||||
let (v3_parts, v3) = parse_release_tagname("v1.1.0").unwrap();
|
||||
let (v4_parts, v4) = parse_release_tagname("v2.0.0").unwrap();
|
||||
|
||||
assert!(v2_parts > v1_parts);
|
||||
assert!(v3_parts > v2_parts);
|
||||
assert!(v4_parts > v3_parts);
|
||||
|
||||
assert_eq!(v1, "1.0.0");
|
||||
assert_eq!(v2, "1.0.1");
|
||||
assert_eq!(v3, "1.1.0");
|
||||
assert_eq!(v4, "2.0.0");
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
get_wifi_status,
|
||||
scan_wifi_networks,
|
||||
GpsMode,
|
||||
enabled_notifications,
|
||||
type Config,
|
||||
type WifiStatus,
|
||||
type WifiNetwork,
|
||||
@@ -214,6 +215,22 @@
|
||||
<div class="border-t border-gray-200 pt-4 mt-6 space-y-3">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4">Notification Settings</h3>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
id="auto_check_updates"
|
||||
type="checkbox"
|
||||
bind:checked={config.auto_check_updates}
|
||||
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded-sm"
|
||||
/>
|
||||
<label for="auto_check_updates" class="ml-2 block text-sm text-gray-700">
|
||||
Automatically check for software updates
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">
|
||||
When enabled, Rayhunter periodically checks GitHub for new releases and
|
||||
shows an update notice in the web UI.
|
||||
</p>
|
||||
|
||||
<ExpandableInput
|
||||
bind:value={config.ntfy_url}
|
||||
checkboxId="ntfy_enabled"
|
||||
@@ -295,6 +312,20 @@
|
||||
Low Battery
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="enable_update_notifications"
|
||||
value={enabled_notifications.Update}
|
||||
bind:group={config.enabled_notifications}
|
||||
/>
|
||||
<label
|
||||
for="enable_update_notifications"
|
||||
class="ml-2 block text-sm text-gray-700"
|
||||
>
|
||||
Software Updates
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</ExpandableInput>
|
||||
</div>
|
||||
|
||||
50
daemon/web/src/lib/components/UpdateNotice.svelte
Normal file
50
daemon/web/src/lib/components/UpdateNotice.svelte
Normal file
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import type { UpdateStatus } from '$lib/utils.svelte';
|
||||
|
||||
let { status = null }: { status: UpdateStatus | null } = $props();
|
||||
|
||||
let is_visible = $derived(
|
||||
Boolean(status?.update_available && status.latest_version && status.latest_release_url)
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if is_visible && status}
|
||||
<div class="bg-sky-100 border-sky-300 drop-shadow-sm p-4 flex flex-col gap-2 border rounded-md">
|
||||
<span class="text-xl font-bold flex flex-row items-center gap-2 text-sky-800">
|
||||
<svg
|
||||
class="w-6 h-6 text-sky-700"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm11-4a1 1 0 1 0-2 0v5a1 1 0 1 0 2 0V8Zm-1 7a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H12Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Software Update Available
|
||||
</span>
|
||||
<p>
|
||||
A new version of Rayhunter is available! You are currently running version {status.current_version},
|
||||
and the latest release is version {status.latest_version}.
|
||||
</p>
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<span class="text-sm text-sky-900/80">
|
||||
View the latest release on GitHub to see what's new and download the update.
|
||||
</span>
|
||||
<a
|
||||
class="inline-flex items-center justify-center rounded-md bg-sky-700 px-4 py-2 text-white font-semibold hover:bg-sky-800"
|
||||
href={status.latest_release_url}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
aria-label="View latest release on GitHub"
|
||||
>
|
||||
View Release
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -16,6 +16,7 @@ export interface AnalyzerConfig {
|
||||
export enum enabled_notifications {
|
||||
Warning = 'Warning',
|
||||
LowBattery = 'LowBattery',
|
||||
Update = 'Update',
|
||||
}
|
||||
|
||||
export interface WebdavConfig {
|
||||
@@ -52,6 +53,7 @@ export interface Config {
|
||||
key_input_mode: number;
|
||||
ntfy_url: string | null;
|
||||
enabled_notifications: enabled_notifications[];
|
||||
auto_check_updates: boolean;
|
||||
analyzers: AnalyzerConfig;
|
||||
min_space_to_start_recording_mb: number;
|
||||
min_space_to_continue_recording_mb: number;
|
||||
@@ -170,10 +172,23 @@ export interface TimeResponse {
|
||||
offset_seconds: number;
|
||||
}
|
||||
|
||||
export interface UpdateStatus {
|
||||
current_version: string;
|
||||
latest_version?: string | null;
|
||||
latest_release_url?: string | null;
|
||||
update_available: boolean;
|
||||
last_checked?: string | null;
|
||||
last_error?: string | null;
|
||||
}
|
||||
|
||||
export async function get_daemon_time(): Promise<TimeResponse> {
|
||||
return JSON.parse(await req('GET', '/api/time'));
|
||||
}
|
||||
|
||||
export async function get_update_status(): Promise<UpdateStatus> {
|
||||
return JSON.parse(await req('GET', '/api/update-status'));
|
||||
}
|
||||
|
||||
export interface GpsData {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
import {
|
||||
get_manifest,
|
||||
get_system_stats,
|
||||
get_update_status,
|
||||
get_gps,
|
||||
get_config,
|
||||
GpsMode,
|
||||
type UpdateStatus,
|
||||
type GpsData,
|
||||
} from '$lib/utils.svelte';
|
||||
import ManifestTable from '$lib/components/ManifestTable.svelte';
|
||||
@@ -19,6 +21,7 @@
|
||||
import ActionErrors from '$lib/components/ActionErrors.svelte';
|
||||
import ClockDriftAlert from '$lib/components/ClockDriftAlert.svelte';
|
||||
import LogView from '$lib/components/LogView.svelte';
|
||||
import UpdateNotice from '$lib/components/UpdateNotice.svelte';
|
||||
|
||||
let manager: AnalysisManager = new AnalysisManager();
|
||||
let loaded = $state(false);
|
||||
@@ -31,6 +34,7 @@
|
||||
let config_shown: boolean = $state(false);
|
||||
let gps_data: GpsData | null = $state(null);
|
||||
let gps_mode: GpsMode = $state(GpsMode.Disabled);
|
||||
let update_status: UpdateStatus | null = $state(null);
|
||||
$effect(() => {
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
@@ -49,6 +53,13 @@
|
||||
current_entry = new_manifest.current_entry;
|
||||
|
||||
system_stats = await get_system_stats();
|
||||
// Allow update status to fail
|
||||
try {
|
||||
update_status = await get_update_status();
|
||||
} catch (error) {
|
||||
console.error('Error fetching update status:', error);
|
||||
update_status = null;
|
||||
}
|
||||
const config = await get_config();
|
||||
gps_mode = config.gps_mode;
|
||||
gps_data = await get_gps();
|
||||
@@ -252,6 +263,7 @@
|
||||
{/if}
|
||||
<ActionErrors />
|
||||
<ClockDriftAlert />
|
||||
<UpdateNotice status={update_status} />
|
||||
{#if loaded}
|
||||
<div class="flex flex-col lg:flex-row gap-4">
|
||||
{#if current_entry}
|
||||
|
||||
4
dist/config.toml.in
vendored
4
dist/config.toml.in
vendored
@@ -28,6 +28,10 @@ key_input_mode = 0
|
||||
# What notification types to enable. Does nothing if the above ntfy_url is not set.
|
||||
enabled_notifications = ["Warning", "LowBattery"]
|
||||
|
||||
# If true, Rayhunter will periodically check GitHub for new releases and show
|
||||
# an update notice in the web UI.
|
||||
auto_check_updates = false
|
||||
|
||||
# Disk Space Management
|
||||
# Minimum free space (MB) required to start recording
|
||||
min_space_to_start_recording_mb = 1
|
||||
|
||||
@@ -15,10 +15,12 @@ Through web UI you can set:
|
||||
- *Disable button control*: built-in power button of the device is not used by Rayhunter.
|
||||
- *Double-tap power button to start new recording*: double clicking on a built-in power button of the device stops and immediately restarts the recording. This could be useful if Rayhunter's heuristics is triggered and you get the red line, and you want to "reset" the past warnings. Normally you can do that through web UI, but sometimes it is easier to double tap on power button.
|
||||
- **Colorblind Mode** enables color blind mode (blue line is shown instead of green line, red line remains red). Please note that this does not cover all types of color blindness, but switching green to blue should be about enough to differentiate the color change for most types of color blindness.
|
||||
- **Automatically check for software updates** enables periodic checks against the Rayhunter GitHub releases page. When a newer release is found, the web UI shows a notice and, if ntfy update notifications are enabled, a notification is sent.
|
||||
- **ntfy URL**, which allows setting a [ntfy](https://ntfy.sh/) URL to which notifications of new detections will be sent. The topic should be unique to your device, e.g., `https://ntfy.sh/rayhunter_notifications_ba9di7ie` or `https://myserver.example.com/rayhunter_notifications_ba9di7ie`. The ntfy Android and iOS apps can then be used to receive notifications. More information can be found in the [ntfy docs](https://docs.ntfy.sh/).
|
||||
- **Enabled Notification Types** allows enabling or disabling the following types of notifications:
|
||||
- *Warnings*, which will alert when a heuristic is triggered. Alerts will be sent at most once every five minutes.
|
||||
- *Low Battery*, which will alert when the device's battery is low. Notifications may not be supported for all devices—you can check if your device is supported by looking at whether the battery level indicator is functioning on the System Information section of the Rayhunter UI.
|
||||
- *Software Updates*, which will alert when a new Rayhunter release is available. Only triggers when *Automatically check for software updates* is enabled.
|
||||
- With **Analyzer Heuristic Settings** you can switch on or off built-in [Rayhunter heuristics](heuristics.md). Some heuristics are experimental or can trigger a lot of false positive warnings in some networks (our tests have shown that some heuristics have different behavior in US or European networks). In that case you can decide whether you would like to have the heuristics that trigger a lot of false positives on or off. Please note that we are constantly improving and adding new heuristics, so a new release may reduce false positives in existing heuristics as well.
|
||||
|
||||
## GPS
|
||||
|
||||
Reference in New Issue
Block a user