feat: enhance scheduler functionality and UI improvements

- Updated scheduler.js to improve tab switching and content display.
- Refactored script fetching logic to handle new data structure.
- Enhanced schedule creation and editing with updated payload structure.
- Improved trigger testing and creation with consistent naming conventions.
- Added new CSS styles for actions and plugins pages to enhance UI/UX.
- Introduced responsive design adjustments for better mobile experience.
This commit is contained in:
infinition
2026-03-19 16:59:55 +01:00
parent b0584a1a8e
commit b541ec1f61
14 changed files with 2178 additions and 1286 deletions

View File

@@ -1,4 +1,4 @@
ftp_bruteforce.py - Threaded FTP credential bruteforcer, results stored in DB.
"""ftp_bruteforce.py - Threaded FTP credential bruteforcer, results stored in DB."""
import os
import threading

View File

@@ -1,4 +1,4 @@
smb_bruteforce.py - Threaded SMB credential bruteforcer with share enumeration.
"""smb_bruteforce.py - Threaded SMB credential bruteforcer with share enumeration."""
import os
import shlex

View File

@@ -1,4 +1,4 @@
sql_bruteforce.py - Threaded MySQL credential bruteforcer with database enumeration.
"""sql_bruteforce.py - Threaded MySQL credential bruteforcer with database enumeration."""
import os
import pymysql

View File

@@ -1,4 +1,4 @@
telnet_bruteforce.py - Threaded Telnet credential bruteforcer.
"""telnet_bruteforce.py - Threaded Telnet credential bruteforcer."""
import os
import telnetlib

View File

@@ -13,8 +13,10 @@
@import url("./pages/files.css");
@import url("./pages/compat.css");
@import url("./pages/backup.css");
@import url("./pages/actions.css");
@import url("./pages/actions-studio.css");
@import url("./pages/sentinel.css");
@import url("./pages/bifrost.css");
@import url("./pages/loki.css");
@import url("./pages/llm.css");
@import url("./pages/plugins.css");

688
web/css/pages/actions.css Normal file
View File

