LARS updates, rollbacks

This commit is contained in:
Benjamin Arntzen
2025-04-11 01:36:07 +01:00
parent 582cae2550
commit 49a8a1677f
8 changed files with 1828 additions and 1814 deletions

1192
lars/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,40 +4,41 @@ version = "0.1.0"
edition = "2021"
[dependencies]
axum = { version = "0.7", features = ["ws", "macros"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
minijinja = { version = "2.9", features = ["loader", "json"] }
minijinja-autoreload = "2.9"
tower-http = { version = "0.6", features = ["fs", "trace", "cors"] }
tower-livereload = "0.9"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
kube = { version = "0.99", features = ["runtime", "derive", "client", "config"] }
k8s-openapi = { version = "0.24", features = ["v1_28"] }
prometheus-client = "0.22"
thiserror = "1.0"
uuid = { version = "1.7", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
sqlx = { version = "0.8", features = [ "runtime-tokio", "sqlite", "macros", "chrono", "uuid", "migrate"] }
dotenvy = "0.15"
reqwest = { version = "0.11", features = ["json"] }
axum = { version = "0.7.5", features = ["macros", "http2"] }
axum-extra = { version = "0.9.3", features = ["typed-header"] }
chrono = { version = "0.4.38", features = ["serde"] }
dotenvy = "0.15.7"
futures = "0.3.30"
minijinja = { version = "1.0.14", features = ["loader"] }
minijinja-autoreload = "1.0.14"
rand = "0.8.5"
serde = { version = "1.0.203", features = ["derive"] }
serde_json = "1.0.117"
serde_yaml = "0.9"
sqlx = { version = "0.7.4", features = ["runtime-tokio", "sqlite", "chrono", "macros", "migrate"] }
tokio = { version = "1.38.0", features = ["full"] }
tower-http = { version = "0.5.2", features = ["trace", "fs"] }
tower-livereload = { version = "0.9.2", optional = true }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
uuid = { version = "1.8.0", features = ["v4", "serde"] }
urlencoding = "2.1.3"
reqwest = { version = "0.12.5", features = ["json"] }
base64 = "0.22.1"
# Optional, but often useful for config management
# config = { version = "0.14", features = ["yaml", "json", "toml", "env"] }
# dotenvy = "0.15"
# Added for SSE stream building
futures = "0.3"
async-stream = "0.3"
# Added for mock data generation
rand = "0.8"
urlencoding = "2.1.2"
[dev-dependencies]
# Add development-specific dependencies here later if needed
# e.g., for integration testing
[features]
default = []
debug = ["tower-livereload"]

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@
--tw-space-x-reverse: 0;
--tw-divide-y-reverse: 0;
--tw-border-style: solid;
--tw-leading: initial;
--tw-font-weight: initial;
--tw-tracking: initial;
--tw-shadow: 0 0 #0000;
@@ -22,19 +23,6 @@
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-offset-shadow: 0 0 #0000;
--tw-blur: initial;
--tw-brightness: initial;
--tw-contrast: initial;
--tw-grayscale: initial;
--tw-hue-rotate: initial;
--tw-invert: initial;
--tw-opacity: initial;
--tw-saturate: initial;
--tw-sepia: initial;
--tw-drop-shadow: initial;
--tw-drop-shadow-color: initial;
--tw-drop-shadow-alpha: 100%;
--tw-drop-shadow-size: initial;
--tw-duration: initial;
}
}
@@ -46,18 +34,33 @@
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
--color-red-100: oklch(93.6% 0.032 17.717);
--color-red-300: oklch(80.8% 0.114 19.571);
--color-red-500: oklch(63.7% 0.237 25.331);
--color-red-600: oklch(57.7% 0.245 27.325);
--color-red-700: oklch(50.5% 0.213 27.518);
--color-red-800: oklch(44.4% 0.177 26.899);
--color-red-900: oklch(39.6% 0.141 25.723);
--color-yellow-400: oklch(85.2% 0.199 91.936);
--color-yellow-500: oklch(79.5% 0.184 86.047);
--color-green-100: oklch(96.2% 0.044 156.743);
--color-green-300: oklch(87.1% 0.15 154.449);
--color-green-600: oklch(62.7% 0.194 149.214);
--color-green-700: oklch(52.7% 0.154 150.069);
--color-green-800: oklch(44.8% 0.119 151.328);
--color-green-900: oklch(39.3% 0.095 152.535);
--color-blue-100: oklch(93.2% 0.032 255.585);
--color-blue-200: oklch(88.2% 0.059 254.128);
--color-blue-300: oklch(80.9% 0.105 251.813);
--color-blue-400: oklch(70.7% 0.165 254.624);
--color-blue-500: oklch(62.3% 0.214 259.815);
--color-blue-600: oklch(54.6% 0.245 262.881);
--color-blue-700: oklch(48.8% 0.243 264.376);
--color-blue-800: oklch(42.4% 0.199 265.638);
--color-blue-900: oklch(37.9% 0.146 265.522);
--color-indigo-500: oklch(58.5% 0.233 277.117);
--color-indigo-600: oklch(51.1% 0.262 276.966);
--color-indigo-700: oklch(45.7% 0.24 277.023);
--color-purple-500: oklch(62.7% 0.265 303.9);
--color-gray-50: oklch(98.5% 0.002 247.839);
--color-gray-100: oklch(96.7% 0.003 264.542);
@@ -318,6 +321,9 @@
.hidden {
display: none;
}
.inline-flex {
display: inline-flex;
}
.table {
display: table;
}
@@ -348,6 +354,9 @@
.flex-1 {
flex: 1;
}
.cursor-not-allowed {
cursor: not-allowed;
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
@@ -452,6 +461,9 @@
.border-gray-300 {
border-color: var(--color-gray-300);
}
.bg-blue-100 {
background-color: var(--color-blue-100);
}
.bg-blue-200 {
background-color: var(--color-blue-200);
}
@@ -473,18 +485,30 @@
.bg-gray-300 {
background-color: var(--color-gray-300);
}
.bg-gray-400 {
background-color: var(--color-gray-400);
}
.bg-gray-700 {
background-color: var(--color-gray-700);
}
.bg-gray-800 {
background-color: var(--color-gray-800);
}
.bg-green-100 {
background-color: var(--color-green-100);
}
.bg-green-600 {
background-color: var(--color-green-600);
}
.bg-indigo-500 {
background-color: var(--color-indigo-500);
}
.bg-purple-500 {
background-color: var(--color-purple-500);
}
.bg-red-100 {
background-color: var(--color-red-100);
}
.bg-red-500 {
background-color: var(--color-red-500);
}
@@ -515,6 +539,9 @@
.p-6 {
padding: calc(var(--spacing) * 6);
}
.px-2 {
padding-inline: calc(var(--spacing) * 2);
}
.px-3 {
padding-inline: calc(var(--spacing) * 3);
}
@@ -571,6 +598,10 @@
font-size: var(--text-xs);
line-height: var(--tw-leading, var(--text-xs--line-height));
}
.leading-5 {
--tw-leading: calc(var(--spacing) * 5);
line-height: calc(var(--spacing) * 5);
}
.font-bold {
--tw-font-weight: var(--font-weight-bold);
font-weight: var(--font-weight-bold);
@@ -590,6 +621,12 @@
.whitespace-nowrap {
white-space: nowrap;
}
.text-blue-600 {
color: var(--color-blue-600);
}
.text-blue-800 {
color: var(--color-blue-800);
}
.text-gray-300 {
color: var(--color-gray-300);
}
@@ -602,15 +639,24 @@
.text-gray-600 {
color: var(--color-gray-600);
}
.text-gray-700 {
color: var(--color-gray-700);
}
.text-gray-800 {
color: var(--color-gray-800);
}
.text-gray-900 {
color: var(--color-gray-900);
}
.text-green-800 {
color: var(--color-green-800);
}
.text-red-500 {
color: var(--color-red-500);
}
.text-red-800 {
color: var(--color-red-800);
}
.text-white {
color: var(--color-white);
}
@@ -623,6 +669,9 @@
.italic {
font-style: italic;
}
.opacity-50 {
opacity: 50%;
}
.shadow-lg {
--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@@ -635,9 +684,6 @@
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.filter {
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
}
.transition-all {
transition-property: all;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
@@ -652,6 +698,13 @@
--tw-duration: 300ms;
transition-duration: 300ms;
}
.hover\:bg-blue-600 {
&:hover {
@media (hover: hover) {
background-color: var(--color-blue-600);
}
}
}
.hover\:bg-blue-700 {
&:hover {
@media (hover: hover) {
@@ -673,6 +726,13 @@
}
}
}
.hover\:bg-indigo-600 {
&:hover {
@media (hover: hover) {
background-color: var(--color-indigo-600);
}
}
}
.hover\:bg-red-700 {
&:hover {
@media (hover: hover) {
@@ -680,6 +740,13 @@
}
}
}
.hover\:underline {
&:hover {
@media (hover: hover) {
text-decoration-line: underline;
}
}
}
.focus\:border-blue-500 {
&:focus {
border-color: var(--color-blue-500);
@@ -739,6 +806,11 @@
background-color: var(--color-blue-800);
}
}
.dark\:bg-blue-900 {
@media (prefers-color-scheme: dark) {
background-color: var(--color-blue-900);
}
}
.dark\:bg-gray-600 {
@media (prefers-color-scheme: dark) {
background-color: var(--color-gray-600);
@@ -767,6 +839,31 @@
background-color: var(--color-gray-900);
}
}
.dark\:bg-green-900 {
@media (prefers-color-scheme: dark) {
background-color: var(--color-green-900);
}
}
.dark\:bg-indigo-600 {
@media (prefers-color-scheme: dark) {
background-color: var(--color-indigo-600);
}
}
.dark\:bg-red-900 {
@media (prefers-color-scheme: dark) {
background-color: var(--color-red-900);
}
}
.dark\:text-blue-300 {
@media (prefers-color-scheme: dark) {
color: var(--color-blue-300);
}
}
.dark\:text-blue-400 {
@media (prefers-color-scheme: dark) {
color: var(--color-blue-400);
}
}
.dark\:text-gray-100 {
@media (prefers-color-scheme: dark) {
color: var(--color-gray-100);
@@ -782,6 +879,16 @@
color: var(--color-gray-400);
}
}
.dark\:text-green-300 {
@media (prefers-color-scheme: dark) {
color: var(--color-green-300);
}
}
.dark\:text-red-300 {
@media (prefers-color-scheme: dark) {
color: var(--color-red-300);
}
}
.dark\:text-white {
@media (prefers-color-scheme: dark) {
color: var(--color-white);
@@ -796,6 +903,15 @@
}
}
}
.dark\:hover\:bg-indigo-700 {
@media (prefers-color-scheme: dark) {
&:hover {
@media (hover: hover) {
background-color: var(--color-indigo-700);
}
}
}
}
}
@property --tw-space-y-reverse {
syntax: "*";
@@ -817,6 +933,10 @@
inherits: false;
initial-value: solid;
}
@property --tw-leading {
syntax: "*";
inherits: false;
}
@property --tw-font-weight {
syntax: "*";
inherits: false;
@@ -890,59 +1010,6 @@
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-blur {
syntax: "*";
inherits: false;
}
@property --tw-brightness {
syntax: "*";
inherits: false;
}
@property --tw-contrast {
syntax: "*";
inherits: false;
}
@property --tw-grayscale {
syntax: "*";
inherits: false;
}
@property --tw-hue-rotate {
syntax: "*";
inherits: false;
}
@property --tw-invert {
syntax: "*";
inherits: false;
}
@property --tw-opacity {
syntax: "*";
inherits: false;
}
@property --tw-saturate {
syntax: "*";
inherits: false;
}
@property --tw-sepia {
syntax: "*";
inherits: false;
}
@property --tw-drop-shadow {
syntax: "*";
inherits: false;
}
@property --tw-drop-shadow-color {
syntax: "*";
inherits: false;
}
@property --tw-drop-shadow-alpha {
syntax: "<percentage>";
inherits: false;
initial-value: 100%;
}
@property --tw-drop-shadow-size {
syntax: "*";
inherits: false;
}
@property --tw-duration {
syntax: "*";
inherits: false;

View File

@@ -1,27 +1,35 @@
{% extends "base.html.j2" %}
{% block title %}LARS Run History{% endblock %}
{% block title %}LARS Simulation History{% endblock %}
{% block content %}
<div class="container mx-auto p-6">
<h1 class="text-3xl font-bold mb-6 text-gray-800 dark:text-gray-100">Simulation Run History</h1>
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold text-gray-800 dark:text-gray-100">Simulation Run History</h1>
{# Maybe add a button to manually refresh? #}
{# <button id="refresh-history-btn" class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-md">Refresh</button> #}
</div>
<div class="bg-white dark:bg-gray-800 p-5 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-700/50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Chart</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Nodes</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Duration (min)</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">CPU Cores (Avg)</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Memory (GB Avg)</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Observed At</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Issue</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Chart</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Nodes</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Duration</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Status</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Finished At</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Pred Cost (CPU/Mem)</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Actual Cost (CPU/Mem)</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Results</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700" id="history-table-body">
{# Data will be loaded here via JavaScript #}
<tr>
<td colspan="6" class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 italic text-center">Loading history...</td>
<td colspan="10" class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 italic text-center">Loading history...</td>
</tr>
</tbody>
</table>
@@ -33,21 +41,85 @@
<script>
const historyTableBody = document.getElementById('history-table-body');
// --- Formatting Helpers ---
function formatOptional(value, placeholder = 'N/A') {
return value !== null && value !== undefined ? value : placeholder;
}
function formatFloat(value, digits = 2, placeholder = 'N/A') {
return value !== null && value !== undefined ? value.toFixed(digits) : placeholder;
}
function formatDateTime(dateTimeString, placeholder = 'N/A') {
if (!dateTimeString) return placeholder;
try {
// Append 'Z' if no timezone info is present to treat as UTC
const isoString = dateTimeString.endsWith('Z') ? dateTimeString : dateTimeString + 'Z';
return new Date(isoString).toLocaleString();
} catch (e) {
console.error("Error parsing date:", dateTimeString, e);
return placeholder;
}
}
function formatDuration(seconds) {
if (seconds === null || seconds === undefined) return 'N/A';
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
let durationStr = `${minutes}m`;
if (remainingSeconds > 0) {
durationStr += ` ${remainingSeconds}s`;
}
return durationStr;
}
function formatStatus(status) {
let badgeColor = 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'; // Default
if (status === 'Completed') {
badgeColor = 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300';
} else if (status.startsWith('Failed')) {
badgeColor = 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300';
} else if (status === 'Running' || status === 'Deploying' || status === 'WaitingForRollout' || status === 'CleaningUp') {
badgeColor = 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300';
}
return `<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${badgeColor}">${status}</span>`;
}
// --- Row Generation ---
function formatHistoryRow(entry) {
const observedDate = new Date(entry.observed_at).toLocaleString();
const durationMins = Math.round(entry.duration_secs / 60);
const issueLink = entry.issue_number ? `<a href="https://github.com/vacp2p/vaclab/issues/${entry.issue_number}" target="_blank" class="text-blue-600 dark:text-blue-400 hover:underline">#${entry.issue_number}</a>` : 'N/A';
const resultsLink = entry.results_url ? `<a href="${entry.results_url}" target="_blank" class="text-blue-600 dark:text-blue-400 hover:underline">View</a>` : 'N/A';
// Rerun button disabled if status is failed or completed, could re-enable later
const rerunDisabled = !(entry.status === 'Completed' || entry.status.startsWith('Failed'));
const rerunButtonClass = rerunDisabled
? "cursor-not-allowed opacity-50 bg-gray-400 dark:bg-gray-600 text-gray-700 dark:text-gray-400"
: "bg-indigo-500 hover:bg-indigo-600 dark:bg-indigo-600 dark:hover:bg-indigo-700 text-white";
return `
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">${entry.chart}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">${entry.node_count}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">${durationMins}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">${entry.cpu_cores.toFixed(2)}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">${entry.memory_gb.toFixed(2)}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">${observedDate}</td>
<tr data-simulation-id="${entry.simulation_id}">
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">${issueLink}</td>
<td class="px-4 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">${entry.chart}</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">${entry.node_count}</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">${formatDuration(entry.duration_secs)}</td>
<td class="px-4 py-4 whitespace-nowrap text-sm">${formatStatus(entry.status)}</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">${formatDateTime(entry.end_time)}</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">${formatFloat(entry.predicted_cpu_cores)} / ${formatFloat(entry.predicted_memory_gb)}</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">${formatFloat(entry.actual_cpu_cores)} / ${formatFloat(entry.actual_memory_gb)}</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">${resultsLink}</td>
<td class="px-4 py-4 whitespace-nowrap text-sm font-medium">
<button
class="rerun-btn px-3 py-1 text-xs rounded-md ${rerunButtonClass}"
data-simulation-id="${entry.simulation_id}"
${rerunDisabled ? 'disabled' : ''}>
Rerun
</button>
</td>
</tr>
`;
}
// --- Data Loading ---
async function loadHistory() {
if (!historyTableBody) return;
try {
@@ -68,7 +140,54 @@
}
}
// Load history when the page loads
// --- Rerun Logic ---
async function handleRerunClick(event) {
const button = event.target;
const simulationId = button.dataset.simulationId;
if (!simulationId) return;
button.disabled = true;
button.textContent = 'Queuing...';
button.classList.add('opacity-50', 'cursor-not-allowed');
try {
const response = await fetch(`/api/rerun/${simulationId}`, { method: 'POST' });
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Rerun request failed: ${response.status} - ${errorText}`);
}
const result = await response.json();
console.log('Rerun queued:', result);
// Optionally provide more feedback, e.g., flash message or redirect
alert(`Simulation ${simulationId} queued for rerun.`);
// You might want to redirect to the main page or refresh history after a delay
// window.location.href = '/';
button.textContent = 'Queued'; // Keep disabled but show queued
} catch (error) {
console.error("Failed to queue rerun:", error);
alert(`Failed to queue rerun for ${simulationId}. See console for details.`);
// Re-enable the button on failure
button.disabled = false;
button.textContent = 'Rerun';
button.classList.remove('opacity-50', 'cursor-not-allowed');
}
}
function addRerunListeners() {
const rerunButtons = historyTableBody.querySelectorAll('.rerun-btn');
rerunButtons.forEach(button => {
button.addEventListener('click', handleRerunClick);
});
}
// --- Initial Load ---
document.addEventListener('DOMContentLoaded', loadHistory);
// Optional: Add listener for manual refresh button if you add one
// const refreshBtn = document.getElementById('refresh-history-btn');
// if (refreshBtn) {
// refreshBtn.addEventListener('click', loadHistory);
// }
</script>
{% endblock %}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 51 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long