SAR Search Pattern Update
This commit is contained in:
@@ -50,7 +50,7 @@ Returns a JSON array of all indexed simulations, including their ID, name, creat
|
||||
|
||||
### `POST /api/simulations/create`
|
||||
Creates a new simulation entry.
|
||||
- **Request Body:** JSON object containing the combined configurations (e.g., `search`, `uav`, `ugv`). The configuration *must* contain at least one search pattern definition (either `spiral`, `lawnmower`, or `levy`) inside the JSON.
|
||||
- **Request Body:** JSON object containing the combined configurations (e.g., `search`, `uav`, `ugv`). The configuration *must* contain at least one search pattern definition (either `spiral`, `lawnmower`, `levy`, or `sar`) inside the JSON.
|
||||
- **Behavior:** Checks if an identical config exists. If so, it returns the existing simulation ID/Name for overwriting. If not, it allocates a new `simulation_X` incrementally, inserts it into the database, builds the file directory in `../results`, and returns the new ID/Name. Returns `400 Bad Request` if payload is not valid JSON or lacks a known search pattern.
|
||||
- **Response:** JSON object `{"id": <int>, "name": <string>}`
|
||||
|
||||
|
||||
@@ -48,12 +48,15 @@ func (rt *Router) CreateSimulation(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := searchMap["levy"]; ok {
|
||||
hasPattern = true
|
||||
}
|
||||
if _, ok := searchMap["sar"]; ok {
|
||||
hasPattern = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !hasPattern {
|
||||
http.Error(w, "Simulation configuration must include a search pattern (spiral, lawnmower, or levy)", http.StatusBadRequest)
|
||||
http.Error(w, "Simulation configuration must include a search pattern (spiral, lawnmower, levy, or sar)", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
<option value="spiral">Spiral</option>
|
||||
<option value="lawnmower">Lawnmower</option>
|
||||
<option value="levy">Levy</option>
|
||||
<option value="sar">SAR</option>
|
||||
</select>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -1,57 +1,59 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
parseConfig,
|
||||
formatDate,
|
||||
type SimulationState,
|
||||
} from "./simulationState.svelte";
|
||||
import { formatTime } from "$lib/ts/utils";
|
||||
import {
|
||||
parseConfig,
|
||||
formatDate,
|
||||
type SimulationState,
|
||||
} from "./simulationState.svelte";
|
||||
import { formatTime } from "$lib/ts/utils";
|
||||
|
||||
let { state }: { state: SimulationState } = $props();
|
||||
let { state }: { state: SimulationState } = $props();
|
||||
</script>
|
||||
|
||||
<main class="table-content">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Sim Name</th>
|
||||
<th>Search Pattern</th>
|
||||
<th>Search Time</th>
|
||||
<th>Total Time</th>
|
||||
<th>Date Run</th>
|
||||
<th>Link</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each state.paginatedSimulations as sim}
|
||||
<tr>
|
||||
<td>{sim.id}</td>
|
||||
<td>{sim.name}</td>
|
||||
<td style="text-transform: capitalize;"
|
||||
>{parseConfig(sim.config).pattern || "Unknown"}</td
|
||||
>
|
||||
<td>{formatTime(sim.search_time || NaN)}</td>
|
||||
<td>{formatTime(sim.total_time || NaN)}</td>
|
||||
<td>{formatDate(sim.created_at)}</td>
|
||||
<td><a href={`/simulation/${sim.id}`}>View Details</a></td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Sim Name</th>
|
||||
<th>Search Pattern</th>
|
||||
<th>Search Time</th>
|
||||
<th>Total Time</th>
|
||||
<th>Date Run</th>
|
||||
<th>Link</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each state.paginatedSimulations as sim}
|
||||
<tr>
|
||||
<td>{sim.id}</td>
|
||||
<td>{sim.name}</td>
|
||||
<td style="text-transform: capitalize;"
|
||||
>{parseConfig(sim.config).pattern === "sar"
|
||||
? "SAR"
|
||||
: parseConfig(sim.config).pattern || "Unknown"}</td
|
||||
>
|
||||
<td>{formatTime(sim.search_time || NaN)}</td>
|
||||
<td>{formatTime(sim.total_time || NaN)}</td>
|
||||
<td>{formatDate(sim.created_at)}</td>
|
||||
<td><a href={`/simulation/${sim.id}`}>View Details</a></td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="pagination">
|
||||
<button
|
||||
onclick={() => state.goToPage(state.currentPage - 1)}
|
||||
disabled={state.currentPage === 1}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<span>Page {state.currentPage} of {state.totalPages}</span>
|
||||
<button
|
||||
onclick={() => state.goToPage(state.currentPage + 1)}
|
||||
disabled={state.currentPage === state.totalPages}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
<button
|
||||
onclick={() => state.goToPage(state.currentPage - 1)}
|
||||
disabled={state.currentPage === 1}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<span>Page {state.currentPage} of {state.totalPages}</span>
|
||||
<button
|
||||
onclick={() => state.goToPage(state.currentPage + 1)}
|
||||
disabled={state.currentPage === state.totalPages}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -29,6 +29,7 @@ export function parseConfig(config: string) {
|
||||
if (searchObj.spiral) pattern = "spiral";
|
||||
else if (searchObj.lawnmower) pattern = "lawnmower";
|
||||
else if (searchObj.levy) pattern = "levy";
|
||||
else if (searchObj.sar) pattern = "sar";
|
||||
|
||||
return {
|
||||
altitude: searchObj.altitude != null ? parseFloat(searchObj.altitude) : null,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,364 +1,340 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { formatDate } from "$lib/simulationState.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { formatDate } from "$lib/simulationState.svelte";
|
||||
|
||||
import {
|
||||
type SimulationDetails,
|
||||
flattenJSON,
|
||||
isImage,
|
||||
getResourceUrl,
|
||||
formatTime,
|
||||
} from "$lib/ts/utils";
|
||||
import {
|
||||
type SimulationDetails,
|
||||
flattenJSON,
|
||||
isImage,
|
||||
getResourceUrl,
|
||||
formatTime,
|
||||
} from "$lib/ts/utils";
|
||||
|
||||
let { data } = $props();
|
||||
let id = $derived(data.id);
|
||||
let { data } = $props();
|
||||
let id = $derived(data.id);
|
||||
|
||||
let simulation = $state<SimulationDetails | null>(null);
|
||||
let simulation = $state<SimulationDetails | null>(null);
|
||||
|
||||
let parsedConfig: { key: string; value: string }[] | null = $derived.by(
|
||||
() => {
|
||||
if (!simulation?.config) return null;
|
||||
try {
|
||||
let cleanedConfig = simulation.config.replace(
|
||||
/"([^"]+)\.yaml"\s*:/g,
|
||||
'"$1":',
|
||||
);
|
||||
return flattenJSON(JSON.parse(cleanedConfig));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
);
|
||||
let parsedConfig: { key: string; value: string }[] | null = $derived.by(
|
||||
() => {
|
||||
if (!simulation?.config) return null;
|
||||
try {
|
||||
let cleanedConfig = simulation.config.replace(
|
||||
/"([^"]+)\.yaml"\s*:/g,
|
||||
'"$1":',
|
||||
);
|
||||
return flattenJSON(JSON.parse(cleanedConfig));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let searchConfig = $derived(
|
||||
parsedConfig
|
||||
?.filter((c) => c.key.startsWith("search."))
|
||||
.map((c) => ({ ...c, key: c.key.replace("search.", "") })),
|
||||
);
|
||||
let uavConfig = $derived(
|
||||
parsedConfig
|
||||
?.filter((c) => c.key.startsWith("uav."))
|
||||
.map((c) => ({ ...c, key: c.key.replace("uav.", "") })),
|
||||
);
|
||||
let ugvConfig = $derived(
|
||||
parsedConfig
|
||||
?.filter((c) => c.key.startsWith("ugv."))
|
||||
.map((c) => ({ ...c, key: c.key.replace("ugv.", "") })),
|
||||
);
|
||||
let otherConfig = $derived(
|
||||
parsedConfig?.filter(
|
||||
(c) =>
|
||||
!c.key.startsWith("search.") &&
|
||||
!c.key.startsWith("uav.") &&
|
||||
!c.key.startsWith("ugv."),
|
||||
),
|
||||
);
|
||||
let searchConfig = $derived(
|
||||
parsedConfig
|
||||
?.filter((c) => c.key.startsWith("search."))
|
||||
.map((c) => ({ ...c, key: c.key.replace("search.", "") })),
|
||||
);
|
||||
let uavConfig = $derived(
|
||||
parsedConfig
|
||||
?.filter((c) => c.key.startsWith("uav."))
|
||||
.map((c) => ({ ...c, key: c.key.replace("uav.", "") })),
|
||||
);
|
||||
let ugvConfig = $derived(
|
||||
parsedConfig
|
||||
?.filter((c) => c.key.startsWith("ugv."))
|
||||
.map((c) => ({ ...c, key: c.key.replace("ugv.", "") })),
|
||||
);
|
||||
let otherConfig = $derived(
|
||||
parsedConfig?.filter(
|
||||
(c) =>
|
||||
!c.key.startsWith("search.") &&
|
||||
!c.key.startsWith("uav.") &&
|
||||
!c.key.startsWith("ugv."),
|
||||
),
|
||||
);
|
||||
|
||||
function groupConfig(data: { key: string; value: string }[] | undefined) {
|
||||
if (!data) return {};
|
||||
const groups: Record<string, { key: string; value: string }[]> = {
|
||||
_general: [],
|
||||
};
|
||||
for (const item of data) {
|
||||
const parts = item.key.split(".");
|
||||
if (parts.length > 1) {
|
||||
const groupName = parts[0];
|
||||
const subKey = parts.slice(1).join(".");
|
||||
if (!groups[groupName]) groups[groupName] = [];
|
||||
groups[groupName].push({ key: subKey, value: item.value });
|
||||
} else {
|
||||
groups["_general"].push(item);
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
function groupConfig(data: { key: string; value: string }[] | undefined) {
|
||||
if (!data) return {};
|
||||
const groups: Record<string, { key: string; value: string }[]> = {
|
||||
_general: [],
|
||||
};
|
||||
for (const item of data) {
|
||||
const parts = item.key.split(".");
|
||||
if (parts.length > 1) {
|
||||
const groupName = parts[0];
|
||||
const subKey = parts.slice(1).join(".");
|
||||
if (!groups[groupName]) groups[groupName] = [];
|
||||
groups[groupName].push({ key: subKey, value: item.value });
|
||||
} else {
|
||||
groups["_general"].push(item);
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
let imagesList = $derived<string[]>(
|
||||
simulation && simulation.resources
|
||||
? simulation.resources.filter(isImage)
|
||||
: [],
|
||||
);
|
||||
let imagesList = $derived<string[]>(
|
||||
simulation && simulation.resources
|
||||
? simulation.resources.filter(isImage)
|
||||
: [],
|
||||
);
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/simulations/${id}`);
|
||||
if (res.ok) {
|
||||
simulation = await res.json();
|
||||
onMount(async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/simulations/${id}`);
|
||||
if (res.ok) {
|
||||
simulation = await res.json();
|
||||
|
||||
setTimeout(() => {
|
||||
window.print();
|
||||
}, 500);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.print();
|
||||
}, 500);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if simulation}
|
||||
<div class="print-container">
|
||||
<h1>Simulation Report: {simulation.name}</h1>
|
||||
<p><strong>ID:</strong> {simulation.id}</p>
|
||||
<p>
|
||||
<strong>Search Pattern:</strong>
|
||||
<span style="text-transform: capitalize;">
|
||||
{parsedConfig?.find((c) => c.key === "search.spiral.max_legs")
|
||||
? "Spiral"
|
||||
: parsedConfig?.find(
|
||||
(c) => c.key === "search.lawnmower.width",
|
||||
)
|
||||
? "Lawnmower"
|
||||
: parsedConfig?.find(
|
||||
(c) => c.key === "search.levy.max_steps",
|
||||
)
|
||||
? "Levy"
|
||||
: "Unknown"}
|
||||
</span>
|
||||
</p>
|
||||
<p><strong>Date Code:</strong> {formatDate(simulation.created_at)}</p>
|
||||
<div class="print-container">
|
||||
<h1>Simulation Report: {simulation.name}</h1>
|
||||
<p><strong>ID:</strong> {simulation.id}</p>
|
||||
<p>
|
||||
<strong>Search Pattern:</strong>
|
||||
<span style="text-transform: capitalize;">
|
||||
{parsedConfig?.find((c) => c.key === "search.spiral.max_legs")
|
||||
? "Spiral"
|
||||
: parsedConfig?.find((c) => c.key === "search.lawnmower.width")
|
||||
? "Lawnmower"
|
||||
: parsedConfig?.find((c) => c.key === "search.levy.max_steps")
|
||||
? "Levy"
|
||||
: parsedConfig?.find((c) => c.key.startsWith("search.sar"))
|
||||
? "SAR"
|
||||
: "Unknown"}
|
||||
</span>
|
||||
</p>
|
||||
<p><strong>Date Code:</strong> {formatDate(simulation.created_at)}</p>
|
||||
|
||||
{#if simulation.search_time !== null && simulation.total_time !== null}
|
||||
<p>
|
||||
<strong>Search Time:</strong>
|
||||
{formatTime(simulation.search_time)} |
|
||||
<strong>Total Time:</strong>
|
||||
{formatTime(simulation.total_time)}
|
||||
</p>
|
||||
{/if}
|
||||
{#if simulation.search_time !== null && simulation.total_time !== null}
|
||||
<p>
|
||||
<strong>Search Time:</strong>
|
||||
{formatTime(simulation.search_time)} |
|
||||
<strong>Total Time:</strong>
|
||||
{formatTime(simulation.total_time)}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if simulation.cpu_info || simulation.gpu_info || simulation.ram_info}
|
||||
<div
|
||||
class="hardware-box"
|
||||
style="margin-top: 20px; border: 1px solid #ccc; padding: 15px; background-color: #f9f9f9; break-inside: avoid;"
|
||||
>
|
||||
<h3>Hardware Spec</h3>
|
||||
<p><strong>CPU:</strong> {simulation.cpu_info || "N/A"}</p>
|
||||
<p><strong>GPU:</strong> {simulation.gpu_info || "N/A"}</p>
|
||||
<p><strong>RAM:</strong> {simulation.ram_info || "N/A"}</p>
|
||||
</div>
|
||||
{/if}
|
||||
{#if simulation.cpu_info || simulation.gpu_info || simulation.ram_info}
|
||||
<div
|
||||
class="hardware-box"
|
||||
style="margin-top: 20px; border: 1px solid #ccc; padding: 15px; background-color: #f9f9f9; break-inside: avoid;"
|
||||
>
|
||||
<h3>Hardware Spec</h3>
|
||||
<p><strong>CPU:</strong> {simulation.cpu_info || "N/A"}</p>
|
||||
<p><strong>GPU:</strong> {simulation.gpu_info || "N/A"}</p>
|
||||
<p><strong>RAM:</strong> {simulation.ram_info || "N/A"}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if simulation.config}
|
||||
<div class="config-box">
|
||||
<strong>Configuration Options:</strong>
|
||||
{#if simulation.config}
|
||||
<div class="config-box">
|
||||
<strong>Configuration Options:</strong>
|
||||
|
||||
{#snippet ConfigTable(
|
||||
title: string,
|
||||
data: { key: string; value: string }[] | undefined,
|
||||
)}
|
||||
{#if data && data.length > 0}
|
||||
{@const groups = groupConfig(data)}
|
||||
<div class="config-category">
|
||||
<h2>{title}</h2>
|
||||
<div class="tables-container">
|
||||
{#if groups["_general"].length > 0}
|
||||
<div class="table-wrapper">
|
||||
<h3 class="table-label">General</h3>
|
||||
<table class="config-table">
|
||||
<thead>
|
||||
<tr
|
||||
><th>Parameter</th><th
|
||||
>Value</th
|
||||
></tr
|
||||
>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each groups["_general"] as item}
|
||||
<tr
|
||||
><td>{item.key}</td><td
|
||||
>{item.value}</td
|
||||
></tr
|
||||
>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
{#each Object.entries(groups).filter(([k]) => k !== "_general") as [groupName, items]}
|
||||
<div class="table-wrapper">
|
||||
<h3
|
||||
class="table-label"
|
||||
style="text-transform: capitalize;"
|
||||
>
|
||||
{groupName}
|
||||
</h3>
|
||||
<table class="config-table">
|
||||
<thead>
|
||||
<tr
|
||||
><th>Parameter</th><th
|
||||
>Value</th
|
||||
></tr
|
||||
>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each items as item}
|
||||
<tr
|
||||
><td>{item.key}</td><td
|
||||
>{item.value}</td
|
||||
></tr
|
||||
>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
{#snippet ConfigTable(
|
||||
title: string,
|
||||
data: { key: string; value: string }[] | undefined,
|
||||
)}
|
||||
{#if data && data.length > 0}
|
||||
{@const groups = groupConfig(data)}
|
||||
<div class="config-category">
|
||||
<h2>{title}</h2>
|
||||
<div class="tables-container">
|
||||
{#if groups["_general"].length > 0}
|
||||
<div class="table-wrapper">
|
||||
<h3 class="table-label">General</h3>
|
||||
<table class="config-table">
|
||||
<thead>
|
||||
<tr><th>Parameter</th><th>Value</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each groups["_general"] as item}
|
||||
<tr><td>{item.key}</td><td>{item.value}</td></tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
{#each Object.entries(groups).filter(([k]) => k !== "_general") as [groupName, items]}
|
||||
<div class="table-wrapper">
|
||||
<h3 class="table-label" style="text-transform: capitalize;">
|
||||
{groupName}
|
||||
</h3>
|
||||
<table class="config-table">
|
||||
<thead>
|
||||
<tr><th>Parameter</th><th>Value</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each items as item}
|
||||
<tr><td>{item.key}</td><td>{item.value}</td></tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
<div class="super-container">
|
||||
{@render ConfigTable("Search", searchConfig)}
|
||||
{@render ConfigTable("UGV", ugvConfig)}
|
||||
{@render ConfigTable("UAV", uavConfig)}
|
||||
{@render ConfigTable("Other", otherConfig)}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="super-container">
|
||||
{@render ConfigTable("Search", searchConfig)}
|
||||
{@render ConfigTable("UGV", ugvConfig)}
|
||||
{@render ConfigTable("UAV", uavConfig)}
|
||||
{@render ConfigTable("Other", otherConfig)}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<h2>Visuals</h2>
|
||||
{#if imagesList.length > 0}
|
||||
<div class="image-grid">
|
||||
{#each imagesList as image}
|
||||
<div class="image-wrapper">
|
||||
<img
|
||||
src={getResourceUrl(simulation.name, image)}
|
||||
alt={image}
|
||||
/>
|
||||
<p class="caption">{image}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p>No images found in this simulation.</p>
|
||||
{/if}
|
||||
</div>
|
||||
<h2>Visuals</h2>
|
||||
{#if imagesList.length > 0}
|
||||
<div class="image-grid">
|
||||
{#each imagesList as image}
|
||||
<div class="image-wrapper">
|
||||
<img src={getResourceUrl(simulation.name, image)} alt={image} />
|
||||
<p class="caption">{image}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p>No images found in this simulation.</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<p>Loading printable report...</p>
|
||||
<p>Loading printable report...</p>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
:global(body) {
|
||||
font-family: Arial, sans-serif;
|
||||
color: #000;
|
||||
background: #fff;
|
||||
}
|
||||
:global(body) {
|
||||
font-family: Arial, sans-serif;
|
||||
color: #000;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.print-container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.print-container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
border-bottom: 2px solid #000;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
h1 {
|
||||
border-bottom: 2px solid #000;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 30px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
h2 {
|
||||
margin-top: 30px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
h3 {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.config-box {
|
||||
margin-top: 20px;
|
||||
border: 1px solid #ccc;
|
||||
padding: 15px;
|
||||
background-color: #f9f9f9;
|
||||
break-inside: avoid;
|
||||
}
|
||||
.config-box {
|
||||
margin-top: 20px;
|
||||
border: 1px solid #ccc;
|
||||
padding: 15px;
|
||||
background-color: #f9f9f9;
|
||||
break-inside: avoid;
|
||||
}
|
||||
|
||||
.super-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.super-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.config-category {
|
||||
padding: 15px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.config-category {
|
||||
padding: 15px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.config-category h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
font-size: 18px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.config-category h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
font-size: 18px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.tables-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.tables-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
}
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.config-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
}
|
||||
.config-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.config-table td {
|
||||
border: 1px solid #888;
|
||||
padding: 5px;
|
||||
}
|
||||
.config-table td {
|
||||
border: 1px solid #888;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.config-table tr:nth-child(even) {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
.config-table tr:nth-child(even) {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.image-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.image-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
break-inside: avoid;
|
||||
text-align: center;
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
background: #fafafa;
|
||||
}
|
||||
.image-wrapper {
|
||||
break-inside: avoid;
|
||||
text-align: center;
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.caption {
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
}
|
||||
.caption {
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
@media print {
|
||||
@page {
|
||||
margin: 1cm;
|
||||
}
|
||||
.print-container {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
@media print {
|
||||
@page {
|
||||
margin: 1cm;
|
||||
}
|
||||
.print-container {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user