mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-30 17:53:35 -07:00
898 lines
42 KiB
Svelte
898 lines
42 KiB
Svelte
<script lang="ts">
|
|
import {
|
|
get_config,
|
|
set_config,
|
|
test_notification,
|
|
get_wifi_status,
|
|
scan_wifi_networks,
|
|
GpsMode,
|
|
type Config,
|
|
type WifiStatus,
|
|
type WifiNetwork,
|
|
} from '../utils.svelte';
|
|
import Modal from './Modal.svelte';
|
|
|
|
let { shown = $bindable() }: { shown: boolean } = $props();
|
|
let config = $state<Config | null>(null);
|
|
|
|
let loading = $state(false);
|
|
let saving = $state(false);
|
|
let testingNotification = $state(false);
|
|
let message = $state('');
|
|
let messageType = $state<'success' | 'error' | null>(null);
|
|
let testMessage = $state('');
|
|
let testMessageType = $state<'success' | 'error' | null>(null);
|
|
let wifiStatus = $state<WifiStatus | null>(null);
|
|
let wifiStatusTimer = $state<ReturnType<typeof setInterval> | null>(null);
|
|
let scanning = $state(false);
|
|
let scanResults = $state<WifiNetwork[]>([]);
|
|
let dnsServersInput = $state('');
|
|
let webdavExpanded = $state(false);
|
|
let webdavUrlInput = $state<HTMLInputElement | null>(null);
|
|
|
|
async function load_config() {
|
|
try {
|
|
loading = true;
|
|
config = await get_config();
|
|
dnsServersInput = config.dns_servers ? config.dns_servers.join(', ') : '';
|
|
webdavExpanded = config.webdav.url.trim() !== '';
|
|
message = '';
|
|
messageType = null;
|
|
poll_wifi_status();
|
|
} catch (error) {
|
|
message = `Failed to load config: ${error}`;
|
|
messageType = 'error';
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
|
|
async function save_config() {
|
|
if (!config) return;
|
|
|
|
const trimmed = dnsServersInput.trim();
|
|
config.dns_servers =
|
|
trimmed.length > 0
|
|
? trimmed
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter((s) => s.length > 0)
|
|
: null;
|
|
|
|
try {
|
|
saving = true;
|
|
await set_config(config);
|
|
message =
|
|
'Config saved successfully! Rayhunter is restarting now. Reload the page in a few seconds.';
|
|
messageType = 'success';
|
|
} catch (error) {
|
|
message = `Failed to save config: ${error}`;
|
|
messageType = 'error';
|
|
} finally {
|
|
saving = false;
|
|
}
|
|
}
|
|
|
|
async function poll_wifi_status() {
|
|
if (wifiStatusTimer) clearInterval(wifiStatusTimer);
|
|
try {
|
|
wifiStatus = await get_wifi_status();
|
|
} catch {
|
|
wifiStatus = null;
|
|
}
|
|
wifiStatusTimer = setInterval(async () => {
|
|
try {
|
|
wifiStatus = await get_wifi_status();
|
|
} catch {
|
|
wifiStatus = null;
|
|
}
|
|
}, 5000);
|
|
}
|
|
|
|
let scanError = $state('');
|
|
|
|
async function do_scan() {
|
|
scanning = true;
|
|
scanError = '';
|
|
try {
|
|
scanResults = await scan_wifi_networks();
|
|
} catch (error) {
|
|
scanResults = [];
|
|
scanError = `Scan failed: ${error}`;
|
|
} finally {
|
|
scanning = false;
|
|
}
|
|
}
|
|
|
|
function select_network(network: WifiNetwork) {
|
|
if (config) {
|
|
config.wifi_ssid = network.ssid;
|
|
config.wifi_password = '';
|
|
config.wifi_security =
|
|
network.security === 'WPA3' || network.security === 'WPA3 (transition)'
|
|
? 'sae'
|
|
: 'wpa_psk';
|
|
scanResults = [];
|
|
}
|
|
}
|
|
|
|
async function send_test_notification() {
|
|
try {
|
|
testingNotification = true;
|
|
testMessage = '';
|
|
testMessageType = null;
|
|
await test_notification();
|
|
testMessage = 'Test notification sent successfully!';
|
|
testMessageType = 'success';
|
|
} catch (error) {
|
|
testMessage = `${error}`;
|
|
testMessageType = 'error';
|
|
} finally {
|
|
testingNotification = false;
|
|
}
|
|
}
|
|
|
|
$effect(() => {
|
|
if (shown && !config) {
|
|
load_config();
|
|
}
|
|
if (!shown && wifiStatusTimer) {
|
|
clearInterval(wifiStatusTimer);
|
|
wifiStatusTimer = null;
|
|
}
|
|
return () => {
|
|
if (wifiStatusTimer) {
|
|
clearInterval(wifiStatusTimer);
|
|
wifiStatusTimer = null;
|
|
}
|
|
};
|
|
});
|
|
</script>
|
|
|
|
<Modal bind:shown title="Configuration">
|
|
<div class="p-2">
|
|
{#if loading}
|
|
<div class="text-center py-4">Loading config...</div>
|
|
{:else if config}
|
|
<form
|
|
class="space-y-4"
|
|
onsubmit={(e) => {
|
|
e.preventDefault();
|
|
save_config();
|
|
}}
|
|
>
|
|
<div>
|
|
<label for="ui_level" class="block text-sm font-medium text-gray-700 mb-1">
|
|
Device UI Level
|
|
</label>
|
|
<select
|
|
id="ui_level"
|
|
bind:value={config.ui_level}
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-rayhunter-blue"
|
|
>
|
|
<option value={0}>Invisible mode</option>
|
|
<option value={1}>Subtle mode (colored line)</option>
|
|
<option value={2}>Demo mode (orca gif)</option>
|
|
<option value={3}>EFF logo</option>
|
|
<option value={4}>High visibility (full screen color)</option>
|
|
</select>
|
|
<p class="text-xs text-gray-500 mt-1">
|
|
Note: Rayhunter draws over the device's native UI, so some flickering is
|
|
expected
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
for="key_input_mode"
|
|
class="block text-sm font-medium text-gray-700 mb-1"
|
|
>
|
|
Device Input Mode
|
|
</label>
|
|
<select
|
|
id="key_input_mode"
|
|
bind:value={config.key_input_mode}
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-rayhunter-blue"
|
|
>
|
|
<option value={0}>Disable button control</option>
|
|
<option value={1}>Double-tap power button to start new recording</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="space-y-3">
|
|
<div class="flex items-center">
|
|
<input
|
|
id="colorblind_mode"
|
|
type="checkbox"
|
|
bind:checked={config.colorblind_mode}
|
|
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded-sm"
|
|
/>
|
|
<label for="colorblind_mode" class="ml-2 block text-sm text-gray-700">
|
|
Colorblind Mode
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<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>
|
|
<label for="ntfy_url" class="block text-sm font-medium text-gray-700 mb-1">
|
|
ntfy URL for Sending Notifications (if unset you will not receive
|
|
notifications)
|
|
</label>
|
|
<input
|
|
id="ntfy_url"
|
|
type="url"
|
|
bind:value={config.ntfy_url}
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-rayhunter-blue"
|
|
/>
|
|
<p class="text-xs text-gray-500 mt-1">
|
|
Test button below uses the saved configuration URL, not the input above
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<button
|
|
type="button"
|
|
onclick={send_test_notification}
|
|
disabled={testingNotification}
|
|
class="bg-rayhunter-blue hover:bg-rayhunter-dark-blue disabled:opacity-50 disabled:cursor-not-allowed text-white font-bold py-2 px-4 rounded-md flex flex-row gap-1 items-center"
|
|
>
|
|
{#if testingNotification}
|
|
<div
|
|
class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"
|
|
></div>
|
|
Sending...
|
|
{:else}
|
|
<svg
|
|
class="w-4 h-4"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
|
|
></path>
|
|
</svg>
|
|
Send Test Notification
|
|
{/if}
|
|
</button>
|
|
{#if testMessage}
|
|
<div
|
|
class="mt-2 p-2 rounded-sm text-sm {testMessageType === 'error'
|
|
? 'bg-red-100 text-red-700'
|
|
: 'bg-green-100 text-green-700'}"
|
|
>
|
|
{testMessage}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<div class="block text-sm font-medium text-gray-700 mb-1">
|
|
Enabled Notification Types
|
|
</div>
|
|
<div class="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
id="enable_warning_notifications"
|
|
value="Warning"
|
|
bind:group={config.enabled_notifications}
|
|
/>
|
|
<label
|
|
for="enable_warning_notifications"
|
|
class="ml-2 block text-sm text-gray-700"
|
|
>
|
|
Warnings
|
|
</label>
|
|
</div>
|
|
<div class="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
id="enable_lowbattery_notifications"
|
|
value="LowBattery"
|
|
bind:group={config.enabled_notifications}
|
|
/>
|
|
<label
|
|
for="enable_lowbattery_notifications"
|
|
class="ml-2 block text-sm text-gray-700"
|
|
>
|
|
Low Battery
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<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">Storage Management</h3>
|
|
|
|
<div>
|
|
<label
|
|
for="min_space_to_start_recording_mb"
|
|
class="block text-sm font-medium text-gray-700 mb-1"
|
|
>
|
|
Minimum Space to Start Recording (MB)
|
|
</label>
|
|
<input
|
|
id="min_space_to_start_recording_mb"
|
|
type="number"
|
|
min="1"
|
|
bind:value={config.min_space_to_start_recording_mb}
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-rayhunter-blue"
|
|
/>
|
|
<p class="text-xs text-gray-500 mt-1">
|
|
Recording will not start if less than this amount of disk space is free
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
for="min_space_to_continue_recording_mb"
|
|
class="block text-sm font-medium text-gray-700 mb-1"
|
|
>
|
|
Minimum Space to Continue Recording (MB)
|
|
</label>
|
|
<input
|
|
id="min_space_to_continue_recording_mb"
|
|
type="number"
|
|
min="1"
|
|
bind:value={config.min_space_to_continue_recording_mb}
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-rayhunter-blue"
|
|
/>
|
|
<p class="text-xs text-gray-500 mt-1">
|
|
Recording will stop automatically if disk space drops below this level
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<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">WebDAV Upload</h3>
|
|
<p class="text-xs text-gray-500">
|
|
Once a recording has been closed for at least the configured age, both the
|
|
.qmdl and .ndjson files are uploaded in the background to the WebDAV server.
|
|
</p>
|
|
|
|
<div class="flex items-center">
|
|
<input
|
|
id="webdav_enabled"
|
|
type="checkbox"
|
|
checked={webdavExpanded}
|
|
onchange={(e) => {
|
|
webdavExpanded = e.currentTarget.checked;
|
|
if (webdavExpanded) {
|
|
setTimeout(() => webdavUrlInput?.focus(), 0);
|
|
} else {
|
|
if (config) config.webdav.url = '';
|
|
}
|
|
}}
|
|
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded-sm"
|
|
/>
|
|
<label for="webdav_enabled" class="ml-2 block text-sm text-gray-700">
|
|
Enable WebDAV upload
|
|
</label>
|
|
</div>
|
|
|
|
{#if webdavExpanded}
|
|
<div>
|
|
<label
|
|
for="webdav_url"
|
|
class="block text-sm font-medium text-gray-700 mb-1"
|
|
>
|
|
Server URL
|
|
</label>
|
|
<input
|
|
id="webdav_url"
|
|
type="url"
|
|
bind:this={webdavUrlInput}
|
|
bind:value={config.webdav.url}
|
|
onblur={() => {
|
|
if (config && config.webdav.url.trim() === '') {
|
|
webdavExpanded = false;
|
|
}
|
|
}}
|
|
placeholder="https://dav.example.com/rayhunter/"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-rayhunter-blue"
|
|
/>
|
|
<p class="text-xs text-gray-500 mt-1">
|
|
Files are uploaded via HTTP PUT under this base URL. No folders are
|
|
created, and folders in this base URL are assumed to exist already.
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
for="webdav_username"
|
|
class="block text-sm font-medium text-gray-700 mb-1"
|
|
>
|
|
Username
|
|
</label>
|
|
<input
|
|
id="webdav_username"
|
|
type="text"
|
|
bind:value={config.webdav.username}
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-rayhunter-blue"
|
|
/>
|
|
<p class="text-xs text-gray-500 mt-1">
|
|
Optional. Leave blank for unauthenticated uploads.
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
for="webdav_password"
|
|
class="block text-sm font-medium text-gray-700 mb-1"
|
|
>
|
|
Password
|
|
</label>
|
|
<input
|
|
id="webdav_password"
|
|
type="password"
|
|
bind:value={config.webdav.password}
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-rayhunter-blue"
|
|
/>
|
|
<p class="text-xs text-gray-500 mt-1">
|
|
A password without a username will be rejected and the request will
|
|
be sent unauthenticated.
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
for="webdav_upload_timeout_secs"
|
|
class="block text-sm font-medium text-gray-700 mb-1"
|
|
>
|
|
Upload Timeout (seconds)
|
|
</label>
|
|
<input
|
|
id="webdav_upload_timeout_secs"
|
|
type="number"
|
|
min="1"
|
|
bind:value={config.webdav.upload_timeout_secs}
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-rayhunter-blue"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
for="webdav_poll_interval_secs"
|
|
class="block text-sm font-medium text-gray-700 mb-1"
|
|
>
|
|
Poll Interval (seconds)
|
|
</label>
|
|
<input
|
|
id="webdav_poll_interval_secs"
|
|
type="number"
|
|
min="1"
|
|
bind:value={config.webdav.poll_interval_secs}
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-rayhunter-blue"
|
|
/>
|
|
<p class="text-xs text-gray-500 mt-1">
|
|
How often the worker checks for new entries to upload.
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label
|
|
for="webdav_min_age_secs"
|
|
class="block text-sm font-medium text-gray-700 mb-1"
|
|
>
|
|
Minimum Age Before Upload (seconds)
|
|
</label>
|
|
<input
|
|
id="webdav_min_age_secs"
|
|
type="number"
|
|
min="0"
|
|
bind:value={config.webdav.min_age_secs}
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-rayhunter-blue"
|
|
/>
|
|
<p class="text-xs text-gray-500 mt-1">
|
|
How long a recording must be closed before it becomes eligible for
|
|
upload.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="flex items-center">
|
|
<input
|
|
id="webdav_delete_on_upload"
|
|
type="checkbox"
|
|
bind:checked={config.webdav.delete_on_upload}
|
|
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded-sm"
|
|
/>
|
|
<label
|
|
for="webdav_delete_on_upload"
|
|
class="ml-2 block text-sm text-gray-700"
|
|
>
|
|
Delete on successful upload
|
|
</label>
|
|
</div>
|
|
<p class="text-xs text-gray-500">
|
|
When enabled, the local files are removed after a successful upload.
|
|
Otherwise the manifest is just marked as uploaded.
|
|
</p>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if config.device === 'orbic' || config.device === 'moxee' || config.device === 'tmobile' || config.device === 'wingtech'}
|
|
<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">WiFi Client Mode</h3>
|
|
<p class="text-xs text-gray-500">
|
|
Connect the device to an existing WiFi network for internet access (e.g.
|
|
notifications, remote access). The hotspot AP stays running alongside
|
|
WiFi client mode.
|
|
</p>
|
|
|
|
<div class="flex items-center">
|
|
<input
|
|
id="wifi_enabled"
|
|
type="checkbox"
|
|
bind:checked={config.wifi_enabled}
|
|
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded-sm"
|
|
/>
|
|
<label for="wifi_enabled" class="ml-2 block text-sm text-gray-700">
|
|
Enable WiFi
|
|
</label>
|
|
</div>
|
|
<p class="text-xs text-gray-500">
|
|
Unchecking stops WiFi without clearing saved credentials.
|
|
</p>
|
|
|
|
{#if wifiStatus && config.wifi_enabled}
|
|
{#if wifiStatus.state === 'connected'}
|
|
<p class="text-xs text-green-600">
|
|
Connected to "{wifiStatus.ssid}" ({wifiStatus.ip})
|
|
</p>
|
|
{:else if wifiStatus.state === 'connecting'}
|
|
<p class="text-xs text-amber-600">Connecting...</p>
|
|
{:else if wifiStatus.state === 'recovering'}
|
|
<p class="text-xs text-amber-600">Recovering connection...</p>
|
|
{:else if wifiStatus.state === 'dataPathDead'}
|
|
<p class="text-xs text-amber-600">
|
|
Data path stalled, attempting recovery...
|
|
</p>
|
|
{:else if wifiStatus.state === 'failed'}
|
|
<p class="text-xs text-red-600">
|
|
Failed: {wifiStatus.error}
|
|
</p>
|
|
{/if}
|
|
{/if}
|
|
|
|
<div>
|
|
<label
|
|
for="wifi_ssid"
|
|
class="block text-sm font-medium text-gray-700 mb-1"
|
|
>
|
|
WiFi Network Name (SSID)
|
|
</label>
|
|
<div class="flex gap-2">
|
|
<input
|
|
id="wifi_ssid"
|
|
type="text"
|
|
bind:value={config.wifi_ssid}
|
|
placeholder="MyWiFiNetwork"
|
|
class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-rayhunter-blue"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onclick={do_scan}
|
|
disabled={scanning}
|
|
class="px-3 py-2 text-sm bg-gray-100 hover:bg-gray-200 disabled:opacity-50 border border-gray-300 rounded-md"
|
|
>
|
|
{scanning ? 'Scanning...' : 'Scan'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{#if scanError}
|
|
<p class="text-xs text-red-600">{scanError}</p>
|
|
{/if}
|
|
|
|
{#if scanResults.length > 0}
|
|
<div
|
|
class="border border-gray-200 rounded-md max-h-40 overflow-y-auto divide-y divide-gray-200"
|
|
>
|
|
{#each scanResults as network}
|
|
<button
|
|
type="button"
|
|
class="w-full px-3 py-2 text-left text-sm hover:bg-gray-50 flex justify-between"
|
|
onclick={() => select_network(network)}
|
|
>
|
|
<span>{network.ssid}</span>
|
|
<span class="text-gray-400"
|
|
>{network.signal_dbm} dBm · {network.security}</span
|
|
>
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
|
|
{#if config.wifi_ssid}
|
|
<div>
|
|
<label
|
|
for="wifi_security"
|
|
class="block text-sm font-medium text-gray-700 mb-1"
|
|
>
|
|
Security Type
|
|
</label>
|
|
<select
|
|
id="wifi_security"
|
|
bind:value={config.wifi_security}
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-rayhunter-blue"
|
|
>
|
|
<option value="wpa_psk">WPA2 (WPA-PSK)</option>
|
|
<option value="sae">WPA3 (SAE)</option>
|
|
</select>
|
|
</div>
|
|
{/if}
|
|
|
|
<div>
|
|
<label
|
|
for="wifi_password"
|
|
class="block text-sm font-medium text-gray-700 mb-1"
|
|
>
|
|
WiFi Password
|
|
</label>
|
|
<input
|
|
id="wifi_password"
|
|
type="password"
|
|
bind:value={config.wifi_password}
|
|
placeholder="Enter password"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-rayhunter-blue"
|
|
/>
|
|
<p class="text-xs text-gray-500 mt-1">
|
|
Changing the network requires re-entering the password.
|
|
</p>
|
|
</div>
|
|
|
|
{#if config.wifi_ssid}
|
|
<div>
|
|
<label
|
|
for="dns_servers"
|
|
class="block text-sm font-medium text-gray-700 mb-1"
|
|
>
|
|
DNS Servers
|
|
</label>
|
|
<input
|
|
id="dns_servers"
|
|
type="text"
|
|
bind:value={dnsServersInput}
|
|
placeholder="9.9.9.9, 149.112.112.112"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-rayhunter-blue"
|
|
/>
|
|
<p class="text-xs text-gray-500 mt-1">
|
|
Comma-separated. Used when WiFi is active. Defaults to 9.9.9.9,
|
|
149.112.112.112 (Quad9).
|
|
</p>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="border-t border-gray-200 pt-4 mt-6">
|
|
<h3 class="text-lg font-semibold text-gray-800 mb-4">
|
|
Analyzer Heuristic Settings
|
|
</h3>
|
|
<div class="space-y-3">
|
|
<div class="flex items-center">
|
|
<input
|
|
id="imsi_requested"
|
|
type="checkbox"
|
|
bind:checked={config.analyzers.imsi_requested}
|
|
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded-sm"
|
|
/>
|
|
<label for="imsi_requested" class="ml-2 block text-sm text-gray-700">
|
|
IMSI Requested Heuristic
|
|
</label>
|
|
</div>
|
|
|
|
<div class="flex items-center">
|
|
<input
|
|
id="connection_redirect_2g_downgrade"
|
|
type="checkbox"
|
|
bind:checked={config.analyzers.connection_redirect_2g_downgrade}
|
|
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded-sm"
|
|
/>
|
|
<label
|
|
for="connection_redirect_2g_downgrade"
|
|
class="ml-2 block text-sm text-gray-700"
|
|
>
|
|
Connection Redirect 2G Downgrade Heuristic
|
|
</label>
|
|
</div>
|
|
|
|
<div class="flex items-center">
|
|
<input
|
|
id="lte_sib6_and_7_downgrade"
|
|
type="checkbox"
|
|
bind:checked={config.analyzers.lte_sib6_and_7_downgrade}
|
|
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded-sm"
|
|
/>
|
|
<label
|
|
for="lte_sib6_and_7_downgrade"
|
|
class="ml-2 block text-sm text-gray-700"
|
|
>
|
|
LTE SIB6 and SIB7 Downgrade Heuristic
|
|
</label>
|
|
</div>
|
|
|
|
<div class="flex items-center">
|
|
<input
|
|
id="null_cipher"
|
|
type="checkbox"
|
|
bind:checked={config.analyzers.null_cipher}
|
|
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded-sm"
|
|
/>
|
|
<label for="null_cipher" class="ml-2 block text-sm text-gray-700">
|
|
Null Cipher Heuristic
|
|
</label>
|
|
</div>
|
|
|
|
<div class="flex items-center">
|
|
<input
|
|
id="nas_null_cipher"
|
|
type="checkbox"
|
|
bind:checked={config.analyzers.nas_null_cipher}
|
|
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded-sm"
|
|
/>
|
|
<label for="nas_null_cipher" class="ml-2 block text-sm text-gray-700">
|
|
NAS Null Cipher Heuristic
|
|
</label>
|
|
</div>
|
|
|
|
<div class="flex items-center">
|
|
<input
|
|
id="incomplete_sib"
|
|
type="checkbox"
|
|
bind:checked={config.analyzers.incomplete_sib}
|
|
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded-sm"
|
|
/>
|
|
<label for="incomplete_sib" class="ml-2 block text-sm text-gray-700">
|
|
Incomplete SIB Heuristic
|
|
</label>
|
|
</div>
|
|
|
|
<div class="flex items-center">
|
|
<input
|
|
id="test_analyzer"
|
|
type="checkbox"
|
|
bind:checked={config.analyzers.test_analyzer}
|
|
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded-sm"
|
|
/>
|
|
<label for="test_analyzer" class="ml-2 block text-sm text-gray-700">
|
|
Test Heuristic (noisy!)
|
|
</label>
|
|
</div>
|
|
<div class="flex items-center">
|
|
<input
|
|
id="diagnostic_analyzer"
|
|
type="checkbox"
|
|
bind:checked={config.analyzers.diagnostic_analyzer}
|
|
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded-sm"
|
|
/>
|
|
<label
|
|
for="diagnostic_analyzer"
|
|
class="ml-2 block text-sm text-gray-700"
|
|
>
|
|
Diagnostic Analyzer
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<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">GPS Settings</h3>
|
|
<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={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 === GpsMode.Api}
|
|
POST latitude, longitude, and timestamp to <code>/api/gps</code> from
|
|
any device on the network.
|
|
{: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 === 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
|
|
bind:value={config.gps_fixed_latitude}
|
|
placeholder="e.g. 37.7749"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
|
|
/>
|
|
<p class="text-xs text-gray-500 mt-1">Decimal degrees, -90 to 90</p>
|
|
</div>
|
|
<div>
|
|
<label
|
|
for="gps_fixed_longitude"
|
|
class="block text-sm font-medium text-gray-700 mb-1"
|
|
>Fixed Longitude</label
|
|
>
|
|
<input
|
|
id="gps_fixed_longitude"
|
|
type="number"
|
|
min="-180"
|
|
max="180"
|
|
step="any"
|
|
required
|
|
bind:value={config.gps_fixed_longitude}
|
|
placeholder="e.g. -122.4194"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
|
|
/>
|
|
<p class="text-xs text-gray-500 mt-1">Decimal degrees, -180 to 180</p>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="flex gap-2 pt-4">
|
|
<button
|
|
type="submit"
|
|
disabled={saving}
|
|
class="bg-blue-500 hover:bg-blue-700 disabled:opacity-50 text-white font-bold py-2 px-4 rounded-md flex flex-row gap-1 items-center"
|
|
>
|
|
{#if saving}
|
|
<div
|
|
class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"
|
|
></div>
|
|
Saving...
|
|
{:else}
|
|
<svg
|
|
class="w-4 h-4"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M5 13l4 4L19 7"
|
|
></path>
|
|
</svg>
|
|
Apply and restart
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
{#if message}
|
|
<div
|
|
class="mt-4 p-3 rounded-sm {messageType === 'error'
|
|
? 'bg-red-100 text-red-700'
|
|
: 'bg-green-100 text-green-700'}"
|
|
>
|
|
{message}
|
|
</div>
|
|
{/if}
|
|
{:else}
|
|
<div class="text-center py-4 text-red-600">
|
|
Failed to load configuration. Please try reloading the page.
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</Modal>
|