Add button to set current time

When there is a significant difference between the user's browser's time
and the system time, a button appears in the web UI to fix the system
time. This time will then be used to correct both data inside of PCAPs
and any metadata.

We don't actually set the system time to this value. Instead, rayhunter
adjusts any timestamps it handles by an offset. That offset defaults to
zero, and the user adjusts it by hitting the button in the web UI. The
main reason for this is device portability.

I haven't investigated whether it would actually be easy to set the real
system time. It's possible that it works the same way across all
devices.
This commit is contained in:
Markus Unterwaditzer
2026-01-25 19:45:08 +01:00
committed by Will Greenberg
parent 781d07230c
commit bef6b51e28
10 changed files with 216 additions and 16 deletions

View File

@@ -12,6 +12,7 @@
onclick,
ariaLabel,
errorMessage,
jsonBody,
}: {
url: string;
method?: string;
@@ -23,6 +24,7 @@
onclick?: () => void | Promise<void>;
ariaLabel?: string;
errorMessage?: string;
jsonBody?: unknown;
} = $props();
let is_requesting = $state(false);
@@ -51,7 +53,8 @@
await user_action_req(
method,
url,
errorMessage ? errorMessage : 'Error performing action'
errorMessage ? errorMessage : 'Error performing action',
jsonBody
);
if (onclick) {
await onclick();

View File

@@ -0,0 +1,119 @@
<script lang="ts">
import { get_daemon_time } from '$lib/utils.svelte';
import ApiRequestButton from './ApiRequestButton.svelte';
let show_alert = $state(false);
let device_system_time = $state('');
let device_adjusted_time = $state('');
let browser_time = $state('');
let has_offset = $state(false);
let computed_offset = $state(0);
let dismissed = $state(false);
let check_completed = $state(false);
const DRIFT_THRESHOLD_SECONDS = 30;
function format_time(date: Date): string {
return date.toLocaleString();
}
async function check_clock_drift() {
if (check_completed) return;
try {
const daemon_time_response = await get_daemon_time();
const browser_now = new Date();
const daemon_system_ms = new Date(daemon_time_response.system_time).getTime();
const device_adjusted_ms = new Date(daemon_time_response.adjusted_time).getTime();
const drift_seconds = Math.round((browser_now.getTime() - device_adjusted_ms) / 1000);
if (Math.abs(drift_seconds) > DRIFT_THRESHOLD_SECONDS && !dismissed) {
device_system_time = format_time(new Date(daemon_time_response.system_time));
device_adjusted_time = format_time(new Date(daemon_time_response.adjusted_time));
browser_time = format_time(browser_now);
has_offset = daemon_time_response.offset_seconds !== 0;
// Calculate offset needed: browser_time - daemon_system_time
computed_offset = Math.round((browser_now.getTime() - daemon_system_ms) / 1000);
show_alert = true;
}
} catch (err) {
console.error('Failed to check clock drift:', err);
}
check_completed = true;
}
function dismiss() {
show_alert = false;
dismissed = true;
}
// Check clock drift on component mount
$effect(() => {
check_clock_drift();
});
</script>
{#if show_alert}
<div
class="bg-yellow-100 border-yellow-400 drop-shadow p-4 flex flex-col gap-2 border rounded-md"
>
<span class="text-xl font-bold flex flex-row items-center gap-2 text-yellow-700">
<svg
class="w-6 h-6 text-yellow-600"
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 0v4a1 1 0 0 0 .293.707l3 3a1 1 0 0 0 1.414-1.414L13 11.586V8Z"
clip-rule="evenodd"
/>
</svg>
Clock Mismatch Detected
</span>
<p>
Normally the device should get the correct time from the network. Consider using another
SIM card for better results.
</p>
<table class="w-fit">
<tbody>
<tr>
<td class="pr-2">Device (system):</td>
<td class="font-mono">{device_system_time}</td>
</tr>
{#if has_offset}
<tr>
<td class="pr-2">Device (adjusted):</td>
<td class="font-mono">{device_adjusted_time}</td>
</tr>
{/if}
<tr>
<td class="pr-2">Browser:</td>
<td class="font-mono">{browser_time}</td>
</tr>
</tbody>
</table>
<p>Copy browser clock to device?</p>
<div class="flex flex-row gap-2 justify-end">
<button
class="font-medium py-2 px-4 rounded-md border border-gray-400 hover:bg-yellow-200"
onclick={dismiss}
>
Dismiss
</button>
<ApiRequestButton
url="/api/time-offset"
label="Sync Clock"
loadingLabel="Syncing..."
variant="green"
jsonBody={{ offset_seconds: computed_offset }}
onclick={dismiss}
errorMessage="Error syncing clock"
/>
</div>
</div>
{/if}

View File

@@ -26,15 +26,18 @@ export interface Config {
analyzers: AnalyzerConfig;
}
export async function req(method: string, url: string): Promise<string> {
const response = await fetch(url, {
method: method,
});
const body = await response.text();
export async function req(method: string, url: string, json_body?: unknown): Promise<string> {
const options: RequestInit = { method };
if (json_body !== undefined) {
options.body = JSON.stringify(json_body);
options.headers = { 'Content-Type': 'application/json' };
}
const response = await fetch(url, options);
const responseBody = await response.text();
if (response.status >= 200 && response.status < 300) {
return body;
return responseBody;
} else {
throw new Error(body);
throw new Error(responseBody);
}
}
@@ -42,13 +45,13 @@ export async function req(method: string, url: string): Promise<string> {
export async function user_action_req(
method: string,
url: string,
error_msg: string
error_msg: string,
json_body?: unknown
): Promise<string | undefined> {
try {
return await req(method, url);
return await req(method, url, json_body);
} catch (error) {
if (error instanceof Error) {
console.log('beeeo');
add_error(error, error_msg);
}
return undefined;
@@ -105,3 +108,13 @@ export interface RouteStatus {
export async function get_route_status(): Promise<RouteStatus> {
return JSON.parse(await req('GET', '/api/route-status'));
}
export interface TimeResponse {
system_time: string;
adjusted_time: string;
offset_seconds: number;
}
export async function get_daemon_time(): Promise<TimeResponse> {
return JSON.parse(await req('GET', '/api/time'));
}

View File

@@ -11,6 +11,7 @@
import ConfigForm from '$lib/components/ConfigForm.svelte';
import ActionErrors from '$lib/components/ActionErrors.svelte';
import IPRouteAlert from '$lib/components/IPRouteAlert.svelte';
import ClockDriftAlert from '$lib/components/ClockDriftAlert.svelte';
import LogView from '$lib/components/LogView.svelte';
import WarningIcon from '$lib/components/WarningIcon.svelte';
@@ -196,6 +197,7 @@
{/if}
<ActionErrors />
<IPRouteAlert />
<ClockDriftAlert />
{#if loaded}
<div class="flex flex-col lg:flex-row gap-4">
{#if current_entry}