mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-06-01 18:53:34 -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,
|
||||
|
||||
+16
-1
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user