From b541ec1f61d5e082e885661a4f185d535879f075 Mon Sep 17 00:00:00 2001 From: infinition Date: Thu, 19 Mar 2026 16:59:55 +0100 Subject: [PATCH] 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. --- actions/ftp_bruteforce.py | 2 +- actions/smb_bruteforce.py | 2 +- actions/sql_bruteforce.py | 2 +- actions/telnet_bruteforce.py | 2 +- web/css/pages.css | 2 + web/css/pages/actions.css | 688 ++++++++++++++++++++++++++ web/css/pages/compat.css | 4 +- web/css/pages/files.css | 509 +------------------- web/css/pages/plugins.css | 277 +++++++++++ web/css/pages/scheduler.css | 294 ++++++++++++ web/css/pages/zombieland.css | 496 +------------------ web/css/zombieland.css | 905 +++++++++++++++++++++++++++-------- web/js/pages/actions.js | 173 +++++-- web/js/pages/scheduler.js | 108 +++-- 14 files changed, 2178 insertions(+), 1286 deletions(-) create mode 100644 web/css/pages/actions.css create mode 100644 web/css/pages/plugins.css diff --git a/actions/ftp_bruteforce.py b/actions/ftp_bruteforce.py index 44f8e72..b495043 100644 --- a/actions/ftp_bruteforce.py +++ b/actions/ftp_bruteforce.py @@ -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 diff --git a/actions/smb_bruteforce.py b/actions/smb_bruteforce.py index 90fe4b5..de2b4f0 100644 --- a/actions/smb_bruteforce.py +++ b/actions/smb_bruteforce.py @@ -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 diff --git a/actions/sql_bruteforce.py b/actions/sql_bruteforce.py index fdfa0aa..ddafaa6 100644 --- a/actions/sql_bruteforce.py +++ b/actions/sql_bruteforce.py @@ -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 diff --git a/actions/telnet_bruteforce.py b/actions/telnet_bruteforce.py index 1e5d17f..24e5312 100644 --- a/actions/telnet_bruteforce.py +++ b/actions/telnet_bruteforce.py @@ -1,4 +1,4 @@ -“””telnet_bruteforce.py - Threaded Telnet credential bruteforcer.””” +"""telnet_bruteforce.py - Threaded Telnet credential bruteforcer.""" import os import telnetlib diff --git a/web/css/pages.css b/web/css/pages.css index c517964..342468f 100644 --- a/web/css/pages.css +++ b/web/css/pages.css @@ -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"); diff --git a/web/css/pages/actions.css b/web/css/pages/actions.css new file mode 100644 index 0000000..a4cb76f --- /dev/null +++ b/web/css/pages/actions.css @@ -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); +} diff --git a/web/css/pages/compat.css b/web/css/pages/compat.css index 8f82654..ebf8e9c 100644 --- a/web/css/pages/compat.css +++ b/web/css/pages/compat.css @@ -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; diff --git a/web/css/pages/files.css b/web/css/pages/files.css index 6150298..50e9450 100644 --- a/web/css/pages/files.css +++ b/web/css/pages/files.css @@ -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) diff --git a/web/css/pages/plugins.css b/web/css/pages/plugins.css new file mode 100644 index 0000000..570892f --- /dev/null +++ b/web/css/pages/plugins.css @@ -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; } diff --git a/web/css/pages/scheduler.css b/web/css/pages/scheduler.css index 8b29226..d6cbc9f 100644 --- a/web/css/pages/scheduler.css +++ b/web/css/pages/scheduler.css @@ -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); } + diff --git a/web/css/pages/zombieland.css b/web/css/pages/zombieland.css index be2686f..5e11b15 100644 --- a/web/css/pages/zombieland.css +++ b/web/css/pages/zombieland.css @@ -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; - } -} \ No newline at end of file +/* Intentionally empty — see /web/css/zombieland.css */ diff --git a/web/css/zombieland.css b/web/css/zombieland.css index be2686f..173fc16 100644 --- a/web/css/zombieland.css +++ b/web/css/zombieland.css @@ -1,16 +1,23 @@ /* ========================================================================== - ZOMBIELAND (C2 Module) CSS + ZOMBIELAND — C2 Dashboard + Professional command-and-control dashboard layout. + Uses Bjorn design tokens exclusively (no hardcoded colors). ========================================================================== */ -/* Main layout constraints */ +/* ---------------------------------------------------------------- + 0) Container & top-level layout + ---------------------------------------------------------------- */ .zombieland-container.page-with-sidebar { height: calc(100vh - var(--h-topbar, 56px) - var(--h-bottombar, 56px) - 24px); display: flex; min-height: 0; } +/* ---------------------------------------------------------------- + 1) Sidebar + ---------------------------------------------------------------- */ .zl-sidebar.page-sidebar { - width: 260px; + width: 270px; flex-shrink: 0; display: flex; flex-direction: column; @@ -20,6 +27,100 @@ overflow-y: auto; } +.zl-sidebar .sidehead { + padding: 12px 14px; + border-bottom: 1px solid var(--c-border); +} + +.zl-sidebar .sidecontent { + padding: 12px 14px; + display: flex; + flex-direction: column; + gap: 14px; + flex: 1; + overflow-y: auto; +} + +/* ---------------------------------------------------------------- + 2) Stat cards (sidebar) + ---------------------------------------------------------------- */ +.zl-stats-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} + +.stat-item { + background: rgba(0, 0, 0, 0.25); + border: 1px solid var(--c-border); + padding: 10px 12px; + border-radius: 10px; + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + text-align: center; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.stat-item:hover { + border-color: var(--c-border-strong); + box-shadow: 0 0 12px var(--acid-0f); +} + +/* Make the last stat card (C2 Port) span both columns */ +.stat-item:last-child { + grid-column: 1 / -1; + flex-direction: row; + justify-content: space-between; +} + +.stat-item .stat-value { + font-weight: 800; + color: var(--acid); + font-size: 1.45rem; + font-family: 'Fira Code', 'JetBrains Mono', monospace; + line-height: 1.1; + letter-spacing: -0.02em; +} + +.stat-item .stat-value.stat-online { + color: var(--ok); + text-shadow: 0 0 8px rgba(44, 255, 126, 0.4); +} + +.stat-item .stat-value.stat-offline { + color: var(--danger); + text-shadow: 0 0 8px rgba(255, 59, 59, 0.3); +} + +.stat-item .stat-label { + font-size: 0.68rem; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.06em; + font-weight: 600; +} + +/* ---------------------------------------------------------------- + 3) Sidebar toolbar (buttons) + ---------------------------------------------------------------- */ +.zl-toolbar { + display: flex; + flex-direction: column; + gap: 6px; +} + +.zl-toolbar .btn { + width: 100%; + justify-content: flex-start; + font-size: 0.8rem; + gap: 6px; +} + +/* ---------------------------------------------------------------- + 4) Main area + ---------------------------------------------------------------- */ .zl-main.page-main { flex: 1; min-width: 0; @@ -28,75 +129,20 @@ 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 */ +/* ---------------------------------------------------------------- + 5) Main grid — Console (2/3) + Agents (1/3) + ---------------------------------------------------------------- */ .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; } +/* ---------------------------------------------------------------- + 6) Shared panel styles + ---------------------------------------------------------------- */ .zl-console-panel, .zl-agents-panel, .zl-logs-panel { @@ -106,111 +152,261 @@ display: flex; flex-direction: column; overflow: hidden; -} - -/* Log Panel underneath */ -.zl-logs-panel { - height: 150px; - flex-shrink: 0; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); } .zl-panel-header { display: flex; justify-content: space-between; align-items: center; - padding: 8px 12px; - background: rgba(0, 0, 0, 0.2); + padding: 8px 14px; + background: linear-gradient(180deg, rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0.15)); border-bottom: 1px solid var(--c-border); flex-shrink: 0; + gap: 8px; + min-height: 40px; } .zl-panel-title { - font-weight: bold; - font-size: 0.9rem; + font-weight: 700; + font-size: 0.85rem; color: var(--acid); + display: flex; + align-items: center; + gap: 6px; + white-space: nowrap; } +.zl-panel-title i, +.zl-panel-title svg { + width: 14px; + height: 14px; + opacity: 0.7; +} + +/* ---------------------------------------------------------------- + 7) Console panel + ---------------------------------------------------------------- */ + +/* Quick command bar */ .zl-quickbar { display: flex; gap: 4px; + flex-wrap: wrap; } .quick-cmd { - background: transparent; + background: rgba(0, 0, 0, 0.3); border: 1px solid var(--c-border-strong); color: var(--muted); font-size: 0.7rem; - padding: 2px 6px; + font-family: 'Fira Code', 'JetBrains Mono', monospace; + padding: 3px 8px; border-radius: 4px; cursor: pointer; - transition: 0.2s; + transition: all 0.15s ease; + white-space: nowrap; } .quick-cmd:hover { color: var(--acid); border-color: var(--acid); + background: rgba(0, 255, 154, 0.06); + box-shadow: 0 0 6px var(--acid-0f); } -.zl-console-output, -.zl-logs-output { +/* Console output */ +.zl-console-output { flex: 1; overflow-y: auto; - padding: 10px; - font-family: 'Fira Code', monospace; - font-size: 0.8rem; - background: #020406; + padding: 12px 14px; + font-family: 'Fira Code', 'JetBrains Mono', monospace; + font-size: 0.78rem; + line-height: 1.5; + background: var(--bg); min-height: 0; } -/* Controls */ +/* Console input bar */ .zl-console-input-row { display: flex; gap: 8px; - padding: 8px; + padding: 10px 12px; border-top: 1px solid var(--c-border); background: var(--c-panel-2); flex-shrink: 0; + align-items: center; } -.zl-target-select, -.zl-cmd-input, -.zl-search-input { - background: #000; - color: #fff; +.zl-target-select { + background: var(--bg); + color: var(--ink); border: 1px solid var(--c-border-strong); - padding: 6px 10px; + padding: 7px 10px; border-radius: 6px; + font-size: 0.78rem; + font-family: inherit; + min-width: 120px; + cursor: pointer; + transition: border-color 0.15s; +} + +.zl-target-select:focus { + border-color: var(--acid); + outline: none; + box-shadow: 0 0 0 2px var(--acid-22); } .zl-cmd-input { flex: 1; + background: var(--bg); + color: var(--ink); + border: 1px solid var(--c-border-strong); + padding: 7px 12px; + border-radius: 6px; + font-family: 'Fira Code', 'JetBrains Mono', monospace; + font-size: 0.8rem; + transition: border-color 0.15s; } +.zl-cmd-input::placeholder { + color: var(--muted); + opacity: 0.5; +} + +.zl-cmd-input:focus { + border-color: var(--acid); + outline: none; + box-shadow: 0 0 0 2px var(--acid-22); +} + +/* Console output items */ +.console-line { + margin-bottom: 2px; + display: flex; + gap: 8px; + padding: 2px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.02); +} + +.console-line:last-child { + border-bottom: none; +} + +.console-time { + color: var(--muted); + opacity: 0.6; + font-size: 0.72rem; + white-space: nowrap; + flex-shrink: 0; +} + +.console-type { + font-weight: 700; + font-size: 0.72rem; + min-width: 32px; + text-align: center; + flex-shrink: 0; +} + +.console-type.tx { + color: var(--acid); +} + +.console-type.rx { + color: var(--acid-2); +} + +.console-type.info { + color: var(--ink); + opacity: 0.6; +} + +.console-type.error { + color: var(--danger); +} + +.console-type.success { + color: var(--ok); +} + +.console-target { + color: var(--muted); + opacity: 0.7; + font-size: 0.72rem; + flex-shrink: 0; +} + +.console-content { + flex: 1; + min-width: 0; +} + +.console-content pre { + margin: 0; + white-space: pre-wrap; + word-break: break-all; + font-family: inherit; + font-size: inherit; + color: var(--ink); +} + +/* ---------------------------------------------------------------- + 8) Agents panel + ---------------------------------------------------------------- */ + +/* Agent panel header toolbar */ .zl-toolbar-left { display: flex; position: relative; flex: 1; - max-width: 200px; - margin-left: 10px; + max-width: 180px; + margin-left: 8px; } .zl-search-input { width: 100%; + background: var(--bg); + color: var(--ink); + border: 1px solid var(--c-border-strong); border-radius: 6px; - padding: 4px 20px 4px 8px; - font-size: 0.8rem; + padding: 5px 24px 5px 8px; + font-size: 0.75rem; + transition: border-color 0.15s; +} + +.zl-search-input::placeholder { + color: var(--muted); + opacity: 0.5; +} + +.zl-search-input:focus { + border-color: var(--acid); + outline: none; + box-shadow: 0 0 0 2px var(--acid-22); } .zl-search-clear { position: absolute; - right: 5px; - top: 5px; + right: 4px; + top: 50%; + transform: translateY(-50%); color: var(--muted); background: none; border: none; cursor: pointer; + padding: 2px; + line-height: 1; + opacity: 0.6; + transition: opacity 0.15s; } +.zl-search-clear:hover { + opacity: 1; + color: var(--ink); +} + +/* Agent list container */ .zl-agents-list { flex: 1; overflow-y: auto; @@ -220,75 +416,188 @@ gap: 8px; } -/* Agent Card Styles */ +/* Empty state */ +.zl-empty { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: var(--muted); + font-size: 0.82rem; + opacity: 0.6; + text-align: center; + padding: 20px; +} + +/* ---------------------------------------------------------------- + 9) Agent cards + ---------------------------------------------------------------- */ .zl-agent-card { - background: rgba(0, 0, 0, 0.3); + background: linear-gradient(180deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.2)); border: 1px solid var(--c-border); - border-radius: 8px; - padding: 8px 12px; + border-radius: 10px; + padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; - transition: 0.2s ease-out; + transition: all 0.2s ease-out; + position: relative; } -.zl-agent-card.selected { - border-color: var(--acid); - background: rgba(0, 255, 160, 0.05); +.zl-agent-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + border-radius: 10px 10px 0 0; + background: transparent; + transition: background 0.2s; } .zl-agent-card:hover { border-color: var(--c-border-hi); + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3); } +.zl-agent-card.selected { + border-color: var(--acid); + background: linear-gradient(180deg, rgba(0, 255, 154, 0.04), rgba(0, 255, 154, 0.01)); +} + +.zl-agent-card.selected::before { + background: var(--acid); +} + +/* Pulse animation on telemetry */ +@keyframes zl-card-pulse { + 0% { box-shadow: 0 0 0 0 rgba(0, 255, 154, 0.3); } + 70% { box-shadow: 0 0 0 6px rgba(0, 255, 154, 0); } + 100% { box-shadow: 0 0 0 0 rgba(0, 255, 154, 0); } +} + +.zl-agent-card.pulse { + animation: zl-card-pulse 0.6s ease-out; +} + +/* Stale / offline visual indicators */ +.zl-agent-card.agent-stale-yellow { + border-left: 3px solid var(--warning); +} + +.zl-agent-card.agent-stale-orange { + border-left: 3px solid #ff8800; +} + +.zl-agent-card.agent-stale-red { + border-left: 3px solid var(--danger); + opacity: 0.7; +} + +/* Card header */ .zl-card-header { display: flex; - justify-content: space-between; align-items: center; + gap: 8px; +} + +.zl-card-header .agent-checkbox { + flex-shrink: 0; + accent-color: var(--acid); + cursor: pointer; + width: 14px; + height: 14px; } .zl-card-identity { + flex: 1; + min-width: 0; display: flex; flex-direction: column; - gap: 2px; - line-height: 1; + gap: 1px; } .zl-card-hostname { - font-weight: bold; - color: #fff; - font-size: 0.9rem; + font-weight: 700; + color: var(--ink); + font-size: 0.85rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .zl-card-id { - font-size: 0.7rem; + font-size: 0.65rem; + color: var(--muted); + opacity: 0.7; + font-family: 'Fira Code', monospace; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Status pill */ +.zl-pill { + padding: 2px 10px; + border-radius: 12px; + font-size: 0.65rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + flex-shrink: 0; + background: rgba(255, 255, 255, 0.05); 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); + color: var(--ok); + background: rgba(44, 255, 126, 0.1); + box-shadow: 0 0 6px rgba(44, 255, 126, 0.15); } .zl-pill.idle { - color: #ffcc00; - background: rgba(255, 204, 0, 0.1); + color: var(--warning); + background: rgba(255, 209, 102, 0.1); } .zl-pill.offline { - color: #ff3333; - background: rgba(255, 51, 51, 0.1); + color: var(--danger); + background: rgba(255, 59, 59, 0.08); } -/* ECG Animation */ +/* Info row (OS, IP, CPU/RAM) */ +.zl-card-info { + display: flex; + flex-wrap: wrap; + gap: 4px 10px; + font-size: 0.72rem; + color: var(--ink); + opacity: 0.8; + background: rgba(0, 0, 0, 0.2); + padding: 5px 8px; + border-radius: 6px; +} + +.zl-info-row { + display: flex; + gap: 4px; + white-space: nowrap; +} + +.zl-info-label { + color: var(--muted); + opacity: 0.6; + font-size: 0.68rem; +} + +.zl-info-value { + color: var(--ink); + font-family: 'Fira Code', monospace; + font-size: 0.7rem; +} + +/* ECG heartbeat row */ .zl-ecg-row { display: flex; align-items: center; @@ -297,13 +606,13 @@ .ecg { width: 100%; - height: 24px; - max-width: 140px; + height: 22px; + max-width: 130px; position: relative; overflow: hidden; - background: rgba(0, 0, 0, 0.5); + background: rgba(0, 0, 0, 0.4); border-radius: 6px; - border: 1px solid #111; + border: 1px solid var(--c-border-muted); } .ecg-wrapper { @@ -325,22 +634,23 @@ } .ecg.green path { - stroke: #00ffa0; - filter: drop-shadow(0 0 2px #00ffa0); + stroke: var(--ok); + filter: drop-shadow(0 0 3px rgba(44, 255, 126, 0.5)); } .ecg.yellow path { - stroke: #ffcc00; - filter: drop-shadow(0 0 2px #ffcc00); + stroke: var(--warning); + filter: drop-shadow(0 0 3px rgba(255, 209, 102, 0.4)); } .ecg.orange path { stroke: #ff8800; - filter: drop-shadow(0 0 2px #ff8800); + filter: drop-shadow(0 0 3px rgba(255, 136, 0, 0.4)); } .ecg.red path { - stroke: #ff3333; + stroke: var(--danger); + filter: drop-shadow(0 0 2px rgba(255, 59, 59, 0.3)); } .ecg.flat .ecg-wrapper { @@ -348,93 +658,231 @@ } @keyframes ecg-slide { - 0% { - transform: translateX(0); - } - - 100% { - transform: translateX(-33.33%); - } + 0% { transform: translateX(0); } + 100% { transform: translateX(-33.33%); } } .zl-ecg-counter { - font-size: 0.7rem; + font-size: 0.68rem; 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; + font-family: 'Fira Code', monospace; + opacity: 0.7; + min-width: 28px; + text-align: right; } +/* Card action buttons */ .zl-card-actions { display: flex; justify-content: flex-end; gap: 4px; - margin-top: 4px; + margin-top: 2px; + padding-top: 4px; + border-top: 1px solid var(--c-border-muted); } -/* Console output items */ -.console-line { - margin-bottom: 4px; +.zl-card-actions .btn { + font-size: 0.72rem; +} + +.zl-card-actions .btn-icon { + width: 26px; + height: 26px; + padding: 0; display: flex; - gap: 8px; + align-items: center; + justify-content: center; } -.console-time { - color: var(--muted); +/* ---------------------------------------------------------------- + 10) Logs panel (bottom) + ---------------------------------------------------------------- */ +.zl-logs-panel { + height: 160px; + flex-shrink: 0; } -.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-logs-output { + flex: 1; + overflow-y: auto; + padding: 8px 14px; + font-family: 'Fira Code', 'JetBrains Mono', monospace; + font-size: 0.72rem; + line-height: 1.5; + background: var(--bg); + min-height: 0; } .zl-log-line { display: flex; gap: 8px; - margin-bottom: 4px; + margin-bottom: 1px; + padding: 1px 0; +} + +.zl-log-text { + color: var(--ink); + opacity: 0.8; + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ---------------------------------------------------------------- + 11) Modals + ---------------------------------------------------------------- */ +.modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.modal-content { + background: var(--grad-card); + border: 1px solid var(--c-border-strong); + padding: 24px; + border-radius: 14px; + width: 100%; + max-width: 440px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); +} + +.modal-title { + color: var(--acid); + font-size: 1rem; + margin: 0 0 16px 0; +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 16px; + padding-top: 12px; + border-top: 1px solid var(--c-border); +} + +.form-grid { + display: grid; + grid-template-columns: auto 1fr; + gap: 8px 12px; + align-items: center; +} + +.form-grid label { + font-size: 0.8rem; + color: var(--muted); +} + +.grid-col-2 { + display: flex; + gap: 8px; +} + +.deploy-options { + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid var(--c-border); +} + +.deploy-options h4 { + color: var(--ink); + font-size: 0.85rem; + margin: 0 0 8px 0; +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.82rem; + color: var(--ink); + cursor: pointer; +} + +.checkbox-label input[type="checkbox"] { + accent-color: var(--acid); +} + +.file-browser-nav { + display: flex; + gap: 8px; + margin-bottom: 12px; +} + +.file-browser-nav .input { + flex: 1; +} + +.file-list { + min-height: 200px; + max-height: 400px; + overflow-y: auto; + background: var(--bg); + border: 1px solid var(--c-border); + border-radius: 8px; + padding: 10px; + font-family: 'Fira Code', monospace; + font-size: 0.78rem; + color: var(--ink); +} + +/* ---------------------------------------------------------------- + 12) Utility classes + ---------------------------------------------------------------- */ +.hidden { + display: none !important; +} + +.flex-grow { + flex: 1; +} + +/* ---------------------------------------------------------------- + 13) Scrollbar styling (scoped to zombieland) + ---------------------------------------------------------------- */ +.zombieland-container ::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +.zombieland-container ::-webkit-scrollbar-track { + background: transparent; +} + +.zombieland-container ::-webkit-scrollbar-thumb { + background: var(--sb-thumb); + border-radius: 3px; +} + +.zombieland-container ::-webkit-scrollbar-thumb:hover { + background: var(--sb-thumb-hi); +} + +/* ---------------------------------------------------------------- + 14) Responsive / mobile + ---------------------------------------------------------------- */ +@media (max-width: 1200px) { + .zl-quickbar { + display: none; + } } -/* Mobile Optimization */ @media (max-width: 900px) { .zombieland-container.page-with-sidebar { height: auto; + min-height: calc(100vh - var(--h-topbar, 56px) - var(--h-bottombar, 56px) - 24px); flex-direction: column; } @@ -442,11 +890,15 @@ width: 100%; max-height: none; flex-shrink: 0; - border-radius: 8px; + border-radius: 10px; } .zl-stats-grid { - grid-template-columns: repeat(2, 1fr); + grid-template-columns: repeat(3, 1fr); + } + + .stat-item:last-child { + grid-column: auto; } .zl-toolbar { @@ -461,33 +913,72 @@ } .zl-console-panel { - height: 350px; + height: 360px; flex: none; } .zl-agents-panel { - height: 350px; + height: 400px; flex: none; } + .zl-logs-panel { + height: 180px; + } + .zl-console-input-row { flex-wrap: wrap; } - .zl-target-select, + .zl-target-select { + width: 100%; + min-width: 0; + } + .zl-cmd-input { width: 100%; - box-sizing: border-box; } .zl-card-header { - flex-direction: column; - align-items: flex-start; - gap: 4px; + flex-wrap: wrap; } .zl-card-info { flex-direction: column; gap: 2px; } -} \ No newline at end of file + + .zl-quickbar { + display: flex; + } + + .zl-toolbar-left { + max-width: none; + margin-left: 0; + margin-top: 6px; + } +} + +@media (max-width: 600px) { + .zl-stats-grid { + grid-template-columns: repeat(2, 1fr); + } + + .stat-item .stat-value { + font-size: 1.15rem; + } + + .zl-console-panel { + height: 300px; + } + + .zl-agents-panel { + height: 350px; + } + + .zl-panel-header { + flex-wrap: wrap; + gap: 6px; + padding: 6px 10px; + } +} diff --git a/web/js/pages/actions.js b/web/js/pages/actions.js index 964dc48..7291fd8 100644 --- a/web/js/pages/actions.js +++ b/web/js/pages/actions.js @@ -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'); } diff --git a/web/js/pages/scheduler.js b/web/js/pages/scheduler.js index ec49d4a..b3f0056 100644 --- a/web/js/pages/scheduler.js +++ b/web/js/pages/scheduler.js @@ -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) {