requests addressed, better error handling, more logging, small text corrections

This commit is contained in:
Carlos Guerra
2026-04-19 16:50:51 +02:00
committed by Will Greenberg
parent ba78c7bd01
commit 5a4a3034be
11 changed files with 123 additions and 82 deletions

View File

@@ -167,15 +167,22 @@ impl DiagTask {
if self.gps_mode == GpsMode::Fixed
&& let Some((lat, lon)) = self.gps_fixed_coords
&& let Some((entry_idx, _)) = qmdl_store.get_current_entry()
&& let Ok(mut gps_file) = qmdl_store.open_entry_gps_for_append(entry_idx).await
{
let record = GpsRecord {
unix_ts: 0,
lat,
lon,
};
if let Ok(json) = serde_json::to_string(&record) {
let _ = gps_file.write_all(format!("{json}\n").as_bytes()).await;
match qmdl_store.open_entry_gps_for_append(entry_idx).await {
Ok(Some(mut gps_file)) => {
let record = GpsRecord {
unix_ts: 0,
lat,
lon,
};
if let Ok(json) = serde_json::to_string(&record) {
let _ = gps_file.write_all(format!("{json}\n").as_bytes()).await;
}
}
Ok(None) => {
error!("GPS sidecar directory not found, cannot write fixed-mode coordinates")
}
Err(e) => error!("failed to open GPS sidecar for fixed-mode entry: {e}"),
}
}
self.stop_current_recording().await;

View File

@@ -2,7 +2,7 @@ use axum::Json;
use axum::extract::State;
use axum::http::StatusCode;
use chrono::Utc;
use log::error;
use log::{error, warn};
use serde::{Deserialize, Deserializer, Serialize};
use std::sync::Arc;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
@@ -47,14 +47,22 @@ pub struct GpsRecord {
pub lon: f64,
}
/// Reads all GPS records from a sidecar NDJSON file, skipping malformed lines.
/// Reads all GPS records from a sidecar NDJSON file, logging and skipping malformed lines.
pub async fn load_gps_records(file: tokio::fs::File) -> Vec<GpsRecord> {
let reader = BufReader::new(file);
let mut lines = reader.lines();
let mut records = Vec::new();
while let Ok(Some(line)) = lines.next_line().await {
if let Ok(record) = serde_json::from_str::<GpsRecord>(&line) {
records.push(record);
loop {
match lines.next_line().await {
Ok(Some(line)) => match serde_json::from_str::<GpsRecord>(&line) {
Ok(record) => records.push(record),
Err(e) => warn!("skipping malformed GPS sidecar line: {e}"),
},
Ok(None) => break,
Err(e) => {
error!("error reading GPS sidecar file: {e}");
break;
}
}
}
records
@@ -67,7 +75,8 @@ pub async fn post_gps(
if state.config.gps_mode != GpsMode::Api {
return Err((
StatusCode::FORBIDDEN,
"GPS API endpoint is disabled. Set gps_mode to 2 in configuration.".to_string(),
"GPS API endpoint is disabled. Set gps_mode to API endpoint in configuration."
.to_string(),
));
}
let mut gps = state.gps_state.write().await;
@@ -75,21 +84,38 @@ pub async fn post_gps(
drop(gps);
let qmdl_store = state.qmdl_store_lock.read().await;
if let Some((entry_idx, _)) = qmdl_store.get_current_entry()
&& let Ok(mut file) = qmdl_store.open_entry_gps_for_append(entry_idx).await
{
let record = GpsRecord {
unix_ts: Utc::now().timestamp(),
lat: gps_data.latitude,
lon: gps_data.longitude,
};
match serde_json::to_string(&record) {
Ok(json) => {
if let Err(e) = file.write_all(format!("{json}\n").as_bytes()).await {
error!("failed to write GPS record to sidecar: {e}");
}
if let Some((entry_idx, _)) = qmdl_store.get_current_entry() {
match qmdl_store.open_entry_gps_for_append(entry_idx).await {
Ok(Some(mut file)) => {
let record = GpsRecord {
unix_ts: Utc::now().timestamp(),
lat: gps_data.latitude,
lon: gps_data.longitude,
};
let json = serde_json::to_string(&record).map_err(|e| {
error!("failed to serialize GPS record: {e}");
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("failed to serialize GPS record: {e}"),
)
})?;
file.write_all(format!("{json}\n").as_bytes())
.await
.map_err(|e| {
error!("failed to write GPS record to sidecar: {e}");
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("failed to write GPS record to sidecar: {e}"),
)
})?;
}
Ok(None) => error!("GPS sidecar directory not found, cannot write GPS record"),
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("failed to open GPS sidecar: {e}"),
));
}
Err(e) => error!("failed to serialize GPS record: {e}"),
}
}

View File

@@ -306,6 +306,8 @@ async fn run_with_config(
config.webdav.clone().into(),
);
}
// For fixed configuration, we use timestamp 0 to not break other
// the GET request for GPS but user won't see the 0 in PCAPs
let initial_gps = if config.gps_mode == GpsMode::Fixed {
match (config.gps_fixed_latitude, config.gps_fixed_longitude) {
(Some(lat), Some(lon)) => Some(gps::GpsData {

View File

@@ -1,7 +1,7 @@
use crate::config::GpsMode;
use crate::gps::{GpsRecord, load_gps_records};
use crate::server::ServerState;
use crate::config::GpsMode;
use anyhow::Error;
use axum::body::Body;
use axum::extract::{Path, State};
@@ -74,41 +74,27 @@ pub(crate) async fn load_gps_records_for_entry(
state: &Arc<ServerState>,
entry_index: usize,
) -> Vec<GpsRecord> {
// Always try the per-session sidecar first — it reflects what was actually
// recorded regardless of what the current gps_mode config is.
let entry_gps_mode;
{
let qmdl_store = state.qmdl_store_lock.read().await;
if let Ok(file) = qmdl_store.open_entry_gps(entry_index).await {
let records = load_gps_records(file).await;
if !records.is_empty() {
return records;
let qmdl_store = state.qmdl_store_lock.read().await;
match qmdl_store.open_entry_gps(entry_index).await {
Ok(Some(file)) => load_gps_records(file).await,
Ok(None) => {
let gps_mode = qmdl_store
.manifest
.entries
.get(entry_index)
.and_then(|e| e.gps_mode);
if gps_mode.is_some_and(|m| m != GpsMode::Disabled) {
error!(
"GPS sidecar expected for entry {entry_index} (mode: {gps_mode:?}) but not found"
);
}
vec![]
}
Err(e) => {
error!("failed to open GPS sidecar: {e}");
vec![]
}
// Capture the entry's recorded GPS mode before releasing the lock.
entry_gps_mode = qmdl_store
.manifest
.entries
.get(entry_index)
.and_then(|e| e.gps_mode);
}
// Sidecar missing or empty — fall back using the entry's own recorded GPS mode,
// not the current config, so old fixed-mode sessions still get coordinates even
// if the mode has since been changed. Use the configured fixed coords directly
// rather than gps_state, which can be overwritten by API calls or be None.
if entry_gps_mode == Some(GpsMode::Fixed)
&& let (Some(lat), Some(lon)) = (
state.config.gps_fixed_latitude,
state.config.gps_fixed_longitude,
)
{
return vec![GpsRecord {
unix_ts: 0,
lat,
lon,
}];
}
vec![]
}
fn find_nearest_gps(records: &[GpsRecord], packet_unix_ts: i64) -> Option<GpsPoint> {

View File

@@ -304,24 +304,33 @@ impl RecordingStore {
.map_err(RecordingStoreError::ReadFileError)
}
pub async fn open_entry_gps(&self, entry_index: usize) -> Result<File, RecordingStoreError> {
pub async fn open_entry_gps(
&self,
entry_index: usize,
) -> Result<Option<File>, RecordingStoreError> {
let entry = &self.manifest.entries[entry_index];
File::open(entry.get_gps_filepath(&self.path))
.await
.map_err(RecordingStoreError::ReadFileError)
match File::open(entry.get_gps_filepath(&self.path)).await {
Ok(file) => Ok(Some(file)),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(None),
Err(e) => Err(RecordingStoreError::ReadFileError(e)),
}
}
pub async fn open_entry_gps_for_append(
&self,
entry_index: usize,
) -> Result<File, RecordingStoreError> {
) -> Result<Option<File>, RecordingStoreError> {
let entry = &self.manifest.entries[entry_index];
OpenOptions::new()
match OpenOptions::new()
.create(true)
.append(true)
.open(entry.get_gps_filepath(&self.path))
.await
.map_err(RecordingStoreError::CreateFileError)
{
Ok(file) => Ok(Some(file)),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(None),
Err(e) => Err(RecordingStoreError::CreateFileError(e)),
}
}
pub async fn clear_and_open_entry_analysis(

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { type ReportMetadata } from '$lib/analysis.svelte';
import type { ManifestEntry } from '$lib/manifest.svelte';
import { GpsMode } from '$lib/utils.svelte';
import { AnalysisManager } from '$lib/analysisManager.svelte';
import AnalysisTable from './AnalysisTable.svelte';
import ReAnalyzeButton from './ReAnalyzeButton.svelte';
@@ -72,7 +73,7 @@
{/if}
<p>
<b>GPS Mode:</b>
{(entry.gps_mode ?? 0) === 0 ? 'Disabled' : entry.gps_mode === 1 ? 'Fixed coordinates' : 'API endpoint'}
{(entry.gps_mode ?? GpsMode.Disabled) === GpsMode.Disabled ? 'Disabled' : entry.gps_mode === GpsMode.Fixed ? 'Fixed coordinates' : 'API endpoint'}
</p>
</div>
{#if metadata && metadata.analyzers}

View File

@@ -5,6 +5,7 @@
test_notification,
get_wifi_status,
scan_wifi_networks,
GpsMode,
type Config,
type WifiStatus,
type WifiNetwork,
@@ -785,21 +786,21 @@
<div>
<label for="gps_mode" class="block text-sm font-medium text-gray-700 mb-1">GPS Mode</label>
<select id="gps_mode" bind:value={config.gps_mode} class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue">
<option value={0}>Disabled</option>
<option value={1}>Fixed coordinates</option>
<option value={2}>API endpoint</option>
<option value={GpsMode.Disabled}>Disabled</option>
<option value={GpsMode.Fixed}>Fixed coordinates</option>
<option value={GpsMode.Api}>API endpoint</option>
</select>
<p class="text-xs text-gray-500 mt-1">
{#if config.gps_mode === 2}
{#if config.gps_mode === GpsMode.Api}
POST latitude, longitude, and timestamp to <code>/api/gps</code> from any device on the network.
{:else if config.gps_mode === 1}
{:else if config.gps_mode === GpsMode.Fixed}
GPS coordinates are fixed to the values below.
{:else}
GPS is disabled; no coordinates will be tracked.
{/if}
</p>
</div>
{#if config.gps_mode === 1}
{#if config.gps_mode === GpsMode.Fixed}
<div>
<label for="gps_fixed_latitude" class="block text-sm font-medium text-gray-700 mb-1">Fixed Latitude</label>
<input id="gps_fixed_latitude" type="number" min="-90" max="90" step="any" required

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { ManifestEntry } from '$lib/manifest.svelte';
import { GpsMode } from '$lib/utils.svelte';
import { AnalysisManager } from '$lib/analysisManager.svelte';
import DownloadLink from '$lib/components/DownloadLink.svelte';
import DeleteButton from '$lib/components/DeleteButton.svelte';
@@ -88,7 +89,7 @@
{/if}
{#if entry.gps_mode !== undefined}
<div class="text-sm text-gray-500">
GPS: {entry.gps_mode === 0 ? 'Disabled' : entry.gps_mode === 1 ? 'Fixed coordinates' : 'API endpoint'}
GPS: {entry.gps_mode === GpsMode.Disabled ? 'Disabled' : entry.gps_mode === GpsMode.Fixed ? 'Fixed coordinates' : 'API endpoint'}
</div>
{/if}
<div class="flex flex-row justify-between lg:justify-end gap-1 mt-2 overflow-x-auto">

View File

@@ -1,5 +1,6 @@
import { get_report, type AnalysisReport } from './analysis.svelte';
import { AnalysisStatus, type AnalysisManager } from './analysisManager.svelte';
import { GpsMode } from './utils.svelte';
interface JsonManifest {
entries: JsonManifestEntry[];
@@ -13,7 +14,7 @@ interface JsonManifestEntry {
qmdl_size_bytes: number;
stop_reason: string | null;
upload_time: string | null;
gps_mode: number | null;
gps_mode: GpsMode | null;
}
export class Manifest {
@@ -62,7 +63,7 @@ export class ManifestEntry {
public analysis_report: AnalysisReport | string | undefined = $state(undefined);
public stop_reason: string | undefined = $state(undefined);
public upload_time: Date | undefined = $state(undefined);
public gps_mode: number | undefined = $state(undefined);
public gps_mode: GpsMode | undefined = $state(undefined);
constructor(json: JsonManifestEntry) {
this.name = json.name;

View File

@@ -28,6 +28,13 @@ export interface WebdavConfig {
delete_on_upload: boolean;
}
export enum GpsMode {
Disabled = 0,
Fixed = 1,
Api = 2,
}
export interface Config {
device: string;
ui_level: number;
@@ -46,7 +53,7 @@ export interface Config {
firewall_restrict_outbound: boolean;
firewall_allowed_ports: number[] | null;
webdav: WebdavConfig;
gps_mode: number;
gps_mode: GpsMode;
gps_fixed_latitude: number | null;
gps_fixed_longitude: number | null;
}

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { ManifestEntry } from '$lib/manifest.svelte';
import { get_manifest, get_system_stats, get_gps, get_config, type GpsData } from '$lib/utils.svelte';
import { get_manifest, get_system_stats, get_gps, get_config, GpsMode, type GpsData } from '$lib/utils.svelte';
import ManifestTable from '$lib/components/ManifestTable.svelte';
import Card from '$lib/components/ManifestCard.svelte';
import type { SystemStats } from '$lib/systemStats';
@@ -23,7 +23,7 @@
let logview_shown: boolean = $state(false);
let config_shown: boolean = $state(false);
let gps_data: GpsData | null = $state(null);
let gps_mode: number = $state(0);
let gps_mode: GpsMode = $state(GpsMode.Disabled);
$effect(() => {
get_config().then((c) => {
gps_mode = c.gps_mode;
@@ -290,7 +290,7 @@
{/if}
<SystemStatsTable stats={system_stats!} />
</div>
{#if gps_mode !== 0}
{#if gps_mode !== GpsMode.Disabled}
<div class="bg-white border border-gray-200 drop-shadow rounded-md p-4 flex flex-col gap-2">
<span class="text-lg font-semibold flex flex-row items-center gap-2">
<svg class="w-5 h-5 text-rayhunter-blue" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">