mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-04-30 09:29:58 -07:00
run prettier
This commit is contained in:
committed by
Will Greenberg
parent
41133ba793
commit
e5c0e13d32
@@ -1,33 +1,84 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { EventType, parse_finished_report, Severity, type QualitativeWarning } from './analysis.svelte';
|
||||
import {
|
||||
EventType,
|
||||
parse_finished_report,
|
||||
Severity,
|
||||
type QualitativeWarning,
|
||||
} from './analysis.svelte';
|
||||
import { parse_ndjson, type NewlineDeliminatedJson } from './ndjson';
|
||||
|
||||
const SAMPLE_REPORT_NDJSON: NewlineDeliminatedJson = [
|
||||
{ "analyzers": [{ "name": "LTE SIB 6/7 Downgrade", "description": "Tests for LTE cells broadcasting a SIB type 6 and 7 which include 2G/3G frequencies with higher priorities." }, { "name": "IMSI Provided", "description": "Tests whether the UE's IMSI was ever provided to the cell" }, { "name": "Null Cipher", "description": "Tests whether the cell suggests using a null cipher (EEA0)" }, { "name": "Example Analyzer", "description": "Always returns true, if you are seeing this you are either a developer or you are about to have problems." }] },
|
||||
{ "timestamp": "2024-10-08T13:25:43.011689003-07:00", "skipped_message_reasons": ["DecodingError(UperDecodeError(Error { cause: BufferTooShort, msg: \"PerCodec:DecodeError:Requested Bits to decode 3, Remaining bits 1\", context: [] }))"], "analysis": [] },
|
||||
{ "timestamp": "2024-10-08T13:25:43.480872496-07:00", "skipped_message_reasons": [], "analysis": [{ "timestamp": "2024-08-19T03:33:54.318Z", "events": [null, null, null, { "event_type": { "type": "QualitativeWarning", "severity": "Low" }, "message": "TMSI was provided to cell" }] }] },
|
||||
{
|
||||
analyzers: [
|
||||
{
|
||||
name: 'LTE SIB 6/7 Downgrade',
|
||||
description:
|
||||
'Tests for LTE cells broadcasting a SIB type 6 and 7 which include 2G/3G frequencies with higher priorities.',
|
||||
},
|
||||
{
|
||||
name: 'IMSI Provided',
|
||||
description: "Tests whether the UE's IMSI was ever provided to the cell",
|
||||
},
|
||||
{
|
||||
name: 'Null Cipher',
|
||||
description: 'Tests whether the cell suggests using a null cipher (EEA0)',
|
||||
},
|
||||
{
|
||||
name: 'Example Analyzer',
|
||||
description:
|
||||
'Always returns true, if you are seeing this you are either a developer or you are about to have problems.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
timestamp: '2024-10-08T13:25:43.011689003-07:00',
|
||||
skipped_message_reasons: [
|
||||
'DecodingError(UperDecodeError(Error { cause: BufferTooShort, msg: "PerCodec:DecodeError:Requested Bits to decode 3, Remaining bits 1", context: [] }))',
|
||||
],
|
||||
analysis: [],
|
||||
},
|
||||
{
|
||||
timestamp: '2024-10-08T13:25:43.480872496-07:00',
|
||||
skipped_message_reasons: [],
|
||||
analysis: [
|
||||
{
|
||||
timestamp: '2024-08-19T03:33:54.318Z',
|
||||
events: [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
event_type: { type: 'QualitativeWarning', severity: 'Low' },
|
||||
message: 'TMSI was provided to cell',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
describe('analysis report parsing', () => {
|
||||
it('parses the example analysis', () => {
|
||||
it('parses the example analysis', () => {
|
||||
const report = parse_finished_report(SAMPLE_REPORT_NDJSON);
|
||||
expect(report.metadata.analyzers).toEqual([
|
||||
{
|
||||
"name":"LTE SIB 6/7 Downgrade",
|
||||
"description":"Tests for LTE cells broadcasting a SIB type 6 and 7 which include 2G/3G frequencies with higher priorities.",
|
||||
name: 'LTE SIB 6/7 Downgrade',
|
||||
description:
|
||||
'Tests for LTE cells broadcasting a SIB type 6 and 7 which include 2G/3G frequencies with higher priorities.',
|
||||
},
|
||||
{
|
||||
"name":"IMSI Provided",
|
||||
"description":"Tests whether the UE's IMSI was ever provided to the cell",
|
||||
name: 'IMSI Provided',
|
||||
description: "Tests whether the UE's IMSI was ever provided to the cell",
|
||||
},
|
||||
{
|
||||
"name":"Null Cipher",
|
||||
"description":"Tests whether the cell suggests using a null cipher (EEA0)",
|
||||
name: 'Null Cipher',
|
||||
description: 'Tests whether the cell suggests using a null cipher (EEA0)',
|
||||
},
|
||||
{
|
||||
"name":"Example Analyzer",
|
||||
"description":"Always returns true, if you are seeing this you are either a developer or you are about to have problems.",
|
||||
}
|
||||
name: 'Example Analyzer',
|
||||
description:
|
||||
'Always returns true, if you are seeing this you are either a developer or you are about to have problems.',
|
||||
},
|
||||
]);
|
||||
expect(report.rows).toHaveLength(2);
|
||||
expect(report.rows[0].skipped_message_reasons).toHaveLength(1);
|
||||
@@ -41,5 +92,5 @@ describe('analysis report parsing', () => {
|
||||
} else {
|
||||
throw 'wrong event type';
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { parse_ndjson, type NewlineDeliminatedJson } from "./ndjson";
|
||||
import { req } from "./utils.svelte";
|
||||
import { parse_ndjson, type NewlineDeliminatedJson } from './ndjson';
|
||||
import { req } from './utils.svelte';
|
||||
|
||||
export type AnalysisReport = {
|
||||
metadata: ReportMetadata;
|
||||
@@ -11,7 +11,7 @@ export type ReportStatistics = {
|
||||
num_warnings: number;
|
||||
num_informational_logs: number;
|
||||
num_skipped_packets: number;
|
||||
}
|
||||
};
|
||||
|
||||
export type ReportMetadata = {
|
||||
analyzers: AnalyzerMetadata[];
|
||||
@@ -69,10 +69,11 @@ export function parse_finished_report(report_json: NewlineDeliminatedJson): Anal
|
||||
let num_skipped_packets = 0;
|
||||
const rows: AnalysisRow[] = report_json.slice(1).map((row_json: any) => {
|
||||
const analysis: PacketAnalysis[] = row_json.analysis.map((analysis_json: any) => {
|
||||
const events: Event[] = analysis_json.events.map((event_json: any): Event | null => {
|
||||
const events: Event[] = analysis_json.events
|
||||
.map((event_json: any): Event | null => {
|
||||
if (event_json === null) {
|
||||
return null;
|
||||
} else if (event_json.event_type.type === "Informational") {
|
||||
} else if (event_json.event_type.type === 'Informational') {
|
||||
num_informational_logs += 1;
|
||||
return {
|
||||
type: EventType.Informational,
|
||||
@@ -82,8 +83,12 @@ export function parse_finished_report(report_json: NewlineDeliminatedJson): Anal
|
||||
num_warnings += 1;
|
||||
return {
|
||||
type: EventType.Warning,
|
||||
severity: event_json.event_type.severity === "High" ? Severity.High :
|
||||
event_json.event_type.severity === "Medium" ? Severity.Medium : Severity.Low,
|
||||
severity:
|
||||
event_json.event_type.severity === 'High'
|
||||
? Severity.High
|
||||
: event_json.event_type.severity === 'Medium'
|
||||
? Severity.Medium
|
||||
: Severity.Low,
|
||||
message: event_json.message,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { get_report, type AnalysisReport } from "./analysis.svelte";
|
||||
import type { Manifest, ManifestEntry } from "./manifest.svelte";
|
||||
import { req } from "./utils.svelte";
|
||||
import { get_report, type AnalysisReport } from './analysis.svelte';
|
||||
import type { Manifest, ManifestEntry } from './manifest.svelte';
|
||||
import { req } from './utils.svelte';
|
||||
|
||||
export enum AnalysisStatus {
|
||||
// rayhunter is currently analyzing this entry (note that this is distinct
|
||||
@@ -19,8 +19,8 @@ type AnalysisStatusJson = {
|
||||
};
|
||||
|
||||
export type AnalysisResult = {
|
||||
name: string,
|
||||
status: AnalysisStatus,
|
||||
name: string;
|
||||
status: AnalysisStatus;
|
||||
};
|
||||
|
||||
export class AnalysisManager {
|
||||
@@ -53,11 +53,13 @@ export class AnalysisManager {
|
||||
|
||||
// fetch the analysis report
|
||||
this.reports.delete(entry);
|
||||
get_report(entry).then(report => {
|
||||
this.reports.set(entry, report);
|
||||
}).catch(err => {
|
||||
this.reports.set(entry, `Failed to get analysis: ${err}`);
|
||||
});
|
||||
get_report(entry)
|
||||
.then((report) => {
|
||||
this.reports.set(entry, report);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.reports.set(entry, `Failed to get analysis: ${err}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { AnalysisStatus } from "$lib/analysisManager.svelte";
|
||||
import { EventType } from "$lib/analysis.svelte";
|
||||
import type { ManifestEntry } from "$lib/manifest.svelte";
|
||||
let { entry, onclick, analysis_visible}: {
|
||||
entry: ManifestEntry,
|
||||
onclick: () => void,
|
||||
analysis_visible: boolean,
|
||||
import { AnalysisStatus } from '$lib/analysisManager.svelte';
|
||||
import { EventType } from '$lib/analysis.svelte';
|
||||
import type { ManifestEntry } from '$lib/manifest.svelte';
|
||||
let {
|
||||
entry,
|
||||
onclick,
|
||||
analysis_visible,
|
||||
}: {
|
||||
entry: ManifestEntry;
|
||||
onclick: () => void;
|
||||
analysis_visible: boolean;
|
||||
} = $props();
|
||||
|
||||
let summary = $derived.by(() => {
|
||||
@@ -16,7 +20,7 @@
|
||||
} else if (entry.analysis_status === AnalysisStatus.Finished) {
|
||||
if (entry.analysis_report === undefined) {
|
||||
return 'Loading...';
|
||||
} else if (typeof(entry.analysis_report) === 'string') {
|
||||
} else if (typeof entry.analysis_report === 'string') {
|
||||
return entry.analysis_report;
|
||||
} else {
|
||||
let num_warnings = 0;
|
||||
@@ -40,13 +44,32 @@
|
||||
let finished = entry.analysis_status === AnalysisStatus.Finished;
|
||||
let report_available = entry.analysis_report !== undefined;
|
||||
return finished && report_available;
|
||||
})
|
||||
});
|
||||
|
||||
let button_class = $derived(ready ? "text-blue-600 border rounded-full px-2" : '');
|
||||
let button_class = $derived(ready ? 'text-blue-600 border rounded-full px-2' : '');
|
||||
</script>
|
||||
|
||||
<button class="flex flex-row gap-1 lg:gap-2" disabled={!ready} {onclick}>
|
||||
<span class="{button_class} {entry.get_num_warnings() < 1 ? 'text-green-700 border-green-500 bg-green-200' : 'text-red-700 border-red-500 bg-red-200'}">{summary}</span>
|
||||
<svg class="w-6 h-6 text-gray-800 transition-transform {analysis_visible ? 'rotate-180' : ''}" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 9-7 7-7-7"/>
|
||||
<span
|
||||
class="{button_class} {entry.get_num_warnings() < 1
|
||||
? 'text-green-700 border-green-500 bg-green-200'
|
||||
: 'text-red-700 border-red-500 bg-red-200'}">{summary}</span
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6 text-gray-800 transition-transform {analysis_visible ? 'rotate-180' : ''}"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m19 9-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</button>
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { AnalysisStatus } from "$lib/analysisManager.svelte";
|
||||
import { EventType, type AnalyzerMetadata, type ReportMetadata, type AnalysisRow, type AnalysisReport } from "$lib/analysis.svelte";
|
||||
import type { ManifestEntry } from "$lib/manifest.svelte";
|
||||
let { report }: {
|
||||
report: AnalysisReport,
|
||||
import { AnalysisStatus } from '$lib/analysisManager.svelte';
|
||||
import {
|
||||
EventType,
|
||||
type AnalyzerMetadata,
|
||||
type ReportMetadata,
|
||||
type AnalysisRow,
|
||||
type AnalysisReport,
|
||||
} from '$lib/analysis.svelte';
|
||||
import type { ManifestEntry } from '$lib/manifest.svelte';
|
||||
let {
|
||||
report,
|
||||
}: {
|
||||
report: AnalysisReport;
|
||||
} = $props();
|
||||
|
||||
const date_formatter = new Intl.DateTimeFormat(undefined, {
|
||||
timeStyle: "long",
|
||||
dateStyle: "short",
|
||||
timeStyle: 'long',
|
||||
dateStyle: 'short',
|
||||
});
|
||||
|
||||
const skipped_messages: Map<string, number> = $derived.by(() => {
|
||||
@@ -25,6 +33,7 @@
|
||||
return map;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<p class="text-lg underline">Warnings and Informational Logs</p>
|
||||
{#if report.statistics.num_warnings === 0 && report.statistics.num_informational_logs === 0}
|
||||
@@ -42,19 +51,23 @@
|
||||
{#each report.rows as row, row_idx}
|
||||
{#each row.analysis as analysis}
|
||||
{@const parsed_date = new Date(analysis.timestamp)}
|
||||
{#each analysis.events.filter(e => e !== null) as event}
|
||||
{#each analysis.events.filter((e) => e !== null) as event}
|
||||
<tr class="even:bg-gray-200 odd:bg-white">
|
||||
{#if event.type === EventType.Warning}
|
||||
{@const severity = ['Low', 'Medium', 'High'][event.severity]}
|
||||
{@const severity_class = ['bg-red-200', 'bg-red-400', 'bg-red-600'][event.severity]}
|
||||
<td class="p-2">{date_formatter.format(parsed_date)}</td>
|
||||
<td class="p-2">{event.message}</td>
|
||||
<td class="p-2 {severity_class} text-center">{severity}</td>
|
||||
{:else if event.type === EventType.Informational}
|
||||
<td class="p-2">{date_formatter.format(parsed_date)}</td>
|
||||
<td class="p-2">{event.message}</td>
|
||||
<td class="p-2">Info</td>
|
||||
{/if}
|
||||
{#if event.type === EventType.Warning}
|
||||
{@const severity = ['Low', 'Medium', 'High'][event.severity]}
|
||||
{@const severity_class = [
|
||||
'bg-red-200',
|
||||
'bg-red-400',
|
||||
'bg-red-600',
|
||||
][event.severity]}
|
||||
<td class="p-2">{date_formatter.format(parsed_date)}</td>
|
||||
<td class="p-2">{event.message}</td>
|
||||
<td class="p-2 {severity_class} text-center">{severity}</td>
|
||||
{:else if event.type === EventType.Informational}
|
||||
<td class="p-2">{date_formatter.format(parsed_date)}</td>
|
||||
<td class="p-2">{event.message}</td>
|
||||
<td class="p-2">Info</td>
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
{/each}
|
||||
@@ -64,24 +77,27 @@
|
||||
{/if}
|
||||
</div>
|
||||
{#if report.statistics.num_skipped_packets > 0}
|
||||
<div>
|
||||
<p class="text-lg underline">Unparsed Messages</p>
|
||||
<p>These are due to a limitation or bug in Rayhunter's parser, and aren't ususally a problem.</p>
|
||||
<table class="table-auto text-left">
|
||||
<thead class="p-2">
|
||||
<tr class="bg-gray-300">
|
||||
<th scope="col" class="p-2">Total Msgs Affected</th>
|
||||
<th scope="col">Reason/Error</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each skipped_messages.entries() as [message, count]}
|
||||
<tr class="even:bg-gray-200 odd:bg-white">
|
||||
<td class="text-center">{count}</td>
|
||||
<td>{message}</td>
|
||||
<div>
|
||||
<p class="text-lg underline">Unparsed Messages</p>
|
||||
<p>
|
||||
These are due to a limitation or bug in Rayhunter's parser, and aren't ususally a
|
||||
problem.
|
||||
</p>
|
||||
<table class="table-auto text-left">
|
||||
<thead class="p-2">
|
||||
<tr class="bg-gray-300">
|
||||
<th scope="col" class="p-2">Total Msgs Affected</th>
|
||||
<th scope="col">Reason/Error</th>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each skipped_messages.entries() as [message, count]}
|
||||
<tr class="even:bg-gray-200 odd:bg-white">
|
||||
<td class="text-center">{count}</td>
|
||||
<td>{message}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { AnalysisStatus } from "$lib/analysisManager.svelte";
|
||||
import { EventType, type AnalyzerMetadata, type ReportMetadata, type AnalysisRow } from "$lib/analysis.svelte";
|
||||
import type { ManifestEntry } from "$lib/manifest.svelte";
|
||||
import AnalysisTable from "./AnalysisTable.svelte";
|
||||
let { entry }: {
|
||||
entry: ManifestEntry,
|
||||
import { AnalysisStatus } from '$lib/analysisManager.svelte';
|
||||
import {
|
||||
EventType,
|
||||
type AnalyzerMetadata,
|
||||
type ReportMetadata,
|
||||
type AnalysisRow,
|
||||
} from '$lib/analysis.svelte';
|
||||
import type { ManifestEntry } from '$lib/manifest.svelte';
|
||||
import AnalysisTable from './AnalysisTable.svelte';
|
||||
let {
|
||||
entry,
|
||||
}: {
|
||||
entry: ManifestEntry;
|
||||
} = $props();
|
||||
|
||||
const date_formatter = new Intl.DateTimeFormat(undefined, {
|
||||
timeStyle: "long",
|
||||
dateStyle: "short",
|
||||
timeStyle: 'long',
|
||||
dateStyle: 'short',
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="container mt-2">
|
||||
{#if entry.analysis_report === undefined}
|
||||
<p>Report unavailable, try refreshing.</p>
|
||||
{:else if typeof(entry.analysis_report) === 'string'}
|
||||
{:else if typeof entry.analysis_report === 'string'}
|
||||
<p>Error getting analysis report: {entry.analysis_report}</p>
|
||||
{:else}
|
||||
{@const metadata: ReportMetadata = entry.analysis_report.metadata}
|
||||
@@ -27,17 +34,17 @@
|
||||
<p>No warnings to display!</p>
|
||||
{/if}
|
||||
{#if metadata !== undefined && metadata.rayhunter !== undefined}
|
||||
<div>
|
||||
<p class="text-lg underline">Metadata</p>
|
||||
<p>Analysis by Rayhunter version {metadata.rayhunter.rayhunter_version}</p>
|
||||
<p><b>Device system OS:</b> {metadata.rayhunter.system_os}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-lg underline">Analyzers</p>
|
||||
{#each metadata.analyzers as analyzer}
|
||||
<p><b>{analyzer.name}:</b> {analyzer.description}</p>
|
||||
{/each}
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-lg underline">Metadata</p>
|
||||
<p>Analysis by Rayhunter version {metadata.rayhunter.rayhunter_version}</p>
|
||||
<p><b>Device system OS:</b> {metadata.rayhunter.system_os}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-lg underline">Analyzers</p>
|
||||
{#each metadata.analyzers as analyzer}
|
||||
<p><b>{analyzer.name}:</b> {analyzer.description}</p>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p>N/A (analysis generated by an older version of rayhunter)</p>
|
||||
{/if}
|
||||
|
||||
@@ -5,19 +5,19 @@
|
||||
|
||||
let loading = $state(false);
|
||||
let saving = $state(false);
|
||||
let message = $state("");
|
||||
let messageType = $state<"success" | "error" | null>(null);
|
||||
let message = $state('');
|
||||
let messageType = $state<'success' | 'error' | null>(null);
|
||||
let showConfig = $state(false);
|
||||
|
||||
async function loadConfig() {
|
||||
try {
|
||||
loading = true;
|
||||
config = await get_config();
|
||||
message = "";
|
||||
message = '';
|
||||
messageType = null;
|
||||
} catch (error) {
|
||||
message = `Failed to load config: ${error}`;
|
||||
messageType = "error";
|
||||
messageType = 'error';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
@@ -25,21 +25,21 @@
|
||||
|
||||
async function saveConfig() {
|
||||
if (!config) return;
|
||||
|
||||
|
||||
try {
|
||||
saving = true;
|
||||
await set_config(config);
|
||||
message = "Config saved successfully! Rayhunter is restarting now. Reload the page in a few seconds.";
|
||||
messageType = "success";
|
||||
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";
|
||||
messageType = 'error';
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Load config when first shown
|
||||
$effect(() => {
|
||||
if (showConfig && !config) {
|
||||
@@ -49,21 +49,33 @@
|
||||
</script>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-md p-6 m-4">
|
||||
<button
|
||||
<button
|
||||
class="w-full flex justify-between items-center text-xl font-bold mb-4 text-rayhunter-dark-blue hover:text-rayhunter-blue"
|
||||
onclick={() => showConfig = !showConfig}
|
||||
onclick={() => (showConfig = !showConfig)}
|
||||
>
|
||||
<span>Configuration</span>
|
||||
<svg class="w-6 h-6 transition-transform {showConfig ? 'rotate-180' : ''}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
<svg
|
||||
class="w-6 h-6 transition-transform {showConfig ? 'rotate-180' : ''}"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
|
||||
{#if showConfig}
|
||||
{#if loading}
|
||||
<div class="text-center py-4">Loading config...</div>
|
||||
{:else if config}
|
||||
<form class="space-y-4" onsubmit={(e) => { e.preventDefault(); saveConfig(); }}>
|
||||
<form
|
||||
class="space-y-4"
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
saveConfig();
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<label for="ui_level" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Device UI Level
|
||||
@@ -81,7 +93,10 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="key_input_mode" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
<label
|
||||
for="key_input_mode"
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Device Input Mode
|
||||
</label>
|
||||
<select
|
||||
@@ -90,7 +105,9 @@
|
||||
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}>0 - Disable button control</option>
|
||||
<option value={1}>1 - Double-tap power button to start/stop recording</option>
|
||||
<option value={1}
|
||||
>1 - Double-tap power button to start/stop recording</option
|
||||
>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -109,7 +126,9 @@
|
||||
</div>
|
||||
|
||||
<div class="border-t pt-4 mt-6">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4">Analyzer Heuristic Settings</h3>
|
||||
<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
|
||||
@@ -130,7 +149,10 @@
|
||||
bind:checked={config.analyzers.connection_redirect_2g_downgrade}
|
||||
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded"
|
||||
/>
|
||||
<label for="connection_redirect_2g_downgrade" class="ml-2 block text-sm text-gray-700">
|
||||
<label
|
||||
for="connection_redirect_2g_downgrade"
|
||||
class="ml-2 block text-sm text-gray-700"
|
||||
>
|
||||
Connection Redirect 2G Downgrade Heuristic
|
||||
</label>
|
||||
</div>
|
||||
@@ -142,7 +164,10 @@
|
||||
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"
|
||||
/>
|
||||
<label for="lte_sib6_and_7_downgrade" class="ml-2 block text-sm text-gray-700">
|
||||
<label
|
||||
for="lte_sib6_and_7_downgrade"
|
||||
class="ml-2 block text-sm text-gray-700"
|
||||
>
|
||||
LTE SIB6 and SIB7 Downgrade Heuristic
|
||||
</label>
|
||||
</div>
|
||||
@@ -168,20 +193,35 @@
|
||||
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>
|
||||
<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
|
||||
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 {messageType === 'error' ? 'bg-red-100 text-red-700' : 'bg-green-100 text-green-700'}">
|
||||
<div
|
||||
class="mt-4 p-3 rounded {messageType === 'error'
|
||||
? 'bg-red-100 text-red-700'
|
||||
: 'bg-green-100 text-green-700'}"
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { req } from "$lib/utils.svelte";
|
||||
import DeleteButton from "./DeleteButton.svelte";
|
||||
import { req } from '$lib/utils.svelte';
|
||||
import DeleteButton from './DeleteButton.svelte';
|
||||
|
||||
function confirmDelete() {
|
||||
if (window.confirm(`Permanently delete ALL recordings?`)) {
|
||||
req('POST', '/api/delete-all-recordings')
|
||||
req('POST', '/api/delete-all-recordings');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { ManifestEntry } from "$lib/manifest.svelte";
|
||||
import { req } from "$lib/utils.svelte";
|
||||
let { text, url, prompt }: {
|
||||
text?: string,
|
||||
url: string,
|
||||
prompt: string,
|
||||
import { ManifestEntry } from '$lib/manifest.svelte';
|
||||
import { req } from '$lib/utils.svelte';
|
||||
let {
|
||||
text,
|
||||
url,
|
||||
prompt,
|
||||
}: {
|
||||
text?: string;
|
||||
url: string;
|
||||
prompt: string;
|
||||
} = $props();
|
||||
|
||||
function confirmDelete() {
|
||||
if (window.confirm(prompt)) {
|
||||
req('POST', url)
|
||||
req('POST', url);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<button class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-md flex flex-row" onclick={confirmDelete} aria-label="delete">
|
||||
<button
|
||||
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-md flex flex-row"
|
||||
onclick={confirmDelete}
|
||||
aria-label="delete"
|
||||
>
|
||||
<p>{text}</p>
|
||||
<svg
|
||||
style="width:24px;height:24px"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<svg style="width:24px;height:24px" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="white"
|
||||
d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"
|
||||
fill="white"
|
||||
d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<script lang="ts">
|
||||
let { url, text, full_button=false }: {
|
||||
let {
|
||||
url,
|
||||
text,
|
||||
full_button = false,
|
||||
}: {
|
||||
url: string;
|
||||
text: string;
|
||||
full_button?: boolean;
|
||||
@@ -10,9 +14,14 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<button class="flex flex-row {full_button ? 'bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-md' : 'text-blue-600 underline'}" onclick={download}>
|
||||
<button
|
||||
class="flex flex-row {full_button
|
||||
? 'bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-md'
|
||||
: 'text-blue-600 underline'}"
|
||||
onclick={download}
|
||||
>
|
||||
{text}
|
||||
<svg class="fill-current w-4 h-4 m-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path d="M13 8V2H7v6H2l8 8 8-8h-5zM0 18h20v2H0v-2z"/>
|
||||
<path d="M13 8V2H7v6H2l8 8 8-8h-5zM0 18h20v2H0v-2z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { ManifestEntry } from "$lib/manifest.svelte";
|
||||
import { ManifestEntry } from '$lib/manifest.svelte';
|
||||
import DownloadLink from '$lib/components/DownloadLink.svelte';
|
||||
import DeleteButton from "$lib/components/DeleteButton.svelte";
|
||||
import AnalysisStatus from "./AnalysisStatus.svelte";
|
||||
import AnalysisView from "./AnalysisView.svelte";
|
||||
import RecordingControls from "./RecordingControls.svelte";
|
||||
let { entry, current, i, server_is_recording }: {
|
||||
import DeleteButton from '$lib/components/DeleteButton.svelte';
|
||||
import AnalysisStatus from './AnalysisStatus.svelte';
|
||||
import AnalysisView from './AnalysisView.svelte';
|
||||
import RecordingControls from './RecordingControls.svelte';
|
||||
let {
|
||||
entry,
|
||||
current,
|
||||
i,
|
||||
server_is_recording,
|
||||
}: {
|
||||
entry: ManifestEntry;
|
||||
current: boolean;
|
||||
i: number;
|
||||
@@ -14,52 +19,71 @@
|
||||
|
||||
// passing `undefined` as the locale uses the browser default
|
||||
const date_formatter = new Intl.DateTimeFormat(undefined, {
|
||||
timeStyle: "long",
|
||||
dateStyle: "short",
|
||||
timeStyle: 'long',
|
||||
dateStyle: 'short',
|
||||
});
|
||||
let status_row_color = $derived.by(() => {
|
||||
const num_warnings = entry.get_num_warnings();
|
||||
if (num_warnings !== undefined && num_warnings > 0) {
|
||||
return "bg-red-100";
|
||||
return 'bg-red-100';
|
||||
}
|
||||
return current ? "bg-green-100" : "bg-gray-100"
|
||||
return current ? 'bg-green-100' : 'bg-gray-100';
|
||||
});
|
||||
let status_border_color = $derived.by(() => {
|
||||
const num_warnings = entry.get_num_warnings();
|
||||
if (num_warnings !== undefined && num_warnings > 0) {
|
||||
return "border-red-100";
|
||||
return 'border-red-100';
|
||||
}
|
||||
return current ? "border-green-100" : "border-gray-100"
|
||||
return current ? 'border-green-100' : 'border-gray-100';
|
||||
});
|
||||
let analysis_visible = $state(false);
|
||||
function toggle_analysis_visibility() {
|
||||
analysis_visible = !analysis_visible;
|
||||
}
|
||||
</script>
|
||||
<div class="{status_row_color} {status_border_color} drop-shadow p-4 flex flex-col gap-2 border rounded-md flex-1">
|
||||
|
||||
<div
|
||||
class="{status_row_color} {status_border_color} drop-shadow p-4 flex flex-col gap-2 border rounded-md flex-1"
|
||||
>
|
||||
{#if current}
|
||||
<div class="flex flex-row justify-between gap-2">
|
||||
<span class="text-xl mb-2">Current Recording</span>
|
||||
<span class=""><AnalysisStatus onclick={toggle_analysis_visibility} entry={entry} analysis_visible={analysis_visible}/></span>
|
||||
<span class=""
|
||||
><AnalysisStatus
|
||||
onclick={toggle_analysis_visibility}
|
||||
{entry}
|
||||
{analysis_visible}
|
||||
/></span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-row justify-between">
|
||||
<span class="font-bold">ID: {entry.name}</span>
|
||||
{#if !current}
|
||||
<span class=""><AnalysisStatus onclick={toggle_analysis_visibility} entry={entry} analysis_visible={analysis_visible}/></span>
|
||||
<span class=""
|
||||
><AnalysisStatus
|
||||
onclick={toggle_analysis_visibility}
|
||||
{entry}
|
||||
{analysis_visible}
|
||||
/></span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="">{entry.get_readable_qmdl_size()}</span>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="">Start: {date_formatter.format(entry.start_time)}</span>
|
||||
<span class="">Last Message: {entry.last_message_time && date_formatter.format(entry.last_message_time) || "N/A"}</span>
|
||||
<span class=""
|
||||
>Last Message: {(entry.last_message_time &&
|
||||
date_formatter.format(entry.last_message_time)) ||
|
||||
'N/A'}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex flex-row justify-between lg:justify-end gap-2 mt-2">
|
||||
<DownloadLink url={entry.get_pcap_url()} text="pcap" full_button=true />
|
||||
<DownloadLink url={entry.get_qmdl_url()} text="qmdl" full_button=true />
|
||||
<DownloadLink url={entry.get_zip_url()} text="zip" full_button=true />
|
||||
<DownloadLink url={entry.get_pcap_url()} text="pcap" full_button="true" />
|
||||
<DownloadLink url={entry.get_qmdl_url()} text="qmdl" full_button="true" />
|
||||
<DownloadLink url={entry.get_zip_url()} text="zip" full_button="true" />
|
||||
{#if current}
|
||||
<RecordingControls {server_is_recording} />
|
||||
{:else}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Manifest, ManifestEntry } from "$lib/manifest.svelte";
|
||||
import TableRow from "./ManifestTableRow.svelte";
|
||||
import Card from "./ManifestCard.svelte"
|
||||
import { Manifest, ManifestEntry } from '$lib/manifest.svelte';
|
||||
import TableRow from './ManifestTableRow.svelte';
|
||||
import Card from './ManifestCard.svelte';
|
||||
interface Props {
|
||||
entries: ManifestEntry[];
|
||||
server_is_recording: boolean;
|
||||
@@ -13,15 +13,15 @@
|
||||
<table class="hidden table-auto text-left lg:table">
|
||||
<thead>
|
||||
<tr class="bg-gray-100 drop-shadow">
|
||||
<th class='p-2' scope="col">ID</th>
|
||||
<th class='p-2' scope="col">Started</th>
|
||||
<th class='p-2' scope="col">Last Message</th>
|
||||
<th class='p-2' scope="col">Size</th>
|
||||
<th class='p-2' scope="col">PCAP</th>
|
||||
<th class='p-2' scope="col">QMDL</th>
|
||||
<th class='p-2' scope="col">ZIP</th>
|
||||
<th class='p-2' scope="col">Analysis</th>
|
||||
<th class='p-2' scope="col"></th>
|
||||
<th class="p-2" scope="col">ID</th>
|
||||
<th class="p-2" scope="col">Started</th>
|
||||
<th class="p-2" scope="col">Last Message</th>
|
||||
<th class="p-2" scope="col">Size</th>
|
||||
<th class="p-2" scope="col">PCAP</th>
|
||||
<th class="p-2" scope="col">QMDL</th>
|
||||
<th class="p-2" scope="col">ZIP</th>
|
||||
<th class="p-2" scope="col">Analysis</th>
|
||||
<th class="p-2" scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -35,4 +35,4 @@
|
||||
{#each entries as entry, i}
|
||||
<Card {entry} current={false} {i} {server_is_recording} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { ManifestEntry } from "$lib/manifest.svelte";
|
||||
import { ManifestEntry } from '$lib/manifest.svelte';
|
||||
import DownloadLink from '$lib/components/DownloadLink.svelte';
|
||||
import DeleteButton from "$lib/components/DeleteButton.svelte";
|
||||
import AnalysisStatus from "./AnalysisStatus.svelte";
|
||||
import AnalysisView from "./AnalysisView.svelte";
|
||||
let { entry, current, i }: {
|
||||
import DeleteButton from '$lib/components/DeleteButton.svelte';
|
||||
import AnalysisStatus from './AnalysisStatus.svelte';
|
||||
import AnalysisView from './AnalysisView.svelte';
|
||||
let {
|
||||
entry,
|
||||
current,
|
||||
i,
|
||||
}: {
|
||||
entry: ManifestEntry;
|
||||
current: boolean;
|
||||
i: number;
|
||||
@@ -12,16 +16,16 @@
|
||||
|
||||
// passing `undefined` as the locale uses the browser default
|
||||
const date_formatter = new Intl.DateTimeFormat(undefined, {
|
||||
timeStyle: "long",
|
||||
dateStyle: "short",
|
||||
timeStyle: 'long',
|
||||
dateStyle: 'short',
|
||||
});
|
||||
let alternating_row_color = $derived(i % 2 == 0 ? "bg-white" : "bg-gray-100");
|
||||
let alternating_row_color = $derived(i % 2 == 0 ? 'bg-white' : 'bg-gray-100');
|
||||
let status_row_color = $derived.by(() => {
|
||||
const num_warnings = entry.get_num_warnings();
|
||||
if (num_warnings !== undefined && num_warnings > 0) {
|
||||
return "bg-red-100";
|
||||
return 'bg-red-100';
|
||||
}
|
||||
return current ? "bg-green-100" : alternating_row_color
|
||||
return current ? 'bg-green-100' : alternating_row_color;
|
||||
});
|
||||
let analysis_visible = $state(false);
|
||||
function toggle_analysis_visibility() {
|
||||
@@ -32,12 +36,16 @@
|
||||
<tr class="{status_row_color} drop-shadow">
|
||||
<td class="p-2">{entry.name}</td>
|
||||
<td class="p-2">{date_formatter.format(entry.start_time)}</td>
|
||||
<td class="p-2">{entry.last_message_time && date_formatter.format(entry.last_message_time) || "N/A"}</td>
|
||||
<td class="p-2"
|
||||
>{(entry.last_message_time && date_formatter.format(entry.last_message_time)) || 'N/A'}</td
|
||||
>
|
||||
<td class="p-2">{entry.get_readable_qmdl_size()}</td>
|
||||
<td class="p-2"><DownloadLink url={entry.get_pcap_url()} text="pcap" /></td>
|
||||
<td class="p-2"><DownloadLink url={entry.get_qmdl_url()} text="qmdl" /></td>
|
||||
<td class="p-2"><DownloadLink url={entry.get_zip_url()} text="zip" /></td>
|
||||
<td class="p-2"><AnalysisStatus onclick={toggle_analysis_visibility} entry={entry} analysis_visible={analysis_visible}/></td>
|
||||
<td class="p-2"
|
||||
><AnalysisStatus onclick={toggle_analysis_visibility} {entry} {analysis_visible} /></td
|
||||
>
|
||||
{#if current}
|
||||
<td class="p-2"></td>
|
||||
{:else}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { req } from "$lib/utils.svelte";
|
||||
let { server_is_recording }: {
|
||||
import { req } from '$lib/utils.svelte';
|
||||
let {
|
||||
server_is_recording,
|
||||
}: {
|
||||
server_is_recording: boolean;
|
||||
} = $props();
|
||||
|
||||
@@ -17,32 +19,78 @@
|
||||
client_set_recording = false;
|
||||
}
|
||||
|
||||
const recording_button_classes = "text-white font-bold py-2 px-4 rounded-md flex flex-row gap-1";
|
||||
const recording_button_classes =
|
||||
'text-white font-bold py-2 px-4 rounded-md flex flex-row gap-1';
|
||||
const stop_recording_classes = `${recording_button_classes} bg-red-500 opacity-50 cursor-not-allowed`;
|
||||
const start_recording_classes = `${recording_button_classes} bg-blue-500 opacity-50 cursor-not-allowed`;
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if waiting_for_server}
|
||||
<button class={server_is_recording ? stop_recording_classes : start_recording_classes} disabled>
|
||||
<span>{server_is_recording ? "Stopping..." : "Starting..."}</span>
|
||||
<svg class="w-4 h-4 text-white animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
<button
|
||||
class={server_is_recording ? stop_recording_classes : start_recording_classes}
|
||||
disabled
|
||||
>
|
||||
<span>{server_is_recording ? 'Stopping...' : 'Starting...'}</span>
|
||||
<svg
|
||||
class="w-4 h-4 text-white animate-spin"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
{:else if server_is_recording}
|
||||
<button class="{recording_button_classes} bg-red-500 hover:bg-red-700" onclick={stop_recording}>
|
||||
<button
|
||||
class="{recording_button_classes} bg-red-500 hover:bg-red-700"
|
||||
onclick={stop_recording}
|
||||
>
|
||||
<span>Stop</span>
|
||||
<svg class="w-6 h-6 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M7 5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H7Z"/>
|
||||
<svg
|
||||
class="w-6 h-6 text-white"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M7 5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H7Z" />
|
||||
</svg>
|
||||
</button>
|
||||
{:else}
|
||||
<button class="{recording_button_classes} bg-blue-500 hover:bg-blue-700" onclick={start_recording}>
|
||||
<button
|
||||
class="{recording_button_classes} bg-blue-500 hover:bg-blue-700"
|
||||
onclick={start_recording}
|
||||
>
|
||||
<span>Start</span>
|
||||
<svg class="w-6 h-6 text-white" 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="M8.6 5.2A1 1 0 0 0 7 6v12a1 1 0 0 0 1.6.8l8-6a1 1 0 0 0 0-1.6l-8-6Z" clip-rule="evenodd"/>
|
||||
<svg
|
||||
class="w-6 h-6 text-white"
|
||||
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="M8.6 5.2A1 1 0 0 0 7 6v12a1 1 0 0 0 1.6.8l8-6a1 1 0 0 0 0-1.6l-8-6Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -1,34 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { type SystemStats } from "$lib/systemStats";
|
||||
let { stats }: {
|
||||
import { type SystemStats } from '$lib/systemStats';
|
||||
let {
|
||||
stats,
|
||||
}: {
|
||||
stats: SystemStats;
|
||||
} = $props();
|
||||
|
||||
const table_cell_classes = "border p-1 lg:p-2";
|
||||
const table_cell_classes = 'border p-1 lg:p-2';
|
||||
</script>
|
||||
|
||||
<div class="flex-1 drop-shadow p-4 flex flex-col gap-2 border rounded-md bg-gray-100 border-gray-100">
|
||||
<div
|
||||
class="flex-1 drop-shadow p-4 flex flex-col gap-2 border rounded-md bg-gray-100 border-gray-100"
|
||||
>
|
||||
<p class="text-xl mb-2">System Information</p>
|
||||
<table class="table-auto border">
|
||||
<tbody>
|
||||
<tr class="border">
|
||||
<th class={table_cell_classes}>
|
||||
Rayhunter Version
|
||||
</th>
|
||||
<th class={table_cell_classes}> Rayhunter Version </th>
|
||||
<td class={table_cell_classes}>{stats.runtime_metadata.rayhunter_version}</td>
|
||||
</tr>
|
||||
<tr class="border">
|
||||
<th class={table_cell_classes}>
|
||||
Storage
|
||||
</th>
|
||||
<th class={table_cell_classes}> Storage </th>
|
||||
<td class={table_cell_classes}>
|
||||
{stats.disk_stats.used_percent} used ({stats.disk_stats.used_size} used / {stats.disk_stats.available_size} available)
|
||||
{stats.disk_stats.used_percent} used ({stats.disk_stats.used_size} used / {stats
|
||||
.disk_stats.available_size} available)
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b">
|
||||
<th class={table_cell_classes}>
|
||||
Memory (RAM)
|
||||
</th>
|
||||
<th class={table_cell_classes}> Memory (RAM) </th>
|
||||
<td class={table_cell_classes}>
|
||||
Free: {stats.memory_stats.free}, Used: {stats.memory_stats.used}
|
||||
</td>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { get_report, type AnalysisReport } from "./analysis.svelte";
|
||||
import { AnalysisStatus, type AnalysisManager } from "./analysisManager.svelte";
|
||||
import { get_report, type AnalysisReport } from './analysis.svelte';
|
||||
import { AnalysisStatus, type AnalysisManager } from './analysisManager.svelte';
|
||||
|
||||
interface JsonManifest {
|
||||
entries: JsonManifestEntry[];
|
||||
@@ -39,7 +39,7 @@ export class Manifest {
|
||||
if (this.current_entry) {
|
||||
try {
|
||||
this.current_entry.analysis_report = await get_report(this.current_entry.name);
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
this.current_entry.analysis_report = `Err: failed to get analysis report: ${err}`;
|
||||
}
|
||||
|
||||
@@ -47,11 +47,11 @@ export class Manifest {
|
||||
// analysis report is always available
|
||||
this.current_entry.analysis_status = AnalysisStatus.Finished;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ManifestEntry {
|
||||
public name = $state("");
|
||||
public name = $state('');
|
||||
public start_time: Date;
|
||||
public last_message_time: Date | undefined = $state(undefined);
|
||||
public qmdl_size_bytes = $state(0);
|
||||
@@ -70,16 +70,16 @@ export class ManifestEntry {
|
||||
}
|
||||
|
||||
get_readable_qmdl_size(): string {
|
||||
if (this.qmdl_size_bytes === 0) return "0 Bytes";
|
||||
if (this.qmdl_size_bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const dm = 2 || 2;
|
||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
const i = Math.floor(Math.log(this.qmdl_size_bytes) / Math.log(k));
|
||||
return `${Number.parseFloat((this.qmdl_size_bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
|
||||
}
|
||||
}
|
||||
|
||||
get_num_warnings(): number | undefined {
|
||||
if (this.analysis_report === undefined || typeof(this.analysis_report) === 'string') {
|
||||
if (this.analysis_report === undefined || typeof this.analysis_report === 'string') {
|
||||
return undefined;
|
||||
}
|
||||
return this.analysis_report.statistics.num_warnings;
|
||||
@@ -103,5 +103,5 @@ export class ManifestEntry {
|
||||
|
||||
get_delete_url(): string {
|
||||
return `/api/delete-recording/${this.name}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,32 +2,32 @@ import { describe, it, expect } from 'vitest';
|
||||
import { parse_ndjson } from './ndjson';
|
||||
|
||||
describe('parsing newline-deliminated json', () => {
|
||||
it('parses normal JSON', () => {
|
||||
it('parses normal JSON', () => {
|
||||
const json = JSON.stringify({ foo: 100 });
|
||||
const result = parse_ndjson(json);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({ foo: 100 });
|
||||
});
|
||||
});
|
||||
|
||||
it('parses simple newline-deliminated json', () => {
|
||||
it('parses simple newline-deliminated json', () => {
|
||||
const json_a = JSON.stringify({ a: 100 });
|
||||
const json_b = JSON.stringify({ b: 200 });
|
||||
const result = parse_ndjson(`${json_a}\n${json_b}`);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toEqual({ a: 100 });
|
||||
expect(result[1]).toEqual({ b: 200 });
|
||||
})
|
||||
});
|
||||
|
||||
it('parses newline-deliminated json with escaped newlines within', () => {
|
||||
const json_a = JSON.stringify({ a: 'this one has\n newlines and\nstuff' });
|
||||
it('parses newline-deliminated json with escaped newlines within', () => {
|
||||
const json_a = JSON.stringify({ a: 'this one has\n newlines and\nstuff' });
|
||||
const json_b = JSON.stringify({ b: 200 });
|
||||
const result = parse_ndjson(`${json_a}\n${json_b}`);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toEqual({ a: 'this one has\n newlines and\nstuff' });
|
||||
expect(result[1]).toEqual({ b: 200 });
|
||||
})
|
||||
});
|
||||
|
||||
it('actually errors out on invalid ndjson', () => {
|
||||
expect(() => parse_ndjson("invalid\njson")).toThrow();
|
||||
expect(() => parse_ndjson('invalid\njson')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,22 +5,22 @@ export interface SystemStats {
|
||||
}
|
||||
|
||||
export interface RuntimeMetadata {
|
||||
rayhunter_version: string,
|
||||
system_os: string,
|
||||
arch: string,
|
||||
rayhunter_version: string;
|
||||
system_os: string;
|
||||
arch: string;
|
||||
}
|
||||
|
||||
export interface DiskStats {
|
||||
partition: string,
|
||||
total_size: string,
|
||||
used_size: string,
|
||||
available_size: string,
|
||||
used_percent: string,
|
||||
mounted_on: string,
|
||||
partition: string;
|
||||
total_size: string;
|
||||
used_size: string;
|
||||
available_size: string;
|
||||
used_percent: string;
|
||||
mounted_on: string;
|
||||
}
|
||||
|
||||
export interface MemoryStats {
|
||||
total: string,
|
||||
used: string,
|
||||
free: string,
|
||||
total: string;
|
||||
used: string;
|
||||
free: string;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Manifest } from "./manifest.svelte";
|
||||
import type { SystemStats } from "./systemStats";
|
||||
import { Manifest } from './manifest.svelte';
|
||||
import type { SystemStats } from './systemStats';
|
||||
|
||||
export interface AnalyzerConfig {
|
||||
imsi_requested: boolean;
|
||||
@@ -46,9 +46,9 @@ export async function set_config(config: Config): Promise<void> {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(config)
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(error);
|
||||
|
||||
Reference in New Issue
Block a user