Inital Commit

This commit is contained in:
2026-02-22 00:50:51 +00:00
commit 4c24b141fe
28 changed files with 1458 additions and 0 deletions

471
src/routes/+page.svelte Normal file
View File

@@ -0,0 +1,471 @@
<script lang="ts">
import { onMount } from "svelte";
let simulations: Array<{
id: number;
name: string;
created_at: string;
config: string;
}> = $state([]);
let error = $state("");
let searchQuery = $state("");
let filterDateFrom = $state("");
let filterDateTo = $state("");
let filterAltMin = $state("");
let filterAltMax = $state("");
let filterUavMin = $state("");
let filterUavMax = $state("");
let filterUgvMin = $state("");
let filterUgvMax = $state("");
let filterPattern = $state("any");
let sortOrder = $state("newest");
let currentPage = $state(1);
let itemsPerPage = 10;
function parseConfig(config: string) {
if (!config)
return {
altitude: null,
uavMin: null,
uavMax: null,
ugvMin: null,
ugvMax: null,
};
let alt = config.match(/altitude:\s*([\d\.]+)/);
let uavMin = config.match(/## UAV[\s\S]*?min_mph:\s*([\d\.]+)/);
let uavMax = config.match(/## UAV[\s\S]*?max_mph:\s*([\d\.]+)/);
let ugvMin = config.match(/## UGV[\s\S]*?min_mph:\s*([\d\.]+)/);
let ugvMax = config.match(/## UGV[\s\S]*?max_mph:\s*([\d\.]+)/);
let pattern = config.match(/(spiral|lawnmower|levy):/);
return {
altitude: alt ? parseFloat(alt[1]) : null,
uavMin: uavMin ? parseFloat(uavMin[1]) : null,
uavMax: uavMax ? parseFloat(uavMax[1]) : null,
ugvMin: ugvMin ? parseFloat(ugvMin[1]) : null,
ugvMax: ugvMax ? parseFloat(ugvMax[1]) : null,
pattern: pattern ? pattern[1] : null,
};
}
let filteredSimulations = $derived(
simulations.filter((sim) => {
let matchText = sim.id.toString().includes(searchQuery);
if (!matchText) return false;
let simDate = new Date(sim.created_at).getTime();
if (filterDateFrom && simDate < new Date(filterDateFrom).getTime())
return false;
if (
filterDateTo &&
simDate > new Date(filterDateTo).getTime() + 86400000
)
return false;
let p = parseConfig(sim.config);
if (
filterAltMin &&
p.altitude !== null &&
p.altitude < parseFloat(filterAltMin)
)
return false;
if (
filterAltMax &&
p.altitude !== null &&
p.altitude > parseFloat(filterAltMax)
)
return false;
if (
filterUavMin &&
p.uavMin !== null &&
p.uavMin < parseFloat(filterUavMin)
)
return false;
if (
filterUavMax &&
p.uavMax !== null &&
p.uavMax > parseFloat(filterUavMax)
)
return false;
if (
filterUgvMin &&
p.ugvMin !== null &&
p.ugvMin < parseFloat(filterUgvMin)
)
return false;
if (
filterUgvMax &&
p.ugvMax !== null &&
p.ugvMax > parseFloat(filterUgvMax)
)
return false;
if (filterPattern !== "any" && p.pattern !== filterPattern)
return false;
return true;
}),
);
let sortedSimulations = $derived(
[...filteredSimulations].sort((a, b) => {
if (sortOrder === "newest") {
return (
new Date(b.created_at).getTime() -
new Date(a.created_at).getTime()
);
} else if (sortOrder === "oldest") {
return (
new Date(a.created_at).getTime() -
new Date(b.created_at).getTime()
);
} else if (sortOrder === "fastest" || sortOrder === "slowest") {
let pA = parseConfig(a.config);
let pB = parseConfig(b.config);
let speedA = (pA.uavMax || 0) + (pA.ugvMax || 0);
let speedB = (pB.uavMax || 0) + (pB.ugvMax || 0);
if (sortOrder === "fastest") {
return speedB - speedA;
} else {
return speedA - speedB;
}
}
return 0;
}),
);
let totalPages = $derived(
Math.max(1, Math.ceil(sortedSimulations.length / itemsPerPage)),
);
let paginatedSimulations = $derived(
sortedSimulations.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage,
),
);
function goToPage(page: number) {
if (page >= 1 && page <= totalPages) {
currentPage = page;
}
}
$effect(() => {
if (
searchQuery ||
sortOrder ||
filterDateFrom ||
filterDateTo ||
filterAltMin ||
filterAltMax ||
filterUavMin ||
filterUavMax ||
filterUgvMin ||
filterUgvMax ||
filterPattern
) {
currentPage = 1;
}
});
onMount(async () => {
try {
const res = await fetch("/api/simulations");
if (!res.ok) throw new Error("Failed to fetch simulations");
simulations = await res.json();
} catch (e: any) {
error = e.message;
}
});
</script>
<h1>Sim-Link Simulation Results</h1>
{#if error}
<p class="error">Error: {error}</p>
{/if}
<div class="page-layout">
<aside class="sidebar">
<details open class="filter-group">
<summary>Search & Sort</summary>
<div class="filter-content">
<input
type="text"
placeholder="Search ID..."
bind:value={searchQuery}
class="input-field full-width"
/>
<select bind:value={sortOrder} class="input-field full-width">
<option value="newest">Newest First</option>
<option value="oldest">Oldest First</option>
<option value="fastest">Fastest</option>
<option value="slowest">Slowest</option>
</select>
</div>
</details>
<details class="filter-group">
<summary>Algorithm Pattern</summary>
<div class="filter-content">
<select
bind:value={filterPattern}
class="input-field full-width"
>
<option value="any">Any</option>
<option value="spiral">Spiral</option>
<option value="lawnmower">Lawnmower</option>
<option value="levy">Levy</option>
</select>
</div>
</details>
<details class="filter-group">
<summary>Date Range</summary>
<div class="filter-content col">
<label
>From <input
type="date"
bind:value={filterDateFrom}
class="input-field"
/></label
>
<label
>To <input
type="date"
bind:value={filterDateTo}
class="input-field"
/></label
>
</div>
</details>
<details class="filter-group">
<summary>Flight Altitude</summary>
<div class="filter-content col">
<label
>Min (ft) <input
type="number"
step="0.1"
bind:value={filterAltMin}
class="input-field small-num"
/></label
>
<label
>Max (ft) <input
type="number"
step="0.1"
bind:value={filterAltMax}
class="input-field small-num"
/></label
>
</div>
</details>
<details class="filter-group">
<summary>UAV Speed</summary>
<div class="filter-content col">
<label
>Min (mph) <input
type="number"
step="0.1"
bind:value={filterUavMin}
class="input-field small-num"
/></label
>
<label
>Max (mph) <input
type="number"
step="0.1"
bind:value={filterUavMax}
class="input-field small-num"
/></label
>
</div>
</details>
<details class="filter-group">
<summary>UGV Speed</summary>
<div class="filter-content col">
<label
>Min (mph) <input
type="number"
step="0.1"
bind:value={filterUgvMin}
class="input-field small-num"
/></label
>
<label
>Max (mph) <input
type="number"
step="0.1"
bind:value={filterUgvMax}
class="input-field small-num"
/></label
>
</div>
</details>
</aside>
<main class="table-content">
<table>
<thead>
<tr>
<th>ID</th>
<th>Sim Name</th>
<th>Date Run</th>
<th>Link</th>
</tr>
</thead>
<tbody>
{#each paginatedSimulations as sim}
<tr>
<td>{sim.id}</td>
<td>{sim.name}</td>
<td>{sim.created_at}</td>
<td
><a href={`/simulation/${sim.id}`}>View Details</a
></td
>
</tr>
{/each}
</tbody>
</table>
<div class="pagination">
<button
onclick={() => goToPage(currentPage - 1)}
disabled={currentPage === 1}
>
Previous
</button>
<span>Page {currentPage} of {totalPages}</span>
<button
onclick={() => goToPage(currentPage + 1)}
disabled={currentPage === totalPages}
>
Next
</button>
</div>
</main>
</div>
<style>
:global(body) {
font-family: "JetBrains Mono", Courier, monospace;
background-color: #ffffff;
color: #000000;
}
.page-layout {
display: flex;
gap: 30px;
align-items: flex-start;
}
.sidebar {
width: 300px;
flex-shrink: 0;
}
.table-content {
flex-grow: 1;
overflow-x: auto;
}
table {
border-collapse: collapse;
width: 100%;
margin-bottom: 20px;
}
th,
td {
border: 1px solid #000000;
padding: 12px;
text-align: left;
}
th {
background-color: #e0e0e0;
}
tbody tr:nth-child(even) {
background-color: #f9f9f9;
}
h1 {
font-size: 28px;
margin-bottom: 30px;
padding-bottom: 10px;
border-bottom: 2px solid #000;
}
a {
color: #0000ff;
text-decoration: underline;
}
a:visited {
color: #800080;
}
.error {
color: red;
font-weight: bold;
margin-bottom: 20px;
}
.filter-group {
border: 1px solid #000;
margin-bottom: 10px;
background-color: #f9f9f9;
}
summary {
font-weight: bold;
padding: 10px;
cursor: pointer;
background-color: #e0e0e0;
border-bottom: 1px solid #000;
}
details:not([open]) summary {
border-bottom: none;
}
.filter-content {
padding: 15px;
display: flex;
flex-direction: column;
gap: 15px;
}
.filter-content.col {
gap: 10px;
}
.full-width {
width: 100%;
box-sizing: border-box;
}
label {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
font-size: 14px;
font-weight: bold;
}
.input-field {
padding: 5px;
border: 1px solid #000;
font-family: inherit;
}
.small-num {
width: 70px;
}
.pagination {
margin-top: 20px;
display: flex;
align-items: center;
gap: 15px;
}
button {
padding: 5px 10px;
border: 1px solid #000;
background-color: #e0e0e0;
cursor: pointer;
font-family: inherit;
}
button:disabled {
background-color: #f0f0f0;
color: #888;
cursor: not-allowed;
}
</style>