@@ -0,0 +1,688 @@
/* ==========================================================================
ACTIONS LAUNCHER (.actions-container)
Viewport-locked layout: sidebar + multi-console, no page-level scroll.
========================================================================== */
/* ── Root container ── */
.actions-container.page-with-sidebar {
--page-sidebar-w: 280px;
height: calc(100vh - var(--h-topbar, 56px) - var(--h-bottombar, 56px) - 24px);
max-height: calc(100vh - var(--h-topbar, 56px) - var(--h-bottombar, 56px) - 24px);
min-height: 0;
overflow: hidden;
}
/* ── Sidebar (locked to viewport, internally scrollable) ── */
.actions-container .al-sidebar.page-sidebar {
display: flex;
flex-direction: column;
max-height: 100%;
overflow: hidden;
}
.actions-container .sideheader {
padding: 8px 10px 6px;
border-bottom: 1px dashed var(--c-border);
flex-shrink: 0;
}
.actions-container .al-side-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 6px;
}
.actions-container .al-side-meta .sidetitle {
color: var(--acid);
font-weight: 800;
letter-spacing: .05em;
font-size: 13px;
}
/* ── Sidebar tabs ── */
.actions-container .tabs-container {
display: flex;
gap: 4px;
flex-wrap: nowrap;
padding: 0;
margin: 0;
border: none;
border-radius: 0;
background: transparent;
box-shadow: none;
min-height: 0;
position: static;
}
.actions-container .tab-btn {
all: unset;
cursor: pointer;
padding: 5px 10px;
border-radius: 8px;
background: var(--c-pill-bg, color-mix(in oklab, var(--c-panel-2) 70%, transparent));
border: 1px solid var(--c-border);
color: var(--muted);
font-size: 11px;
font-weight: 600;
white-space: nowrap;
transition: background .15s, color .15s;
}
.actions-container .tab-btn:hover {
color: var(--ink);
background: color-mix(in oklab, var(--c-panel-2) 90%, transparent);
}
.actions-container .tab-btn.active {
color: var(--acid);
background: var(--grad-chip-selected, color-mix(in oklab, var(--acid) 14%, var(--c-panel-2)));
border-color: color-mix(in oklab, var(--acid) 45%, transparent);
}
/* ── Search ── */
.actions-container .al-search {
display: flex;
gap: 8px;
padding: 6px 10px 8px;
}
.actions-container .al-input {
flex: 1;
background: var(--c-panel);
border: 1px solid var(--c-border-strong);
color: var(--ink);
padding: 6px 10px;
border-radius: var(--control-r, 8px);
font: inherit;
font-size: 12px;
}
.actions-container .al-input:focus {
outline: none;
box-shadow: 0 0 0 2px color-mix(in oklab, var(--acid) 55%, transparent) inset;
}
/* ── Sidebar content (scrollable area) ── */
.actions-container .sidecontent {
padding: 6px;
overflow-y: auto;
overflow-x: hidden;
min-height: 0;
flex: 1;
}
.actions-container .sidebar-page {
display: block;
}
/* ── Actions list ── */
.actions-container .al-list {
display: flex;
flex-direction: column;
gap: 4px;
}
/* ── Action row (compact) ── */
.actions-container .al-row {
position: relative;
display: grid;
grid-template-columns: 36px 1fr auto;
align-items: center;
gap: 8px;
padding: 6px 8px;
background: var(--c-panel-2);
border-radius: 8px;
cursor: pointer;
border: 1px solid transparent;
transition: background .12s, border-color .12s;
}
.actions-container .al-row:hover {
background: color-mix(in oklab, var(--c-panel-2) 80%, var(--acid) 6%);
border-color: color-mix(in oklab, var(--acid) 18%, transparent);
}
.actions-container .al-row.selected {
border-color: color-mix(in oklab, var(--acid) 40%, transparent);
background: color-mix(in oklab, var(--c-panel-2) 70%, var(--acid) 8%);
}
.actions-container .al-row .ic {
width: 36px;
height: 36px;
display: grid;
place-items: center;
border-radius: 6px;
background: var(--c-panel);
overflow: hidden;
flex-shrink: 0;
}
.actions-container .ic-img {
width: 28px;
height: 28px;
object-fit: cover;
display: block;
}
.actions-container .al-row > div:nth-child(2) {
min-width: 0;
display: flex;
flex-direction: column;
gap: 1px;
}
.actions-container .name {
font-weight: 700;
color: var(--acid-2, var(--acid));
font-size: 12px;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.actions-container .desc {
color: var(--muted);
font-size: 11px;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ── Status chip (inside row) ── */
.actions-container .al-row .chip {
position: static;
transform: none;
padding: 2px 6px;
border-radius: 999px;
border: 1px solid var(--c-border);
background: var(--c-chip-bg, color-mix(in oklab, var(--c-panel) 80%, transparent));
color: var(--muted);
font-size: 10px;
line-height: 1;
pointer-events: none;
white-space: nowrap;
flex-shrink: 0;
}
.actions-container .chip.ok {
color: var(--ok, #22c55e);
border-color: color-mix(in oklab, var(--ok, #22c55e) 50%, transparent);
}
.actions-container .chip.err {
color: var(--danger, #ef4444);
border-color: color-mix(in oklab, var(--danger, #ef4444) 50%, transparent);
}
.actions-container .chip.run {
color: var(--acid);
border-color: color-mix(in oklab, var(--acid) 50%, transparent);
}
/* ── Format badge for custom scripts ── */
.actions-container .al-row .format-badge {
font-size: 9px;
text-transform: uppercase;
letter-spacing: .04em;
background: color-mix(in oklab, var(--acid) 12%, transparent);
color: var(--acid);
border-color: color-mix(in oklab, var(--acid) 30%, transparent);
}
/* ── Section divider (Custom Scripts header) ── */
.actions-container .al-section-divider {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px 4px 4px;
margin-top: 4px;
}
.actions-container .al-section-title {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .06em;
color: var(--muted);
}
.actions-container .al-upload-btn {
font-size: 10px;
padding: 3px 8px;
}
.actions-container .al-delete-btn {
font-size: 12px;
padding: 2px 6px;
opacity: .5;
background: transparent;
border: none;
color: var(--muted);
cursor: pointer;
}
.actions-container .al-delete-btn:hover {
opacity: 1;
color: var(--danger, #ef4444);
}
/* ── Buttons (shared) ── */
.actions-container .al-btn {
background: var(--c-btn, var(--c-panel));
color: var(--ink);
border: 1px solid var(--c-border-strong);
border-radius: var(--control-r, 8px);
padding: 5px 10px;
display: inline-flex;
align-items: center;
gap: 6px;
cursor: pointer;
transition: .15s;
font: inherit;
font-size: 11px;
}
.actions-container .al-btn:hover {
background: color-mix(in oklab, var(--c-btn, var(--c-panel)) 85%, var(--acid) 15%);
border-color: color-mix(in oklab, var(--acid) 40%, var(--c-border-strong));
}
.actions-container .al-btn.warn {
background: linear-gradient(180deg, color-mix(in oklab, var(--warning, #f59e0b) 22%, var(--c-btn, var(--c-panel))), var(--c-btn, var(--c-panel)));
color: var(--warning, #f59e0b);
border-color: color-mix(in oklab, var(--warning, #f59e0b) 45%, var(--c-border));
}
/* ── Main area + center panel (fills viewport remainder) ── */
.actions-container #actionsLauncher {
min-height: 0;
height: 100%;
display: flex;
flex-direction: column;
}
.actions-container .panel {
background: var(--grad-card, var(--c-panel));
border: 1px solid var(--c-border);
border-radius: var(--radius, 12px);
box-shadow: var(--shadow);
overflow: hidden;
}
.actions-container .center {
display: flex;
flex-direction: column;
min-height: 0;
height: 100%;
flex: 1;
}
/* ── Toolbar (split selector) ── */
.actions-container .toolbar2 {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-bottom: 1px solid var(--c-border);
background: var(--c-panel);
flex-shrink: 0;
}
.actions-container .spacer {
flex: 1;
}
.actions-container .seg {
display: flex;
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--c-border);
}
.actions-container .seg button {
background: var(--c-panel);
color: var(--muted);
padding: 4px 10px;
border: none;
border-right: 1px solid var(--c-border);
cursor: pointer;
font: inherit;
font-size: 12px;
}
.actions-container .seg button:last-child {
border-right: none;
}
.actions-container .seg button.active {
color: var(--ink-invert, #000);
background: linear-gradient(90deg, var(--acid-2, var(--acid)), color-mix(in oklab, var(--acid-2, var(--acid)) 60%, white));
}
/* ── Multi-console grid (fills remaining height) ── */
.actions-container .multiConsole {
flex: 1;
padding: 8px;
display: grid;
gap: 8px;
min-height: 0;
grid-auto-flow: row;
grid-auto-rows: 1fr;
grid-template-rows: repeat(var(--rows, 1), 1fr);
overflow: hidden;
}
.actions-container .split-1 { grid-template-columns: 1fr; }
.actions-container .split-2 { grid-template-columns: 1fr 1fr; }
.actions-container .split-3 { grid-template-columns: 1fr 1fr 1fr; }
.actions-container .split-4 { grid-template-columns: 1fr 1fr; }
/* ── Console pane ── */
.actions-container .pane {
position: relative;
border: 1px solid var(--c-border);
border-radius: 10px;
background: var(--grad-console, var(--c-panel));
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.actions-container .paneHeader {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 6px 8px;
border-bottom: 1px solid var(--c-border);
background: linear-gradient(180deg, color-mix(in oklab, var(--acid-2, var(--acid)) 6%, transparent), transparent);
flex-shrink: 0;
flex-wrap: wrap;
}
.actions-container .paneTitle {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
flex: 1;
}
.actions-container .paneTitle .dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.actions-container .paneIcon {
width: 28px;
height: 28px;
border-radius: 4px;
object-fit: cover;
flex-shrink: 0;
}
.actions-container .titleBlock {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.actions-container .titleLine strong {
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
}
.actions-container .metaLine {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.actions-container .metaLine .chip {
position: static;
transform: none;
border: 1px solid var(--c-border-strong);
background: var(--c-chip-bg, color-mix(in oklab, var(--c-panel) 80%, transparent));
color: var(--muted);
padding: 1px 6px;
border-radius: 999px;
font-size: 10px;
}
.actions-container .paneBtns {
display: flex;
flex-wrap: wrap;
gap: 4px;
justify-content: flex-end;
flex-shrink: 0;
}
.actions-container .paneBtns .al-btn {
padding: 3px 7px;
font-size: 10px;
}
/* ── Log output ── */
.actions-container .paneLog {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 4px 8px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
font-size: 11px;
min-height: 0;
}
.actions-container .logline {
white-space: pre-wrap;
word-break: break-word;
padding: 2px 4px;
line-height: 1.35;
color: var(--ink);
}
.actions-container .logline.info { color: #bfefff; }
.actions-container .logline.ok { color: #9ff7c5; }
.actions-container .logline.warn { color: #ffd27a; }
.actions-container .logline.err { color: #ff99b3; }
.actions-container .logline.dim { color: #6a8596; }
/* ── Pane drag highlight ── */
.actions-container .paneHighlight {
box-shadow:
0 0 0 2px var(--acid-2, var(--acid)),
0 0 20px color-mix(in oklab, var(--acid-2, var(--acid)) 40%, transparent) inset;
animation: al-hi 700ms ease-out 1;
}
@keyframes al-hi {
0% { transform: scale(1); }
50% { transform: scale(1.005); }
100% { transform: scale(1); }
}
/* ── Arguments tab ── */
.actions-container .section {
padding: 10px;
border-bottom: 1px dashed var(--c-border);
}
.actions-container .h {
font-weight: 800;
letter-spacing: .5px;
color: var(--acid-2, var(--acid));
font-size: 13px;
}
.actions-container .sub {
color: var(--muted);
font-size: 11px;
}
.actions-container .builder {
padding: 10px;
display: grid;
gap: 10px;
}
.actions-container .field {
display: grid;
gap: 4px;
}
.actions-container .label {
font-size: 11px;
color: var(--muted);
}
.actions-container .ctl,
.actions-container .select,
.actions-container .range {
background: var(--c-panel);
color: var(--ink);
border: 1px solid var(--c-border-strong);
border-radius: var(--control-r, 8px);
padding: 6px 10px;
font: inherit;
font-size: 12px;
}
.actions-container .ctl:focus,
.actions-container .select:focus {
outline: none;
box-shadow: 0 0 0 2px color-mix(in oklab, var(--acid) 55%, transparent) inset;
}
.actions-container .chips {
display: flex;
gap: 6px;
flex-wrap: wrap;
padding: 8px 10px;
}
.actions-container .chip2 {
padding: 4px 10px;
border-radius: 999px;
background: var(--c-chip-bg, color-mix(in oklab, var(--c-panel) 80%, transparent));
border: 1px solid var(--c-border-hi, var(--c-border-strong));
cursor: pointer;
font-size: 11px;
transition: .15s;
}
.actions-container .chip2:hover {
border-color: color-mix(in oklab, var(--acid) 50%, var(--c-border));
color: var(--acid);
}
/* ── Mobile responsive ── */
@media (max-width: 860px) {
.actions-container.page-with-sidebar {
height: calc(100vh - var(--h-topbar, 56px) - var(--h-bottombar, 56px) - 12px);
max-height: calc(100vh - var(--h-topbar, 56px) - var(--h-bottombar, 56px) - 12px);
}
.actions-container #actionsLauncher {
min-height: 0;
}
.actions-container .toolbar2 {
display: none !important;
}
.actions-container .paneHeader {
flex-direction: column;
align-items: flex-start;
gap: 6px;
}
.actions-container .paneBtns {
justify-content: flex-start;
width: 100%;
}
.actions-container .paneBtns .al-btn {
padding: 3px 6px;
font-size: 10px;
}
.actions-container .multiConsole {
padding: 6px;
}
}
/* ===== Per-Pane Focus ===== */
.actions-container .pane.paneFocused {
border-color: color-mix(in oklab, var(--acid) 40%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--acid) 15%, transparent);
}
.args-pane-label {
font-size: 11px;
font-weight: 700;
color: var(--acid);
padding: 4px 0 6px;
border-bottom: 1px solid var(--c-border);
margin-bottom: 6px;
}
/* ===== Custom Scripts Split Layout ===== */
.actions-container #tab-actions.al-split-layout {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
overflow: hidden;
}
.actions-container .al-builtins-scroll {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
min-height: 0;
}
.actions-container .al-custom-section {
flex-shrink: 0;
max-height: 180px;
border-top: 1px dashed var(--c-border);
display: flex;
flex-direction: column;
}
.actions-container .al-custom-scroll {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
min-height: 0;
}
.actions-container .al-section-divider {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
background: color-mix(in oklab, var(--c-panel-2, var(--c-panel)) 50%, transparent);
flex-shrink: 0;
}
.actions-container .al-section-title {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--muted);
}

View File

@@ -1768,9 +1768,7 @@
}
/* ---- Final parity aliases ---- */
.actions-container .sidebar-page {
display: block;
}
/* .actions-container .sidebar-page moved to actions.css */
.vuln-container.page-with-sidebar {
--page-sidebar-w: 300px;

View File

@@ -1622,514 +1622,7 @@
}
}
/* ==========================================================================
ACTIONS LAUNCHER (.actions-container)
========================================================================== */
.actions-container #actionsLauncher {
min-height: 0;
height: 100%;
display: grid;
grid-template-columns: 1fr;
gap: var(--gap-3, 10px);
}
.actions-container .panel {
background: var(--grad-card, var(--c-panel));
border: 1px solid var(--c-border);
border-radius: var(--radius, 14px);
box-shadow: var(--elev, 0 10px 30px var(--acid-1a, #00ff9a1a), inset 0 0 0 1px var(--acid-22, #00ff9a22));
overflow: clip;
}
.actions-container .sideheader {
padding: 10px 10px 6px;
border-bottom: 1px dashed var(--c-border);
}
.actions-container .al-side-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
.actions-container .al-side-meta .sidetitle {
color: var(--acid);
font-weight: 800;
letter-spacing: .05em;
}
.actions-container .tabs-container {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.actions-container .tab-btn {
all: unset;
cursor: pointer;
padding: 6px 10px;
border-radius: 10px;
background: var(--c-pill-bg);
border: 1px solid var(--c-border);
color: var(--muted);
}
.actions-container .tab-btn.active {
background: var(--grad-chip-selected);
outline: 2px solid color-mix(in oklab, var(--acid) 55%, transparent);
outline-offset: 0;
}
.actions-container .al-search {
display: flex;
gap: 10px;
padding: 10px;
}
.actions-container .al-input {
flex: 1;
background: var(--c-panel);
border: 1px solid var(--c-border-strong);
color: var(--ink);
padding: 10px 12px;
border-radius: var(--control-r, 10px);
font: inherit;
}
.actions-container .al-input:focus {
outline: none;
box-shadow: 0 0 0 2px color-mix(in oklab, var(--acid) 55%, transparent) inset;
}
.actions-container .sidecontent {
padding: 8px;
overflow: auto;
}
.actions-container .al-list {
display: flex;
flex-direction: column;
gap: 10px;
padding-right: 4px;
}
.actions-container .al-row {
position: relative;
display: grid;
grid-template-columns: 84px 1fr;
gap: 12px;
padding: 10px;
background: var(--c-panel-2);
border-radius: 12px;
cursor: pointer;
transition: transform .15s ease, border-color .15s ease, box-shadow .15s ease;
}
.actions-container .al-row:hover {
transform: translateY(-1px);
border-color: color-mix(in oklab, var(--accent) 25%, var(--c-border));
box-shadow: 0 10px 26px var(--glow-weak);
}
.actions-container .al-row.selected {
outline: 2px solid color-mix(in oklab, var(--acid) 35%, transparent);
box-shadow: 0 12px 30px color-mix(in oklab, var(--acid) 25%, transparent);
}
.actions-container .al-row .ic {
width: 84px;
height: 84px;
display: grid;
place-items: center;
border-radius: 12px;
background: var(--c-panel);
overflow: hidden;
}
.actions-container .ic-img {
width: 70px;
height: 70px;
object-fit: cover;
display: block;
}
.actions-container .al-row>div:nth-child(2) {
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.actions-container .name {
font-weight: 800;
color: var(--acid-2);
font-size: 14px;
line-height: 1.2;
}
.actions-container .desc {
color: var(--muted);
font-size: 13px;
line-height: 1.25;
}
.actions-container .al-row .chip {
position: absolute;
top: 6px;
left: calc(84px/2 + 10px);
transform: translateX(-50%);
padding: 2px 8px;
border-radius: 999px;
border: 1px solid var(--c-border);
background: var(--c-chip-bg);
color: var(--muted);
font-size: 11px;
line-height: 1;
pointer-events: none;
}
.actions-container .chip.ok {
color: var(--ok);
border-color: color-mix(in oklab, var(--ok) 60%, transparent);
}
.actions-container .chip.err {
color: var(--danger);
border-color: color-mix(in oklab, var(--danger) 60%, transparent);
}
.actions-container .chip.run {
color: var(--acid);
border-color: color-mix(in oklab, var(--acid) 60%, transparent);
}
.actions-container .center {
display: flex;
flex-direction: column;
min-height: 0;
height: 100%;
}
.actions-container .toolbar2 {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border-bottom: 1px solid var(--c-border);
background: var(--c-panel);
flex-wrap: wrap;
}
.actions-container .seg {
display: flex;
border-radius: 10px;
overflow: hidden;
border: 1px solid var(--c-border);
}
.actions-container .seg button {
background: var(--c-panel);
color: var(--muted);
padding: 8px 10px;
border: none;
border-right: 1px solid var(--c-border);
cursor: pointer;
font: inherit;
}
.actions-container .seg button:last-child {
border-right: none;
}
.actions-container .seg button.active {
color: var(--ink-invert);
background: linear-gradient(90deg, var(--acid-2), color-mix(in oklab, var(--acid-2) 60%, white));
}
.actions-container .al-btn {
background: var(--c-btn);
color: var(--ink);
border: 1px solid var(--c-border-strong);
border-radius: var(--control-r, 10px);
padding: 8px 12px;
display: inline-flex;
align-items: center;
gap: 8px;
cursor: pointer;
transition: .18s;
box-shadow: var(--elev);
font: inherit;
}
.actions-container .al-btn:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-hover);
}
.actions-container .al-btn.warn {
background: linear-gradient(180deg, color-mix(in oklab, var(--warning) 28%, var(--c-btn)), var(--c-btn));
color: var(--warning);
border-color: color-mix(in oklab, var(--warning) 55%, var(--c-border));
}
.actions-container .multiConsole {
flex: 1;
padding: 10px;
display: grid;
gap: 10px;
height: 100%;
grid-auto-flow: row;
grid-auto-rows: 1fr;
grid-template-rows: repeat(var(--rows, 1), 1fr);
}
.actions-container .split-1 {
grid-template-columns: 1fr;
}
.actions-container .split-2 {
grid-template-columns: 1fr 1fr;
}
.actions-container .split-3 {
grid-template-columns: 1fr 1fr 1fr;
}
.actions-container .split-4 {
grid-template-columns: 1fr 1fr;
}
.actions-container .pane {
position: relative;
border: 1px solid var(--c-border);
border-radius: 12px;
background: var(--grad-console);
display: flex;
flex-direction: column;
box-shadow: inset 0 0 0 1px var(--c-border-muted);
}
.actions-container .paneHeader {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-bottom: 1px solid var(--c-border);
background: linear-gradient(180deg, color-mix(in oklab, var(--acid-2) 8%, transparent), transparent);
}
.actions-container .paneTitle {
display: grid;
grid-template-columns: auto auto 1fr;
align-items: center;
gap: 10px;
min-width: 0;
}
.actions-container .paneTitle .dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex: 0 0 auto;
}
.actions-container .paneIcon {
width: 70px;
height: 70px;
border-radius: 6px;
object-fit: cover;
opacity: .95;
}
.actions-container .titleBlock {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.actions-container .titleLine strong {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.actions-container .metaLine {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.actions-container .metaLine .chip {
border: 1px solid var(--c-border-strong);
background: var(--c-chip-bg);
color: var(--muted);
padding: 3px 8px;
border-radius: 999px;
}
.actions-container .paneBtns {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: flex-end;
}
.actions-container .paneBtns .al-btn {
padding: 6px 8px;
font-size: .9rem;
}
.actions-container .paneLog {
flex: 1;
overflow: auto;
padding: 6px 8px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
font-size: .92rem;
}
.actions-container .logline {
white-space: pre-wrap;
word-break: break-word;
padding: 4px 6px;
line-height: 1.32;
color: var(--ink);
}
.actions-container .logline.info {
color: #bfefff;
}
.actions-container .logline.ok {
color: #9ff7c5;
}
.actions-container .logline.warn {
color: #ffd27a;
}
.actions-container .logline.err {
color: #ff99b3;
}
.actions-container .logline.dim {
color: #6a8596;
}
.actions-container .paneHighlight {
box-shadow: 0 0 0 2px var(--acid-2), 0 0 24px color-mix(in oklab, var(--acid-2) 55%, transparent) inset, 0 0 40px color-mix(in oklab, var(--acid-2) 35%, transparent);
animation: al-hi 900ms ease-out 1;
}
@keyframes al-hi {
0% {
transform: scale(1);
}
50% {
transform: scale(1.01);
}
100% {
transform: scale(1);
}
}
.actions-container .section {
padding: 12px;
border-bottom: 1px dashed var(--c-border);
}
.actions-container .h {
font-weight: 800;
letter-spacing: .5px;
color: var(--acid-2);
}
.actions-container .sub {
color: var(--muted);
font-size: .9rem;
}
.actions-container .builder {
padding: 12px;
display: grid;
gap: 12px;
}
.actions-container .field {
display: grid;
gap: 6px;
}
.actions-container .label {
font-size: .85rem;
color: var(--muted);
}
.actions-container .ctl,
.actions-container .select,
.actions-container .range {
background: var(--c-panel);
color: var(--ink);
border: 1px solid var(--c-border-strong);
border-radius: var(--control-r, 10px);
padding: 10px 12px;
font: inherit;
}
.actions-container .ctl:focus,
.actions-container .select:focus {
outline: none;
box-shadow: 0 0 0 2px color-mix(in oklab, var(--acid) 55%, transparent) inset;
}
.actions-container .chips {
display: flex;
gap: 8px;
flex-wrap: wrap;
padding: 10px;
}
.actions-container .chip2 {
padding: 6px 10px;
border-radius: 999px;
background: var(--c-chip-bg);
border: 1px solid var(--c-border-hi);
cursor: pointer;
transition: .18s;
}
.actions-container .chip2:hover {
box-shadow: 0 0 0 1px var(--c-border-hi) inset, 0 8px 22px var(--glow-weak);
}
@media (max-width: 860px) {
.actions-container #actionsLauncher {
grid-template-columns: 1fr;
}
.actions-container .toolbar2 {
display: none !important;
}
.actions-container .paneHeader {
grid-template-columns: 1fr;
row-gap: 8px;
}
.actions-container .paneBtns {
justify-content: flex-start;
}
.actions-container .paneBtns .al-btn {
padding: 5px 6px;
font-size: .85rem;
}
}
/* Actions launcher styles moved to actions.css */
/* ==========================================================================
ACTIONS STUDIO (.studio-container)

277
web/css/pages/plugins.css Normal file
View File

@@ -0,0 +1,277 @@
/* ==========================================================================
PLUGINS PAGE
========================================================================== */
.plugins-page {
padding: 1rem;
max-width: 900px;
margin: 0 auto;
}
.plugins-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: .5rem;
}
.plugins-header h1 {
font-size: 1.4rem;
font-weight: 800;
margin: 0;
color: var(--ink);
}
.plugins-actions {
display: flex;
gap: 6px;
}
.plugins-actions .btn {
padding: 5px 14px;
border: 1px solid var(--c-border);
border-radius: 6px;
background: var(--c-panel);
color: var(--ink);
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.plugins-actions .btn:hover { border-color: var(--acid); color: var(--acid); }
.plugins-actions .btn-primary {
background: color-mix(in oklab, var(--acid) 15%, transparent);
border-color: var(--acid);
color: var(--acid);
}
.plugins-actions .btn-primary:hover { background: color-mix(in oklab, var(--acid) 25%, transparent); }
.plugins-count {
color: var(--muted);
font-size: 12px;
margin: 0 0 12px;
}
.plugins-empty {
text-align: center;
color: var(--muted);
padding: 2rem;
font-size: 13px;
}
.plugins-grid {
display: flex;
flex-direction: column;
gap: 8px;
}
/* ===== Plugin card ===== */
.plugin-card {
border: 1px solid var(--c-border);
border-radius: 10px;
background: var(--c-panel);
padding: 12px 14px;
display: flex;
flex-direction: column;
gap: 6px;
transition: border-color .15s;
}
.plugin-card:hover { border-color: var(--c-border-strong); }
.plugin-card.plugin-disabled { opacity: .55; }
.plugin-card-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.plugin-card-title {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.plugin-card-title strong {
font-size: 13px;
color: var(--ink);
}
.plugin-type-badge {
display: inline-block;
padding: 1px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
background: color-mix(in oklab, var(--acid) 15%, transparent);
color: var(--acid);
}
.badge-action { background: color-mix(in oklab, #3b82f6 15%, transparent); color: #3b82f6; }
.badge-notifier { background: color-mix(in oklab, #f59e0b 15%, transparent); color: #f59e0b; }
.badge-enricher { background: color-mix(in oklab, #8b5cf6 15%, transparent); color: #8b5cf6; }
.badge-exporter { background: color-mix(in oklab, #06b6d4 15%, transparent); color: #06b6d4; }
.badge-widget { background: color-mix(in oklab, #ec4899 15%, transparent); color: #ec4899; }
.plugin-status {
font-size: 10px;
font-weight: 600;
padding: 1px 6px;
border-radius: 3px;
}
.plugin-status.status-loaded { background: color-mix(in oklab, #22c55e 18%, transparent); color: #22c55e; }
.plugin-status.status-disabled { background: color-mix(in oklab, var(--muted) 15%, transparent); color: var(--muted); }
.plugin-status.status-error { background: color-mix(in oklab, #ef4444 18%, transparent); color: #ef4444; }
.plugin-status.status-missing { background: color-mix(in oklab, #f59e0b 18%, transparent); color: #f59e0b; }
.plugin-card-info { font-size: 12px; }
.plugin-desc { color: var(--ink); margin: 0 0 4px; line-height: 1.4; }
.plugin-meta { color: var(--muted); font-size: 11px; display: flex; gap: 8px; }
.plugin-hooks {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.hook-badge {
padding: 1px 6px;
border: 1px solid var(--c-border);
border-radius: 3px;
font-size: 10px;
color: var(--muted);
font-family: monospace;
}
.plugin-error {
padding: 6px 8px;
background: color-mix(in oklab, #ef4444 10%, transparent);
border: 1px solid color-mix(in oklab, #ef4444 30%, transparent);
border-radius: 5px;
color: #fca5a5;
font-size: 11px;
}
.plugin-deps-warn {
padding: 4px 8px;
background: color-mix(in oklab, #f59e0b 10%, transparent);
border: 1px solid color-mix(in oklab, #f59e0b 30%, transparent);
border-radius: 5px;
color: #fcd34d;
font-size: 11px;
}
.plugin-card-actions {
display: flex;
gap: 4px;
margin-top: 2px;
}
.plugin-card-actions button {
padding: 4px 10px;
border: 1px solid var(--c-border);
border-radius: 5px;
background: transparent;
color: var(--muted);
font-size: 11px;
cursor: pointer;
}
.plugin-card-actions button:hover { color: var(--ink); border-color: var(--c-border-strong); }
.plugin-card-actions .btn-danger { color: #ef4444; border-color: color-mix(in oklab, #ef4444 40%, transparent); }
.plugin-card-actions .btn-danger:hover { background: color-mix(in oklab, #ef4444 12%, transparent); }
/* ===== Plugin toggle ===== */
.plugin-toggle {
position: relative;
display: inline-block;
width: 36px;
height: 20px;
cursor: pointer;
flex-shrink: 0;
}
.plugin-toggle input { opacity: 0; width: 0; height: 0; position: absolute; }
.plugin-toggle-track {
position: absolute;
inset: 0;
border-radius: 10px;
background: var(--c-border);
transition: background .2s;
}
.plugin-toggle-track::before {
content: '';
position: absolute;
width: 16px;
height: 16px;
left: 2px;
bottom: 2px;
border-radius: 50%;
background: var(--ink);
transition: transform .2s;
}
.plugin-toggle input:checked + .plugin-toggle-track { background: var(--acid); }
.plugin-toggle input:checked + .plugin-toggle-track::before { transform: translateX(16px); }
/* ===== Config modal ===== */
.plugin-config-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.plugin-config-modal {
width: min(600px, 90vw);
max-height: 80vh;
background: var(--panel);
border: 1px solid var(--c-border-strong);
border-radius: 12px;
box-shadow: 0 20px 56px rgba(0,0,0,.6);
display: flex;
flex-direction: column;
overflow: hidden;
}
.plugin-config-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
border-bottom: 1px solid var(--c-border);
font-weight: 700;
}
.plugin-config-body {
padding: 14px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
}
.plugin-config-field label {
display: flex;
flex-direction: column;
gap: 3px;
font-size: 11px;
color: var(--muted);
font-weight: 600;
}
.plugin-config-field input,
.plugin-config-field select,
.plugin-config-field textarea {
padding: 5px 8px;
border: 1px solid var(--c-border);
border-radius: 5px;
background: var(--bg);
color: var(--ink);
font-size: 12px;
width: 100%;
}
.plugin-config-footer {
display: flex;
justify-content: flex-end;
gap: 6px;
padding: 10px 14px;
border-top: 1px solid var(--c-border);
}
/* Hidden file input for plugin install */
.plugins-page input[type="file"] { display: none; }

View File

@@ -976,3 +976,297 @@
}
}
/* ===== Schedules & Triggers form panels ===== */
.schedules-panel,
.triggers-panel {
display: flex;
flex-direction: column;
gap: 12px;
}
.schedules-form,
.triggers-form {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px 14px;
border: 1px solid var(--c-border);
border-radius: 10px;
background: var(--c-panel);
}
.schedules-form h3,
.triggers-form h3 {
margin: 0 0 4px;
font-size: 13px;
font-weight: 700;
color: var(--acid);
}
.schedules-form .form-row,
.triggers-form .form-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.schedules-form label,
.triggers-form label {
font-size: 11px;
color: var(--muted);
font-weight: 600;
min-width: 110px;
}
.schedules-form input,
.schedules-form select,
.triggers-form input,
.triggers-form select {
padding: 5px 8px;
border: 1px solid var(--c-border);
border-radius: 5px;
background: var(--bg);
color: var(--ink);
font-size: 12px;
}
.schedules-form input:focus,
.triggers-form input:focus,
.schedules-form select:focus,
.triggers-form select:focus {
outline: 1px solid var(--acid);
border-color: var(--acid);
}
/* Toggle switch (used in schedule/trigger cards) */
.scheduler-container .toggle-switch {
position: relative;
display: inline-block;
width: 32px;
height: 18px;
cursor: pointer;
flex-shrink: 0;
}
.scheduler-container .toggle-switch input { opacity: 0; width: 0; height: 0; position: absolute; }
.scheduler-container .toggle-slider {
position: absolute;
inset: 0;
border-radius: 9px;
background: var(--c-border);
transition: background .2s;
}
.scheduler-container .toggle-slider::before {
content: '';
position: absolute;
width: 14px;
height: 14px;
left: 2px;
bottom: 2px;
border-radius: 50%;
background: var(--ink);
transition: transform .2s;
}
.scheduler-container .toggle-switch input:checked + .toggle-slider { background: var(--acid); }
.scheduler-container .toggle-switch input:checked + .toggle-slider::before { transform: translateX(14px); }
/* ===== Condition builder ===== */
.cond-editor {
border: 1px solid var(--c-border);
border-radius: 8px;
padding: 8px;
background: color-mix(in oklab, var(--bg) 80%, var(--c-panel));
}
.cond-group {
border-left: 3px solid var(--acid);
padding: 6px 8px;
margin: 4px 0;
border-radius: 4px;
background: color-mix(in oklab, var(--acid) 4%, transparent);
}
.cond-group-or { border-left-color: #f59e0b; background: color-mix(in oklab, #f59e0b 4%, transparent); }
.cond-group-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
}
.cond-op-toggle {
padding: 2px 6px;
border: 1px solid var(--c-border);
border-radius: 4px;
background: var(--bg);
color: var(--acid);
font-size: 11px;
font-weight: 700;
}
.cond-children {
display: flex;
flex-direction: column;
gap: 4px;
}
.cond-item-wrapper {
display: flex;
align-items: flex-start;
gap: 4px;
}
.cond-item-wrapper > :first-child { flex: 1; }
.cond-delete-btn {
background: transparent;
border: 1px solid var(--c-border);
border-radius: 4px;
color: #ef4444;
cursor: pointer;
padding: 2px 6px;
font-size: 12px;
line-height: 1;
flex-shrink: 0;
margin-top: 2px;
}
.cond-delete-btn:hover { background: color-mix(in oklab, #ef4444 15%, transparent); }
.cond-block {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
padding: 4px 8px;
border: 1px solid var(--c-border);
border-radius: 6px;
background: var(--c-panel);
}
.cond-source-select {
padding: 3px 6px;
border: 1px solid var(--c-border);
border-radius: 4px;
background: var(--bg);
color: var(--ink);
font-size: 11px;
}
.cond-params {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.cond-param-label {
display: flex;
align-items: center;
gap: 3px;
font-size: 10px;
color: var(--muted);
}
.cond-param-name { font-weight: 600; }
.cond-param-input {
padding: 3px 6px;
border: 1px solid var(--c-border);
border-radius: 4px;
background: var(--bg);
color: var(--ink);
font-size: 11px;
width: 80px;
}
.cond-param-input[type="number"] { width: 60px; }
.cond-add-btn {
padding: 3px 10px;
border: 1px dashed var(--c-border);
border-radius: 4px;
background: transparent;
color: var(--muted);
font-size: 11px;
cursor: pointer;
}
.cond-add-btn:hover { color: var(--acid); border-color: var(--acid); }
.cond-group-actions {
display: flex;
gap: 4px;
margin-top: 4px;
}
/* ===== Packages tab (in Actions page) ===== */
.pkg-install-form {
display: flex;
gap: 6px;
align-items: center;
padding: 8px 0;
}
.pkg-install-form input {
flex: 1;
padding: 5px 8px;
border: 1px solid var(--c-border);
border-radius: 5px;
background: var(--bg);
color: var(--ink);
font-size: 12px;
}
.pkg-install-btn {
padding: 5px 12px;
border: 1px solid var(--acid);
border-radius: 5px;
background: color-mix(in oklab, var(--acid) 15%, transparent);
color: var(--acid);
font-weight: 600;
cursor: pointer;
font-size: 12px;
}
.pkg-install-btn:hover { background: color-mix(in oklab, var(--acid) 25%, transparent); }
.pkg-console {
background: #0a0d10;
border: 1px solid var(--c-border);
border-radius: 6px;
padding: 8px;
font-family: monospace;
font-size: 11px;
color: var(--acid);
max-height: 200px;
overflow-y: auto;
white-space: pre-wrap;
display: none;
}
.pkg-console.active { display: block; }
.pkg-list {
list-style: none;
padding: 0;
margin: 8px 0 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.pkg-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
border: 1px solid var(--c-border);
border-radius: 6px;
background: var(--c-panel);
font-size: 12px;
}
.pkg-item .pkg-name { font-weight: 600; color: var(--ink); }
.pkg-item .pkg-version { color: var(--muted); font-size: 11px; }
.pkg-item .pkg-uninstall {
padding: 2px 8px;
border: 1px solid #ef4444;
border-radius: 4px;
background: transparent;
color: #ef4444;
font-size: 10px;
cursor: pointer;
}
.pkg-item .pkg-uninstall:hover { background: color-mix(in oklab, #ef4444 15%, transparent); }

View File

@@ -1,493 +1,9 @@
/* ==========================================================================
ZOMBIELAND (C2 Module) CSS
ZOMBIELAND C2 Dashboard (pages/ copy)
NOTE: The authoritative version lives at /web/css/zombieland.css and is
loaded dynamically by zombieland.js via loadStylesheet().
This file is kept in sync for consistency but is NOT imported in pages.css
because the JS handles its own stylesheet lifecycle.
========================================================================== */
/* Main layout constraints */
.zombieland-container.page-with-sidebar {
height: calc(100vh - var(--h-topbar, 56px) - var(--h-bottombar, 56px) - 24px);
display: flex;
min-height: 0;
}
.zl-sidebar.page-sidebar {
width: 260px;
flex-shrink: 0;
display: flex;
flex-direction: column;
background: var(--grad-card);
border: 1px solid var(--c-border);
border-radius: 12px;
overflow-y: auto;
}
.zl-main.page-main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 12px;
}
/* Sidebar structure */
.zl-stats-grid {
display: grid;
grid-template-columns: 1fr;
gap: 8px;
margin-bottom: 15px;
}
.stat-item {
background: rgba(0, 0, 0, 0.2);
border: 1px solid var(--c-border);
padding: 10px;
border-radius: 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
.stat-item .stat-value {
font-weight: bold;
color: var(--acid);
font-size: 1.1rem;
}
.stat-item .stat-label {
font-size: 0.8rem;
color: var(--muted);
text-transform: uppercase;
}
.zl-toolbar {
display: flex;
flex-direction: column;
gap: 6px;
}
/* Modals */
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: var(--grad-card);
border: 1px solid var(--c-border);
padding: 20px;
border-radius: 12px;
width: 100%;
max-width: 400px;
}
/* Main Grid Layout */
.zl-main-grid {
display: grid;
grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
gap: 12px;
flex: 1;
/* Takes available space except logs */
min-height: 0;
}
.zl-console-panel,
.zl-agents-panel,
.zl-logs-panel {
border: 1px solid var(--c-border);
border-radius: 12px;
background: var(--c-panel);
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Log Panel underneath */
.zl-logs-panel {
height: 150px;
flex-shrink: 0;
}
.zl-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: rgba(0, 0, 0, 0.2);
border-bottom: 1px solid var(--c-border);
flex-shrink: 0;
}
.zl-panel-title {
font-weight: bold;
font-size: 0.9rem;
color: var(--acid);
}
.zl-quickbar {
display: flex;
gap: 4px;
}
.quick-cmd {
background: transparent;
border: 1px solid var(--c-border-strong);
color: var(--muted);
font-size: 0.7rem;
padding: 2px 6px;
border-radius: 4px;
cursor: pointer;
transition: 0.2s;
}
.quick-cmd:hover {
color: var(--acid);
border-color: var(--acid);
}
.zl-console-output,
.zl-logs-output {
flex: 1;
overflow-y: auto;
padding: 10px;
font-family: 'Fira Code', monospace;
font-size: 0.8rem;
background: #020406;
min-height: 0;
}
/* Controls */
.zl-console-input-row {
display: flex;
gap: 8px;
padding: 8px;
border-top: 1px solid var(--c-border);
background: var(--c-panel-2);
flex-shrink: 0;
}
.zl-target-select,
.zl-cmd-input,
.zl-search-input {
background: #000;
color: #fff;
border: 1px solid var(--c-border-strong);
padding: 6px 10px;
border-radius: 6px;
}
.zl-cmd-input {
flex: 1;
}
.zl-toolbar-left {
display: flex;
position: relative;
flex: 1;
max-width: 200px;
margin-left: 10px;
}
.zl-search-input {
width: 100%;
border-radius: 6px;
padding: 4px 20px 4px 8px;
font-size: 0.8rem;
}
.zl-search-clear {
position: absolute;
right: 5px;
top: 5px;
color: var(--muted);
background: none;
border: none;
cursor: pointer;
}
.zl-agents-list {
flex: 1;
overflow-y: auto;
padding: 10px;
display: flex;
flex-direction: column;
gap: 8px;
}
/* Agent Card Styles */
.zl-agent-card {
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--c-border);
border-radius: 8px;
padding: 8px 12px;
display: flex;
flex-direction: column;
gap: 6px;
transition: 0.2s ease-out;
}
.zl-agent-card.selected {
border-color: var(--acid);
background: rgba(0, 255, 160, 0.05);
}
.zl-agent-card:hover {
border-color: var(--c-border-hi);
}
.zl-card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.zl-card-identity {
display: flex;
flex-direction: column;
gap: 2px;
line-height: 1;
}
.zl-card-hostname {
font-weight: bold;
color: #fff;
font-size: 0.9rem;
}
.zl-card-id {
font-size: 0.7rem;
color: var(--muted);
}
.zl-pill {
padding: 2px 8px;
border-radius: 12px;
font-size: 0.7rem;
font-weight: bold;
background: #222;
}
.zl-pill.online {
color: #00ffa0;
background: rgba(0, 255, 160, 0.1);
}
.zl-pill.idle {
color: #ffcc00;
background: rgba(255, 204, 0, 0.1);
}
.zl-pill.offline {
color: #ff3333;
background: rgba(255, 51, 51, 0.1);
}
/* ECG Animation */
.zl-ecg-row {
display: flex;
align-items: center;
gap: 8px;
}
.ecg {
width: 100%;
height: 24px;
max-width: 140px;
position: relative;
overflow: hidden;
background: rgba(0, 0, 0, 0.5);
border-radius: 6px;
border: 1px solid #111;
}
.ecg-wrapper {
display: flex;
width: 300%;
animation: ecg-slide linear infinite;
}
.ecg svg {
width: 33.33%;
height: 100%;
}
.ecg path {
fill: none;
stroke-width: 1.5;
stroke-linecap: round;
stroke-linejoin: round;
}
.ecg.green path {
stroke: #00ffa0;
filter: drop-shadow(0 0 2px #00ffa0);
}
.ecg.yellow path {
stroke: #ffcc00;
filter: drop-shadow(0 0 2px #ffcc00);
}
.ecg.orange path {
stroke: #ff8800;
filter: drop-shadow(0 0 2px #ff8800);
}
.ecg.red path {
stroke: #ff3333;
}
.ecg.flat .ecg-wrapper {
animation: none;
}
@keyframes ecg-slide {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-33.33%);
}
}
.zl-ecg-counter {
font-size: 0.7rem;
color: var(--muted);
font-family: monospace;
}
.zl-card-info {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
color: #ccc;
background: rgba(0, 0, 0, 0.2);
padding: 4px 8px;
border-radius: 4px;
}
.zl-card-actions {
display: flex;
justify-content: flex-end;
gap: 4px;
margin-top: 4px;
}
/* Console output items */
.console-line {
margin-bottom: 4px;
display: flex;
gap: 8px;
}
.console-time {
color: var(--muted);
}
.console-type {
font-weight: bold;
}
.console-type.tx {
color: var(--acid);
}
.console-type.rx {
color: #00aaff;
}
.console-type.info {
color: #ccc;
}
.console-type.error {
color: #ff3333;
}
.console-type.success {
color: #00ffa0;
}
.console-target {
color: #aaa;
}
.console-content pre {
margin: 0;
white-space: pre-wrap;
font-family: inherit;
}
.zl-log-line {
display: flex;
gap: 8px;
margin-bottom: 4px;
}
/* Mobile Optimization */
@media (max-width: 900px) {
.zombieland-container.page-with-sidebar {
height: auto;
flex-direction: column;
}
.zombieland-container .zl-sidebar {
width: 100%;
max-height: none;
flex-shrink: 0;
border-radius: 8px;
}
.zl-stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.zl-toolbar {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.zl-main-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.zl-console-panel {
height: 350px;
flex: none;
}
.zl-agents-panel {
height: 350px;
flex: none;
}
.zl-console-input-row {
flex-wrap: wrap;
}
.zl-target-select,
.zl-cmd-input {
width: 100%;
box-sizing: border-box;
}
.zl-card-header {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.zl-card-info {
flex-direction: column;
gap: 2px;
}
}
/* Intentionally empty — see /web/css/zombieland.css */

File diff suppressed because it is too large Load Diff

View File

@@ -19,6 +19,7 @@ let activeActionId = null;
let panes = [null, null, null, null];
let split = 1;
let assignTargetPaneIndex = null;
let focusedPaneIndex = 0;
let searchQuery = '';
let currentTab = 'actions';
@@ -30,6 +31,63 @@ function isMobile() {
return window.matchMedia('(max-width: 860px)').matches;
}
const STATE_KEY = 'bjorn.actions.state';
function saveState() {
try {
sessionStorage.setItem(STATE_KEY, JSON.stringify({
split,
panes,
activeActionId,
focusedPaneIndex,
autoClear: [...autoClearPane],
}));
} catch { /* noop */ }
}
function restoreState() {
try {
const raw = sessionStorage.getItem(STATE_KEY);
if (!raw) return false;
const s = JSON.parse(raw);
if (typeof s.split === 'number' && s.split >= 1 && s.split <= 4) split = s.split;
if (Array.isArray(s.panes)) {
panes = s.panes.slice(0, 4).map(v => v || null);
while (panes.length < 4) panes.push(null);
}
if (s.activeActionId) activeActionId = s.activeActionId;
if (typeof s.focusedPaneIndex === 'number') focusedPaneIndex = s.focusedPaneIndex;
if (Array.isArray(s.autoClear)) {
for (let i = 0; i < 4; i++) autoClearPane[i] = !!s.autoClear[i];
}
return true;
} catch { return false; }
}
async function recoverLogs() {
const seen = new Set();
for (const actionId of panes) {
if (!actionId || seen.has(actionId)) continue;
seen.add(actionId);
const action = actions.find(a => a.id === actionId);
if (!action) continue;
try {
const scriptPath = action.path || action.module || action.id;
const res = await api.get('/get_script_output/' + encodeURIComponent(scriptPath), { timeout: 8000, retries: 0 });
if (res?.status === 'success' && res.data) {
const output = Array.isArray(res.data.output) ? res.data.output : [];
if (output.length) logsByAction.set(actionId, output);
if (res.data.is_running) {
action.status = 'running';
startOutputPolling(actionId);
}
}
} catch { /* ignore */ }
}
renderActionsList();
renderConsoles();
}
function q(sel, base = root) { return base?.querySelector(sel) || null; }
export async function mount(container) {
@@ -47,11 +105,25 @@ export async function mount(container) {
enforceMobileOnePane();
await loadActions();
const restored = restoreState();
if (restored) {
// Validate pane assignments
for (let i = 0; i < panes.length; i++) {
if (panes[i] && !actions.some(a => a.id === panes[i])) panes[i] = null;
}
// Update split segment buttons
$$('#splitSeg button', root).forEach(btn =>
btn.classList.toggle('active', Number(btn.dataset.split) === split)
);
}
enforceMobileOnePane();
renderActionsList();
renderConsoles();
if (restored) recoverLogs();
}
export function unmount() {
saveState();
if (typeof sidebarLayoutCleanup === 'function') {
sidebarLayoutCleanup();
sidebarLayoutCleanup = null;
@@ -74,6 +146,7 @@ export function unmount() {
panes = [null, null, null, null];
split = 1;
assignTargetPaneIndex = null;
focusedPaneIndex = 0;
searchQuery = '';
currentTab = 'actions';
logsByAction.clear();
@@ -102,8 +175,15 @@ function buildShell() {
]),
]);
const actionsSidebar = el('div', { id: 'tab-actions', class: 'sidebar-page' }, [
el('div', { id: 'actionsList', class: 'al-list' }),
const actionsSidebar = el('div', { id: 'tab-actions', class: 'sidebar-page al-split-layout' }, [
el('div', { id: 'actionsList', class: 'al-list al-builtins-scroll' }),
el('div', { class: 'al-custom-section' }, [
el('div', { class: 'al-section-divider' }, [
el('span', { class: 'al-section-title' }, ['Custom Scripts']),
el('button', { class: 'al-btn al-upload-btn', type: 'button' }, ['\u2B06 Upload']),
]),
el('div', { id: 'customActionsList', class: 'al-list al-custom-scroll' }),
]),
]);
const argsSidebar = el('div', { id: 'tab-arguments', class: 'sidebar-page', style: 'display:none' }, [
@@ -170,6 +250,15 @@ function bindStaticEvents() {
}
});
// Wire upload button (now static in buildShell)
const uploadBtnStatic = q('.al-upload-btn');
if (uploadBtnStatic) {
tracker.trackEventListener(uploadBtnStatic, 'click', () => {
const fi = q('#customScriptFileInput');
if (fi) fi.click();
});
}
const tabActions = q('#tabBtnActions');
const tabArgs = q('#tabBtnArgs');
const tabPkgs = q('#tabBtnPkgs');
@@ -198,6 +287,7 @@ function bindStaticEvents() {
split = Number(btn.dataset.split || '1');
$$('#splitSeg button', root).forEach((b) => b.classList.toggle('active', b === btn));
renderConsoles();
saveState();
});
});
@@ -313,9 +403,11 @@ function normalizeAction(raw) {
}
function renderActionsList() {
const container = q('#actionsList');
if (!container) return;
empty(container);
const builtinContainer = q('#actionsList');
const customContainer = q('#customActionsList');
if (!builtinContainer) return;
empty(builtinContainer);
if (customContainer) empty(customContainer);
const filtered = actions.filter((a) => {
if (!searchQuery) return true;
@@ -323,40 +415,28 @@ function renderActionsList() {
return searchQuery.split(/\s+/).every((term) => hay.includes(term));
});
if (!filtered.length) {
container.appendChild(el('div', { class: 'sub' }, [t('actions.noActions')]));
return;
}
const builtIn = filtered.filter((a) => a.category !== 'custom');
const custom = filtered.filter((a) => a.category === 'custom');
if (!builtIn.length && !custom.length) {
builtinContainer.appendChild(el('div', { class: 'sub' }, [t('actions.noActions')]));
return;
}
for (const a of builtIn) {
container.appendChild(buildActionRow(a));
builtinContainer.appendChild(buildActionRow(a));
}
if (!builtIn.length) {
builtinContainer.appendChild(el('div', { class: 'sub' }, [t('actions.noActions')]));
}
// Custom Scripts section
const sectionHeader = el('div', { class: 'al-section-divider' }, [
el('span', { class: 'al-section-title' }, ['Custom Scripts']),
el('button', { class: 'al-btn al-upload-btn', type: 'button' }, ['\u2B06 Upload']),
]);
const uploadBtn = sectionHeader.querySelector('.al-upload-btn');
if (uploadBtn) {
tracker.trackEventListener(uploadBtn, 'click', () => {
const fileInput = q('#customScriptFileInput');
if (fileInput) fileInput.click();
});
}
container.appendChild(sectionHeader);
if (!custom.length) {
container.appendChild(el('div', { class: 'sub', style: 'padding:6px 12px' }, ['No custom scripts uploaded.']));
}
for (const a of custom) {
container.appendChild(buildActionRow(a, true));
if (customContainer) {
if (!custom.length) {
customContainer.appendChild(el('div', { class: 'sub', style: 'padding:6px 12px;font-size:11px' }, ['No custom scripts uploaded.']));
}
for (const a of custom) {
customContainer.appendChild(buildActionRow(a, true));
}
}
}
@@ -481,6 +561,7 @@ function onActionSelected(actionId) {
if (target < 0) target = 0;
panes[target] = actionId;
renderConsoles();
saveState();
}
function renderArguments(action) {
@@ -492,6 +573,10 @@ function renderArguments(action) {
empty(builder);
empty(chips);
builder.appendChild(el('div', { class: 'args-pane-label' }, [
`Pane ${focusedPaneIndex + 1}: ${action.name}`
]));
const metaBits = [];
if (action.version) metaBits.push(`v${action.version}`);
if (action.author) metaBits.push(t('actions.byAuthor', { author: action.author }));
@@ -726,6 +811,19 @@ function renderConsoles() {
if (!dropped) return;
panes[i] = dropped;
renderConsoles();
saveState();
});
tracker.trackEventListener(pane, 'click', () => {
focusedPaneIndex = i;
$$('.pane', root).forEach((p, idx) => p.classList.toggle('paneFocused', idx === i));
const pAction = actionId ? actions.find(a => a.id === actionId) : null;
if (pAction) {
activeActionId = pAction.id;
renderArguments(pAction);
renderActionsList();
}
saveState();
});
renderPaneLog(i, actionId);
@@ -799,6 +897,13 @@ async function runActionInPane(index) {
return;
}
// Auto-focus pane and render its args before collecting
if (focusedPaneIndex !== index) {
focusedPaneIndex = index;
$$('.pane', root).forEach((p, idx) => p.classList.toggle('paneFocused', idx === index));
if (action) renderArguments(action);
}
if (!panes[index]) panes[index] = action.id;
if (autoClearPane[index]) clearActionLogs(action.id);
@@ -813,6 +918,7 @@ async function runActionInPane(index) {
const res = await api.post('/run_script', { script_name: action.module || action.id, args });
if (res.status !== 'success') throw new Error(res.message || 'Run failed');
startOutputPolling(action.id);
saveState();
} catch (err) {
action.status = 'error';
appendActionLog(action.id, `Error: ${err.message}`);
@@ -836,6 +942,7 @@ async function stopActionInPane(index) {
appendActionLog(action.id, t('actions.toast.stoppedByUser'));
renderActionsList();
renderConsoles();
saveState();
} catch (err) {
toast(`${t('actions.toast.failedToStop')}: ${err.message}`, 2600, 'error');
}

View File

@@ -84,7 +84,7 @@ function buildShell() {
el('button', { class: 'sched-tab', 'data-tab': 'triggers', onclick: () => switchTab('triggers') }, ['Triggers']),
]),
/* ── Queue tab content (existing kanban) ── */
el('div', { id: 'sched-tab-queue', class: 'sched-tab-content' }, [
el('div', { id: 'sched-tab-queue', class: 'sched-tab-content active' }, [
el('div', { class: 'controls' }, [
el('input', {
type: 'text', id: 'sched-search', placeholder: t('sched.filterPlaceholder'),
@@ -103,11 +103,11 @@ function buildShell() {
]),
]),
/* ── Schedules tab content ── */
el('div', { id: 'sched-tab-schedules', class: 'sched-tab-content', style: 'display:none' }, [
el('div', { id: 'sched-tab-schedules', class: 'sched-tab-content' }, [
buildSchedulesPanel(),
]),
/* ── Triggers tab content ── */
el('div', { id: 'sched-tab-triggers', class: 'sched-tab-content', style: 'display:none' }, [
el('div', { id: 'sched-tab-triggers', class: 'sched-tab-content' }, [
buildTriggersPanel(),
]),
/* history modal */
@@ -143,7 +143,7 @@ function switchTab(tab) {
/* show/hide tab content */
['queue', 'schedules', 'triggers'].forEach(id => {
const panel = $(`#sched-tab-${id}`);
if (panel) panel.style.display = id === tab ? '' : 'none';
if (panel) panel.classList.toggle('active', id === tab);
});
/* stop all pollers first */
@@ -170,7 +170,7 @@ function switchTab(tab) {
async function fetchScriptsList() {
try {
const data = await api.get('/list_scripts', { timeout: 12000 });
scriptsList = Array.isArray(data) ? data : (data?.scripts || data?.actions || []);
scriptsList = Array.isArray(data) ? data : (data?.data || data?.scripts || data?.actions || []);
} catch (e) {
scriptsList = [];
}
@@ -180,8 +180,13 @@ function populateScriptSelect(selectEl) {
empty(selectEl);
selectEl.appendChild(el('option', { value: '' }, ['-- Select script --']));
scriptsList.forEach(s => {
const name = typeof s === 'string' ? s : (s.name || s.action_name || '');
if (name) selectEl.appendChild(el('option', { value: name }, [name]));
if (typeof s === 'string') {
if (s) selectEl.appendChild(el('option', { value: s }, [s]));
} else {
const value = s.b_module || s.b_class || s.name || '';
const label = s.name || value;
if (value) selectEl.appendChild(el('option', { value }, [label]));
}
});
}
@@ -263,9 +268,9 @@ async function createSchedule() {
const type = $('#sched-sform-type')?.value || 'recurring';
const args = $('#sched-sform-args')?.value || '';
const payload = { script, type, args };
const payload = { script_name: script, schedule_type: type, args };
if (type === 'recurring') {
payload.interval = parseInt($('#sched-sform-interval')?.value || '300', 10);
payload.interval_seconds = parseInt($('#sched-sform-interval')?.value || '300', 10);
} else {
payload.run_at = $('#sched-sform-runat')?.value || '';
if (!payload.run_at) { toast('Please set a run time', 2600, 'error'); return; }
@@ -289,8 +294,8 @@ async function refreshScheduleList() {
if (sel && sel.children.length <= 1) populateScriptSelect(sel);
try {
const data = await api.post('/api/schedules/list', {});
const schedules = Array.isArray(data) ? data : (data?.schedules || []);
const resp = await api.post('/api/schedules/list', {});
const schedules = Array.isArray(resp) ? resp : (resp?.data || []);
renderScheduleList(container, schedules);
} catch (e) {
empty(container);
@@ -306,10 +311,14 @@ function renderScheduleList(container, schedules) {
}
schedules.forEach(s => {
const typeBadge = el('span', { class: `badge status-${s.type === 'recurring' ? 'running' : 'upcoming'}` }, [s.type || 'recurring']);
const timing = s.type === 'oneshot'
const sType = s.schedule_type || s.type || 'recurring';
const sScript = s.script_name || s.script || '';
const sInterval = s.interval_seconds || s.interval || 0;
const typeBadge = el('span', { class: `badge status-${sType === 'recurring' ? 'running' : 'upcoming'}` }, [sType]);
const timing = sType === 'oneshot'
? `Run at: ${fmt(s.run_at)}`
: `Every ${ms2str((s.interval || 0) * 1000)}`;
: `Every ${ms2str(sInterval * 1000)}`;
const nextRun = s.next_run_at ? `Next: ${fmt(s.next_run_at)}` : '';
const statusBadge = s.last_status
@@ -319,8 +328,8 @@ function renderScheduleList(container, schedules) {
const toggleBtn = el('label', { class: 'toggle-switch' }, [
el('input', {
type: 'checkbox',
checked: s.enabled !== false,
onchange: () => toggleSchedule(s.id, !s.enabled)
checked: s.enabled !== false && s.enabled !== 0,
onchange: () => toggleSchedule(s.id, !(s.enabled !== false && s.enabled !== 0))
}),
el('span', { class: 'toggle-slider' }),
]);
@@ -331,7 +340,7 @@ function renderScheduleList(container, schedules) {
container.appendChild(el('div', { class: 'card', 'data-schedule-id': s.id }, [
el('div', { class: 'cardHeader' }, [
el('div', { class: 'actionName' }, [
el('span', { class: 'chip', style: `--h:${hashHue(s.script || '')}` }, [s.script || '']),
el('span', { class: 'chip', style: `--h:${hashHue(sScript)}` }, [sScript]),
]),
typeBadge,
toggleBtn,
@@ -375,7 +384,10 @@ function editScheduleInline(s) {
empty(card);
const isRecurring = s.type === 'recurring';
const sType = s.schedule_type || s.type || 'recurring';
const sScript = s.script_name || s.script || '';
const sInterval = s.interval_seconds || s.interval || 300;
const isRecurring = sType === 'recurring';
card.appendChild(el('div', { class: 'schedules-form' }, [
el('h3', {}, ['Edit Schedule']),
@@ -384,7 +396,7 @@ function editScheduleInline(s) {
(() => {
const sel = el('select', { id: `sched-edit-script-${s.id}` });
populateScriptSelect(sel);
sel.value = s.script || '';
sel.value = sScript;
return sel;
})(),
]),
@@ -395,14 +407,14 @@ function editScheduleInline(s) {
el('option', { value: 'recurring' }, ['Recurring']),
el('option', { value: 'oneshot' }, ['One-shot']),
]);
sel.value = s.type || 'recurring';
sel.value = sType;
return sel;
})(),
]),
isRecurring
? el('div', { class: 'form-row' }, [
el('label', {}, ['Interval (seconds): ']),
el('input', { type: 'number', id: `sched-edit-interval-${s.id}`, value: String(s.interval || 300), min: '1', style: 'width:100px' }),
el('input', { type: 'number', id: `sched-edit-interval-${s.id}`, value: String(sInterval), min: '1', style: 'width:100px' }),
])
: el('div', { class: 'form-row' }, [
el('label', {}, ['Run at: ']),
@@ -416,12 +428,12 @@ function editScheduleInline(s) {
el('button', { class: 'btn', onclick: async () => {
const payload = {
id: s.id,
script: $(`#sched-edit-script-${s.id}`)?.value,
type: $(`#sched-edit-type-${s.id}`)?.value,
script_name: $(`#sched-edit-script-${s.id}`)?.value,
schedule_type: $(`#sched-edit-type-${s.id}`)?.value,
args: $(`#sched-edit-args-${s.id}`)?.value || '',
};
if (payload.type === 'recurring') {
payload.interval = parseInt($(`#sched-edit-interval-${s.id}`)?.value || '300', 10);
if (payload.schedule_type === 'recurring') {
payload.interval_seconds = parseInt($(`#sched-edit-interval-${s.id}`)?.value || '300', 10);
} else {
payload.run_at = $(`#sched-edit-runat-${s.id}`)?.value || '';
}
@@ -499,9 +511,10 @@ async function testTriggerConditions() {
const conditions = getConditions(condContainer);
try {
const data = await api.post('/api/triggers/test', { conditions });
resultEl.textContent = data?.result ? 'Result: TRUE' : 'Result: FALSE';
resultEl.style.color = data?.result ? 'var(--green, #0f0)' : 'var(--red, #f00)';
const resp = await api.post('/api/triggers/test', { conditions });
const result = resp?.data?.result;
resultEl.textContent = result ? 'Result: TRUE' : 'Result: FALSE';
resultEl.style.color = result ? 'var(--green, #0f0)' : 'var(--red, #f00)';
} catch (e) {
resultEl.textContent = 'Test failed: ' + e.message;
resultEl.style.color = 'var(--red, #f00)';
@@ -520,7 +533,7 @@ async function createTrigger() {
const args = $('#sched-tform-args')?.value || '';
try {
await api.post('/api/triggers/create', { script, name, conditions, cooldown, args });
await api.post('/api/triggers/create', { script_name: script, trigger_name: name, conditions, cooldown_seconds: cooldown, args });
toast('Trigger created');
$('#sched-tform-name').value = '';
refreshTriggerList();
@@ -538,8 +551,8 @@ async function refreshTriggerList() {
if (sel && sel.children.length <= 1) populateScriptSelect(sel);
try {
const data = await api.post('/api/triggers/list', {});
const triggers = Array.isArray(data) ? data : (data?.triggers || []);
const resp = await api.post('/api/triggers/list', {});
const triggers = Array.isArray(resp) ? resp : (resp?.data || []);
renderTriggerList(container, triggers);
} catch (e) {
empty(container);
@@ -555,13 +568,21 @@ function renderTriggerList(container, triggers) {
}
triggers.forEach(trig => {
const condCount = Array.isArray(trig.conditions) ? trig.conditions.length : 0;
const tScript = trig.script_name || trig.script || '';
const tName = trig.trigger_name || trig.name || '';
const tCooldown = trig.cooldown_seconds || trig.cooldown || 0;
const tEnabled = trig.enabled !== false && trig.enabled !== 0;
// conditions may be a JSON string from DB
let conds = trig.conditions;
if (typeof conds === 'string') { try { conds = JSON.parse(conds); } catch { conds = null; } }
const condCount = conds && typeof conds === 'object' ? (Array.isArray(conds.conditions) ? conds.conditions.length : 1) : 0;
const toggleBtn = el('label', { class: 'toggle-switch' }, [
el('input', {
type: 'checkbox',
checked: trig.enabled !== false,
onchange: () => toggleTrigger(trig.id, !trig.enabled)
checked: tEnabled,
onchange: () => toggleTrigger(trig.id, !tEnabled)
}),
el('span', { class: 'toggle-slider' }),
]);
@@ -571,15 +592,15 @@ function renderTriggerList(container, triggers) {
container.appendChild(el('div', { class: 'card' }, [
el('div', { class: 'cardHeader' }, [
el('div', { class: 'actionName' }, [
el('strong', {}, [trig.name || '']),
el('strong', {}, [tName]),
el('span', { style: 'margin-left:8px' }, [' \u2192 ']),
el('span', { class: 'chip', style: `--h:${hashHue(trig.script || '')}` }, [trig.script || '']),
el('span', { class: 'chip', style: `--h:${hashHue(tScript)}` }, [tScript]),
]),
toggleBtn,
]),
el('div', { class: 'meta' }, [
el('span', {}, [`${condCount} condition${condCount !== 1 ? 's' : ''}`]),
el('span', {}, [`Cooldown: ${ms2str(( trig.cooldown || 0) * 1000)}`]),
el('span', {}, [`Cooldown: ${ms2str(tCooldown * 1000)}`]),
el('span', {}, [`Fired: ${trig.fire_count || 0}`]),
trig.last_fired_at ? el('span', {}, [`Last: ${fmt(trig.last_fired_at)}`]) : null,
].filter(Boolean)),
@@ -1149,14 +1170,19 @@ function showError(msg) {
}
/* ── icon resolution ── */
const ICON_DEFAULT = '/actions/actions_icons/default.png';
const ICON_PENDING = '__pending__';
function resolveIconSync(name) {
if (iconCache.has(name)) return iconCache.get(name);
const cached = iconCache.get(name);
if (cached === ICON_PENDING) return ICON_DEFAULT;
if (cached) return cached;
iconCache.set(name, ICON_PENDING);
resolveIconAsync(name);
return '/actions/actions_icons/default.png';
return ICON_DEFAULT;
}
async function resolveIconAsync(name) {
if (iconCache.has(name)) return;
const candidates = [
`/actions/actions_icons/${name}.png`,
`/resources/images/status/${name}/${name}.bmp`,
@@ -1167,7 +1193,7 @@ async function resolveIconAsync(name) {
if (r.ok) { iconCache.set(name, url); updateIconsInDOM(name, url); return; }
} catch { /* next */ }
}
iconCache.set(name, '/actions/actions_icons/default.png');
iconCache.set(name, ICON_DEFAULT);
}
function updateIconsInDOM(name, url) {