Merge branch 'dev' into add_GetMemoryDomainsBySocket_2026

This commit is contained in:
2026-02-23 18:47:03 +01:00
29 changed files with 977 additions and 165 deletions

View File

@@ -609,6 +609,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT"
},
"node_modules/@urql/core": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@urql/core/-/core-5.2.0.tgz",
@@ -745,9 +751,9 @@
}
},
"node_modules/devalue": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz",
"integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==",
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz",
"integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==",
"license": "MIT"
},
"node_modules/escape-latex": {
@@ -763,9 +769,9 @@
"license": "MIT"
},
"node_modules/esrap": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.1.tgz",
"integrity": "sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz",
"integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
@@ -1176,22 +1182,23 @@
}
},
"node_modules/svelte": {
"version": "5.46.4",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.46.4.tgz",
"integrity": "sha512-VJwdXrmv9L8L7ZasJeWcCjoIuMRVbhuxbss0fpVnR8yorMmjNDwcjIH08vS6wmSzzzgAG5CADQ1JuXPS2nwt9w==",
"version": "5.53.2",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.2.tgz",
"integrity": "sha512-yGONuIrcl/BMmqbm6/52Q/NYzfkta7uVlos5NSzGTfNJTTFtPPzra6rAQoQIwAqupeM3s9uuTf5PvioeiCdg9g==",
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5",
"@types/trusted-types": "^2.0.7",
"acorn": "^8.12.1",
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"devalue": "^5.6.2",
"devalue": "^5.6.3",
"esm-env": "^1.2.1",
"esrap": "^2.2.1",
"esrap": "^2.2.2",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",

View File

@@ -15,6 +15,7 @@
import ShowUsers from "./admin/ShowUsers.svelte";
import Options from "./admin/Options.svelte";
import NoticeEdit from "./admin/NoticeEdit.svelte";
import RunTaggers from "./admin/RunTaggers.svelte";
/* Svelte 5 Props */
let {
@@ -70,4 +71,5 @@
</Col>
<Options config={ccconfig} {clusterNames}/>
<NoticeEdit {ncontent}/>
<RunTaggers />
</Row>

View File

@@ -0,0 +1,142 @@
<!--
@component Admin card for running individual job taggers on all jobs
-->
<script>
import {
Col,
Card,
CardTitle,
CardBody,
Spinner,
Badge,
} from "@sveltestrap/sveltestrap";
import { fade } from "svelte/transition";
import { onMount, onDestroy } from "svelte";
/* State Init */
let taggers = $state([]);
let message = $state({ msg: "", color: "#d63384" });
let displayMessage = $state(false);
let pollTimer = $state(null);
/* Functions */
async function fetchTaggers() {
try {
const res = await fetch("/config/taggers/");
if (res.ok) {
taggers = await res.json();
}
} catch (err) {
console.error("Failed to fetch taggers:", err);
}
}
async function runTagger(name) {
let formData = new FormData();
formData.append("name", name);
try {
const res = await fetch("/config/taggers/run/", {
method: "POST",
body: formData,
});
if (res.ok) {
let text = await res.text();
popMessage(text, "#048109");
startPolling();
await fetchTaggers();
} else {
let text = await res.text();
throw new Error("Response Code " + res.status + " -> " + text);
}
} catch (err) {
popMessage(err, "#d63384");
}
}
function startPolling() {
if (pollTimer) return;
pollTimer = setInterval(async () => {
await fetchTaggers();
const anyRunning = taggers.some((t) => t.running);
if (!anyRunning) {
clearInterval(pollTimer);
pollTimer = null;
}
}, 3000);
}
function popMessage(response, rescolor) {
message = { msg: response, color: rescolor };
displayMessage = true;
setTimeout(function () {
displayMessage = false;
}, 3500);
}
/* Lifecycle */
onMount(async () => {
await fetchTaggers();
const anyRunning = taggers.some((t) => t.running);
if (anyRunning) startPolling();
});
onDestroy(() => {
if (pollTimer) clearInterval(pollTimer);
});
</script>
<Col>
<Card class="h-100">
<CardBody>
<CardTitle class="mb-3">Job Taggers</CardTitle>
<p>Run individual taggers on all existing jobs.</p>
{#if taggers.length === 0}
<p class="text-muted">No taggers available.</p>
{:else}
<table class="table table-sm mb-3">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
{#each taggers as tagger}
<tr>
<td>{tagger.name}</td>
<td><Badge color="secondary">{tagger.type}</Badge></td>
<td>
{#if tagger.running}
<Spinner size="sm" color="primary" /> Running
{:else}
<span class="text-muted">Idle</span>
{/if}
</td>
<td>
<button
class="btn btn-sm btn-primary"
disabled={tagger.running}
onclick={() => runTagger(tagger.name)}
>
Run
</button>
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
<p>
{#if displayMessage}<b
><code style="color: {message.color};" out:fade
>{message.msg}</code
></b
>{/if}
</p>
</CardBody>
</Card>
</Col>

View File

@@ -32,15 +32,6 @@
/* Const Init */
const client = getContextClient();
const stateOptions = [
"all",
"allocated",
"idle",
"down",
"mixed",
"reserved",
"unknown",
];
const healthOptions = [
"all",
"full",
@@ -52,12 +43,10 @@
let pieWidth = $state(0);
let querySorting = $state({ field: "startTime", type: "col", order: "DESC" })
let tableHostFilter = $state("");
let tableStateFilter = $state(stateOptions[0]);
let tableHealthFilter = $state(healthOptions[0]);
let healthTableSorting = $state(
{
schedulerState: { dir: "down", active: true },
healthState: { dir: "down", active: false },
healthState: { dir: "up", active: true },
hostname: { dir: "down", active: false },
}
);
@@ -79,9 +68,7 @@
hostname
cluster
subCluster
schedulerState
healthState
metaData
healthData
}
}
@@ -102,7 +89,7 @@
let healthTableData = $derived.by(() => {
if ($statusQuery?.data) {
return [...$statusQuery.data.nodes.items].sort((n1, n2) => {
return n1['schedulerState'].localeCompare(n2['schedulerState'])
return n1['healthState'].localeCompare(n2['healthState'])
});
} else {
return [];
@@ -114,21 +101,12 @@
if (tableHostFilter != "") {
pendingTableData = pendingTableData.filter((e) => e.hostname.includes(tableHostFilter))
}
if (tableStateFilter != "all") {
pendingTableData = pendingTableData.filter((e) => e.schedulerState.includes(tableStateFilter))
}
if (tableHealthFilter != "all") {
pendingTableData = pendingTableData.filter((e) => e.healthState.includes(tableHealthFilter))
}
return pendingTableData
});
const refinedStateData = $derived.by(() => {
return $statusQuery?.data?.nodeStates.
filter((e) => ['allocated', 'reserved', 'idle', 'mixed','down', 'unknown'].includes(e.state)).
sort((a, b) => b.count - a.count)
});
const refinedHealthData = $derived.by(() => {
return $statusQuery?.data?.nodeStates.
filter((e) => ['full', 'partial', 'failed'].includes(e.state)).
@@ -296,7 +274,7 @@
<thead>
<!-- Header Row 1: Titles and Sorting -->
<tr>
<th style="width: 9%; min-width: 100px; max-width:10%;" onclick={() => sortBy('hostname')}>
<th style="width: 10%; min-width: 100px; max-width:12%;" onclick={() => sortBy('hostname')}>
Hosts ({filteredTableData.length})
<Icon
name="caret-{healthTableSorting['hostname'].dir}{healthTableSorting['hostname']
@@ -304,17 +282,8 @@
? '-fill'
: ''}"
/>
</th>
<th style="width: 9%; min-width: 100px; max-width:10%;" onclick={() => sortBy('schedulerState')}>
Scheduler State
<Icon
name="caret-{healthTableSorting['schedulerState'].dir}{healthTableSorting['schedulerState']
.active
? '-fill'
: ''}"
/>
</th>
<th style="width: 9%; min-width: 100px; max-width:10%;" onclick={() => sortBy('healthState')}>
<th style="width: 10%; min-width: 100px; max-width:12%;" onclick={() => sortBy('healthState')}>
Health State
<Icon
name="caret-{healthTableSorting['healthState'].dir}{healthTableSorting['healthState']
@@ -324,7 +293,6 @@
/>
</th>
<th>Metric Availability</th>
<th>Meta Information</th>
</tr>
<!-- Header Row 2: Filters -->
<tr>
@@ -337,53 +305,27 @@
</InputGroup>
</th>
<th>
<InputGroup size="sm">
<Input type="select" bind:value={tableStateFilter}>
{#each stateOptions as so}
<option value={so}>{so}</option>
{/each}
</Input>
<InputGroupText>
<Icon name="search"></Icon>
</InputGroupText>
</InputGroup>
</th>
<th>
<InputGroup size="sm">
<Input type="select" bind:value={tableHealthFilter}>
<Input size="sm" type="select" bind:value={tableHealthFilter}>
{#each healthOptions as ho}
<option value={ho}>{ho}</option>
{/each}
</Input>
<InputGroupText>
<Icon name="search"></Icon>
</InputGroupText>
</InputGroup>
</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{#each filteredTableData as host (host.hostname)}
<tr>
<th scope="row"><b><a href="/monitoring/node/{cluster}/{host.hostname}" target="_blank">{host.hostname}</a></b></th>
<td>{host.schedulerState}</td>
<td>{host.healthState}</td>
<td style="max-width: 250px;">
<td style="max-width: 76%;">
{#each Object.keys(host.healthData) as hkey}
<p>
<b>{hkey}</b>: {host.healthData[hkey]}
</p>
{/each}
</td>
<td style="max-width: 250px;">
{#each Object.keys(host.metaData) as mkey}
<p>
<b>{mkey}</b>: {host.metaData[mkey]}
</p>
{/each}
</td>
</tr>
{/each}
</tbody>