mirror of
https://github.com/vacp2p/dst-prefect-workflows.git
synced 2026-01-07 20:43:51 -05:00
LARS updates, rollbacks
This commit is contained in:
1192
lars/Cargo.lock
generated
1192
lars/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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"]
|
||||
|
||||
2027
lars/src/main.rs
2027
lars/src/main.rs
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user