Skip to content

Commit d8d8a41

Browse files
committed
feat(ui): improve runner header status and collapsible journal/events
1 parent 112c586 commit d8d8a41

4 files changed

Lines changed: 156 additions & 15 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ More installation details (including Proxmox LXC setup and sudo-safe install flo
5353
## What It Does
5454

5555
- Manage multiple runners (name, command, schedule, cases, notifications)
56-
- UI language switch (DE/EN) (persisted locally in the browser)
56+
- UI language switch (DE/EN/FR) (persisted locally in the browser)
5757
- Clone a saved runner (creates a stored copy directly below the source)
5858
- Run commands manually or on interval after each run finishes
5959
- Show runner active duration while active (running/scheduled) (`hh:mm:ss`, unlimited hours)

static/app.js

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ function loadUIState() {
3939
return {
4040
notifySectionCollapsed: !!parsed.notifySectionCollapsed,
4141
runnerSectionCollapsed: !!parsed.runnerSectionCollapsed,
42+
notifyJournalSectionCollapsed: !!parsed.notifyJournalSectionCollapsed,
43+
eventsSectionCollapsed: !!parsed.eventsSectionCollapsed,
4244
notifySortMode: !!parsed.notifySortMode,
4345
runnerSortMode: !!parsed.runnerSortMode,
4446
lang: normalizeLang(parsed.lang) || "",
@@ -55,6 +57,8 @@ function saveUIState() {
5557
JSON.stringify({
5658
notifySectionCollapsed: !!ui.notifySectionCollapsed,
5759
runnerSectionCollapsed: !!ui.runnerSectionCollapsed,
60+
notifyJournalSectionCollapsed: !!ui.notifyJournalSectionCollapsed,
61+
eventsSectionCollapsed: !!ui.eventsSectionCollapsed,
5862
notifySortMode: !!ui.notifySortMode,
5963
runnerSortMode: !!ui.runnerSortMode,
6064
lang: ui.lang,
@@ -69,6 +73,8 @@ const loadedUIState = loadUIState();
6973
const ui = {
7074
notifySectionCollapsed: loadedUIState.notifySectionCollapsed ?? false,
7175
runnerSectionCollapsed: loadedUIState.runnerSectionCollapsed ?? false,
76+
notifyJournalSectionCollapsed: loadedUIState.notifyJournalSectionCollapsed ?? false,
77+
eventsSectionCollapsed: loadedUIState.eventsSectionCollapsed ?? false,
7278
notifySortMode: loadedUIState.notifySortMode ?? false,
7379
runnerSortMode: loadedUIState.runnerSortMode ?? false,
7480
lang: loadedUIState.lang || detectDefaultLang(),
@@ -1298,6 +1304,28 @@ function renderRunnerSection() {
12981304
syncSortModeButtons();
12991305
}
13001306

1307+
function renderNotifyJournalSection() {
1308+
const toggle = el("notifyJournalSectionToggle");
1309+
const body = el("notifyJournalSectionBody");
1310+
if (toggle) {
1311+
toggle.textContent = ui.notifyJournalSectionCollapsed ? "+" : "-";
1312+
}
1313+
if (body) {
1314+
body.classList.toggle("hidden", ui.notifyJournalSectionCollapsed);
1315+
}
1316+
}
1317+
1318+
function renderEventsSection() {
1319+
const toggle = el("eventsSectionToggle");
1320+
const body = el("eventsSectionBody");
1321+
if (toggle) {
1322+
toggle.textContent = ui.eventsSectionCollapsed ? "+" : "-";
1323+
}
1324+
if (body) {
1325+
body.classList.toggle("hidden", ui.eventsSectionCollapsed);
1326+
}
1327+
}
1328+
13011329
function scheduleOptions(max) {
13021330
let opts = "";
13031331
for (let i = 0; i <= max; i++) opts += `<option value="${i}">${i}</option>`;
@@ -2352,19 +2380,40 @@ async function loadNotifyJournal() {
23522380
function updateGlobalRunningStatus() {
23532381
const runningCount = Object.values(runtime.status).filter((s) => s.running).length;
23542382
const scheduledCount = Object.values(runtime.status).filter((s) => s.scheduled && !s.running).length;
2383+
const status = el("runningStatus");
23552384
const spinner = el("globalSpinner");
23562385
const count = el("runningCount");
23572386

23582387
const hasActivity = runningCount > 0 || scheduledCount > 0;
2388+
const runningNow = runningCount > 0;
2389+
const displayCount = runningNow ? runningCount : scheduledCount;
2390+
2391+
status?.classList.toggle("hidden", !hasActivity);
2392+
status?.classList.toggle("is-active", hasActivity);
23592393

23602394
if (hasActivity) {
23612395
spinner?.classList.remove("hidden");
2396+
spinner?.classList.toggle("is-scheduled", !runningNow);
2397+
if (spinner) {
2398+
spinner.textContent = String(displayCount);
2399+
const spinnerLabel = runningNow
2400+
? `${runningCount} ${t("running_label")}`
2401+
: `${scheduledCount} ${t("scheduled_label")}`;
2402+
spinner.setAttribute("title", spinnerLabel);
2403+
spinner.setAttribute("aria-label", spinnerLabel);
2404+
}
23622405
const parts = [];
23632406
if (runningCount > 0) parts.push(`${runningCount} ${t("running_label")}`);
23642407
if (scheduledCount > 0) parts.push(`${scheduledCount} ${t("scheduled_label")}`);
2365-
if (count) count.textContent = parts.join(", ");
2408+
if (count) count.textContent = parts.join(" ");
23662409
} else {
23672410
spinner?.classList.add("hidden");
2411+
spinner?.classList.remove("is-scheduled");
2412+
if (spinner) {
2413+
spinner.textContent = "";
2414+
spinner.removeAttribute("title");
2415+
spinner.removeAttribute("aria-label");
2416+
}
23682417
if (count) count.textContent = "";
23692418
}
23702419
}
@@ -2644,6 +2693,18 @@ async function wireUI() {
26442693
renderRunnerSection();
26452694
});
26462695

2696+
el("notifyJournalSectionToggle")?.addEventListener("click", () => {
2697+
ui.notifyJournalSectionCollapsed = !ui.notifyJournalSectionCollapsed;
2698+
saveUIState();
2699+
renderNotifyJournalSection();
2700+
});
2701+
2702+
el("eventsSectionToggle")?.addEventListener("click", () => {
2703+
ui.eventsSectionCollapsed = !ui.eventsSectionCollapsed;
2704+
saveUIState();
2705+
renderEventsSection();
2706+
});
2707+
26472708
el("sortNotifyBtn")?.addEventListener("click", () => {
26482709
ui.notifySortMode = !ui.notifySortMode;
26492710
saveUIState();
@@ -2785,6 +2846,8 @@ async function wireUI() {
27852846
try {
27862847
window.addEventListener("beforeunload", handleBeforeUnload);
27872848
applyLanguageToStaticDom();
2849+
renderNotifyJournalSection();
2850+
renderEventsSection();
27882851
const st = await apiGet("/api/state");
27892852
setFromState(st);
27902853
await loadNotifyJournal();

static/style.css

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,30 @@ h1 { font-size: 20px; margin: 0; }
2222
#topHeader { gap: 12px; }
2323
.headerRight { display: flex; align-items: center; gap: 10px; min-width: 0; }
2424
.langToggle { padding: 8px 10px; min-width: 50px; font-weight: 650; }
25+
#runningStatus {
26+
flex: 0 0 auto;
27+
min-height: 34px;
28+
min-width: 110px;
29+
padding: 4px 10px 4px 6px;
30+
border-radius: 999px;
31+
border: 1px solid #27314f;
32+
background: linear-gradient(180deg, #101829 0%, #0b1120 100%);
33+
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
34+
gap: 8px;
35+
transition: border-color 180ms ease, box-shadow 180ms ease, background 180ms ease;
36+
}
37+
#runningStatus.is-active {
38+
border-color: #3458a8;
39+
background: linear-gradient(180deg, rgba(47, 79, 143, 0.28) 0%, rgba(18, 28, 48, 0.24) 100%), linear-gradient(180deg, #101829 0%, #0b1120 100%);
40+
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.045), 0 0 0 1px rgba(45, 91, 255, 0.12);
41+
}
42+
#runningCount {
43+
font-size: 12px;
44+
font-weight: 620;
45+
letter-spacing: 0.01em;
46+
white-space: nowrap;
47+
color: #d9e8ff;
48+
}
2549
#flashDock {
2650
position: relative;
2751
width: clamp(240px, 40vw, 520px);
@@ -186,9 +210,44 @@ label span { display: block; font-size: 12px; opacity: 0.85; margin-bottom: 6px;
186210
.runnerActions .btn { padding: 8px 10px; }
187211
.small { font-size: 12px; opacity: 0.75; }
188212
.spinner {
189-
width: 16px; height: 16px; border-radius: 50%;
190-
border: 2px solid #2a355c; border-top-color: transparent;
191-
animation: spin 0.9s linear infinite;
213+
position: relative;
214+
isolation: isolate;
215+
display: grid;
216+
place-items: center;
217+
width: 22px;
218+
height: 22px;
219+
border-radius: 999px;
220+
font-size: 10px;
221+
font-weight: 760;
222+
letter-spacing: 0.01em;
223+
color: #dbe8ff;
224+
overflow: hidden;
225+
box-shadow: 0 0 0 1px rgba(45, 91, 255, 0.45), 0 0 12px rgba(45, 91, 255, 0.22);
226+
}
227+
.spinner::before {
228+
content: "";
229+
position: absolute;
230+
inset: 2px;
231+
border-radius: inherit;
232+
background: #0d1425;
233+
border: 1px solid rgba(75, 109, 184, 0.52);
234+
z-index: -1;
235+
}
236+
.spinner::after {
237+
content: "";
238+
position: absolute;
239+
inset: 0;
240+
border-radius: inherit;
241+
background: conic-gradient(from 180deg, #2d5bff, #45a0ff, #2f4f8f, #2d5bff);
242+
animation: spin 1.1s linear infinite;
243+
z-index: -2;
244+
}
245+
.spinner.is-scheduled {
246+
box-shadow: 0 0 0 1px rgba(74, 130, 219, 0.44), 0 0 10px rgba(47, 79, 143, 0.2);
247+
}
248+
.spinner.is-scheduled::after {
249+
background: radial-gradient(circle at 35% 35%, #70c2ff 0%, #2f4f8f 45%, #1d2f57 100%);
250+
animation: pulseGlow 1.6s ease-in-out infinite;
192251
}
193252
.hidden { display:none; }
194253
.check { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; opacity: 0.95; }
@@ -309,6 +368,15 @@ a.loglink { display:inline-block; margin-top:8px; font-size:12px; opacity:0.9; }
309368
padding: 8px 10px;
310369
}
311370
@keyframes spin { to { transform: rotate(360deg); } }
371+
@keyframes pulseGlow {
372+
0%, 100% { transform: scale(1); opacity: 0.88; }
373+
50% { transform: scale(1.08); opacity: 1; }
374+
}
375+
@media (prefers-reduced-motion: reduce) {
376+
.spinner::after {
377+
animation: none !important;
378+
}
379+
}
312380
@media (max-width: 920px) {
313381
#topHeader { flex-wrap: wrap; align-items: flex-start; }
314382
.headerRight { width: 100%; justify-content: space-between; }

templates/index.html

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@
1111
<header class="row between center" id="topHeader">
1212
<h1 id="openInfoTitle" class="titleInfoTrigger" role="button" tabindex="0" title="Öffnet die Programm-Info">multi-command-runner</h1>
1313
<div class="headerRight">
14-
<div class="row gap center" id="runningStatus">
15-
<div class="spinner hidden" id="globalSpinner"></div>
16-
<span class="small" id="runningCount"></span>
17-
</div>
1814
<div id="flashDock">
1915
<div id="uiFlash" class="flash info" aria-live="polite"></div>
2016
</div>
@@ -43,6 +39,10 @@ <h2 id="notifySectionTitle" style="margin:0;">Notification services</h2>
4339
<div class="row gap center">
4440
<span id="runnerSectionToggle" class="toggle">-</span>
4541
<h2 id="runnerSectionTitle" style="margin:0;">Runners</h2>
42+
<div class="row gap center hidden" id="runningStatus" role="status" aria-live="polite" aria-atomic="true">
43+
<div class="spinner hidden" id="globalSpinner"></div>
44+
<span id="runningCount"></span>
45+
</div>
4646
</div>
4747
<div class="row gap wrapline">
4848
<button id="sortRunnerBtn" class="btn">Sortieren: Aus</button>
@@ -58,19 +58,29 @@ <h2 id="runnerSectionTitle" style="margin:0;">Runners</h2>
5858
</section>
5959

6060
<section class="card">
61-
<div class="row between center" style="margin-bottom:10px;">
62-
<h2 id="notifyJournalTitle" style="margin:0;">Notification journal</h2>
61+
<div class="row between center wrapline" style="margin-bottom:10px;">
62+
<div class="row gap center">
63+
<span id="notifyJournalSectionToggle" class="toggle">-</span>
64+
<h2 id="notifyJournalTitle" style="margin:0;">Notification journal</h2>
65+
</div>
6366
<button id="clearNotifyJournalBtn" class="btn danger">Journal leeren</button>
6467
</div>
65-
<pre id="notifyJournal" class="output"></pre>
68+
<div id="notifyJournalSectionBody">
69+
<pre id="notifyJournal" class="output"></pre>
70+
</div>
6671
</section>
6772

6873
<section class="card">
69-
<div class="row between center" style="margin-bottom:10px;">
70-
<h2 id="eventsTitle" style="margin:0;">Events</h2>
74+
<div class="row between center wrapline" style="margin-bottom:10px;">
75+
<div class="row gap center">
76+
<span id="eventsSectionToggle" class="toggle">-</span>
77+
<h2 id="eventsTitle" style="margin:0;">Events</h2>
78+
</div>
7179
<button id="clearEventsBtn" class="btn danger">Events leeren</button>
7280
</div>
73-
<pre id="events" class="output"></pre>
81+
<div id="eventsSectionBody">
82+
<pre id="events" class="output"></pre>
83+
</div>
7484
</section>
7585
</div>
7686

0 commit comments

Comments
 (0)