mirror of
https://github.com/infinition/Bjorn.git
synced 2026-03-19 18:20:24 +00:00
feat: enhance scheduler functionality and UI improvements
- Updated scheduler.js to improve tab switching and content display. - Refactored script fetching logic to handle new data structure. - Enhanced schedule creation and editing with updated payload structure. - Improved trigger testing and creation with consistent naming conventions. - Added new CSS styles for actions and plugins pages to enhance UI/UX. - Introduced responsive design adjustments for better mobile experience.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
“””telnet_bruteforce.py - Threaded Telnet credential bruteforcer.”””
|
||||
"""telnet_bruteforce.py - Threaded Telnet credential bruteforcer."""
|
||||
|
||||
import os
|
||||
import telnetlib
|
||||
|
||||
@@ -13,8 +13,10 @@
|
||||
@import url("./pages/files.css");
|
||||
@import url("./pages/compat.css");
|
||||
@import url("./pages/backup.css");
|
||||
@import url("./pages/actions.css");
|
||||
@import url("./pages/actions-studio.css");
|
||||
@import url("./pages/sentinel.css");
|
||||
@import url("./pages/bifrost.css");
|
||||
@import url("./pages/loki.css");
|
||||
@import url("./pages/llm.css");
|
||||
@import url("./pages/plugins.css");
|
||||
|
||||
688
web/css/pages/actions.css
Normal file
688
web/css/pages/actions.css
Normal file
@@ -0,0 +1,688 @@
|
||||
/* ==========================================================================
|
||||
ACTIONS LAUNCHER (.actions-container)
|
||||
Viewport-locked layout: sidebar + multi-console, no page-level scroll.
|
||||
========================================================================== */
|
||||
|
||||
/* ── Root container ── */
|
||||
.actions-container.page-with-sidebar {
|
||||
--page-sidebar-w: 280px;
|
||||
height: calc(100vh - var(--h-topbar, 56px) - var(--h-bottombar, 56px) - 24px);
|
||||
max-height: calc(100vh - var(--h-topbar, 56px) - var(--h-bottombar, 56px) - 24px);
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Sidebar (locked to viewport, internally scrollable) ── */
|
||||
.actions-container .al-sidebar.page-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.actions-container .sideheader {
|
||||
padding: 8px 10px 6px;
|
||||
border-bottom: 1px dashed var(--c-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.actions-container .al-side-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.actions-container .al-side-meta .sidetitle {
|
||||
color: var(--acid);
|
||||
font-weight: 800;
|
||||
letter-spacing: .05em;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ── Sidebar tabs ── */
|
||||
.actions-container .tabs-container {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: nowrap;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
min-height: 0;
|
||||
position: static;
|
||||
}
|
||||
|
||||
.actions-container .tab-btn {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
padding: 5px 10px;
|
||||
border-radius: 8px;
|
||||
background: var(--c-pill-bg, color-mix(in oklab, var(--c-panel-2) 70%, transparent));
|
||||
border: 1px solid var(--c-border);
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
transition: background .15s, color .15s;
|
||||
}
|
||||
|
||||
.actions-container .tab-btn:hover {
|
||||
color: var(--ink);
|
||||
background: color-mix(in oklab, var(--c-panel-2) 90%, transparent);
|
||||
}
|
||||
|
||||
.actions-container .tab-btn.active {
|
||||
color: var(--acid);
|
||||
background: var(--grad-chip-selected, color-mix(in oklab, var(--acid) 14%, var(--c-panel-2)));
|
||||
border-color: color-mix(in oklab, var(--acid) 45%, transparent);
|
||||
}
|
||||
|
||||
/* ── Search ── */
|
||||
.actions-container .al-search {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 6px 10px 8px;
|
||||
}
|
||||
|
||||
.actions-container .al-input {
|
||||
flex: 1;
|
||||
background: var(--c-panel);
|
||||
border: 1px solid var(--c-border-strong);
|
||||
color: var(--ink);
|
||||
padding: 6px 10px;
|
||||
border-radius: var(--control-r, 8px);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.actions-container .al-input:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px color-mix(in oklab, var(--acid) 55%, transparent) inset;
|
||||
}
|
||||
|
||||
/* ── Sidebar content (scrollable area) ── */
|
||||
.actions-container .sidecontent {
|
||||
padding: 6px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.actions-container .sidebar-page {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ── Actions list ── */
|
||||
.actions-container .al-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* ── Action row (compact) ── */
|
||||
.actions-container .al-row {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 36px 1fr auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
background: var(--c-panel-2);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
transition: background .12s, border-color .12s;
|
||||
}
|
||||
|
||||
.actions-container .al-row:hover {
|
||||
background: color-mix(in oklab, var(--c-panel-2) 80%, var(--acid) 6%);
|
||||
border-color: color-mix(in oklab, var(--acid) 18%, transparent);
|
||||
}
|
||||
|
||||
.actions-container .al-row.selected {
|
||||
border-color: color-mix(in oklab, var(--acid) 40%, transparent);
|
||||
background: color-mix(in oklab, var(--c-panel-2) 70%, var(--acid) 8%);
|
||||
}
|
||||
|
||||
.actions-container .al-row .ic {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 6px;
|
||||
background: var(--c-panel);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.actions-container .ic-img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.actions-container .al-row > div:nth-child(2) {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.actions-container .name {
|
||||
font-weight: 700;
|
||||
color: var(--acid-2, var(--acid));
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.actions-container .desc {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── Status chip (inside row) ── */
|
||||
.actions-container .al-row .chip {
|
||||
position: static;
|
||||
transform: none;
|
||||
padding: 2px 6px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--c-border);
|
||||
background: var(--c-chip-bg, color-mix(in oklab, var(--c-panel) 80%, transparent));
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.actions-container .chip.ok {
|
||||
color: var(--ok, #22c55e);
|
||||
border-color: color-mix(in oklab, var(--ok, #22c55e) 50%, transparent);
|
||||
}
|
||||
|
||||
.actions-container .chip.err {
|
||||
color: var(--danger, #ef4444);
|
||||
border-color: color-mix(in oklab, var(--danger, #ef4444) 50%, transparent);
|
||||
}
|
||||
|
||||
.actions-container .chip.run {
|
||||
color: var(--acid);
|
||||
border-color: color-mix(in oklab, var(--acid) 50%, transparent);
|
||||
}
|
||||
|
||||
/* ── Format badge for custom scripts ── */
|
||||
.actions-container .al-row .format-badge {
|
||||
font-size: 9px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .04em;
|
||||
background: color-mix(in oklab, var(--acid) 12%, transparent);
|
||||
color: var(--acid);
|
||||
border-color: color-mix(in oklab, var(--acid) 30%, transparent);
|
||||
}
|
||||
|
||||
/* ── Section divider (Custom Scripts header) ── */
|
||||
.actions-container .al-section-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px 4px 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.actions-container .al-section-title {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .06em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.actions-container .al-upload-btn {
|
||||
font-size: 10px;
|
||||
padding: 3px 8px;
|
||||
}
|
||||
|
||||
.actions-container .al-delete-btn {
|
||||
font-size: 12px;
|
||||
padding: 2px 6px;
|
||||
opacity: .5;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.actions-container .al-delete-btn:hover {
|
||||
opacity: 1;
|
||||
color: var(--danger, #ef4444);
|
||||
}
|
||||
|
||||
/* ── Buttons (shared) ── */
|
||||
.actions-container .al-btn {
|
||||
background: var(--c-btn, var(--c-panel));
|
||||
color: var(--ink);
|
||||
border: 1px solid var(--c-border-strong);
|
||||
border-radius: var(--control-r, 8px);
|
||||
padding: 5px 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
transition: .15s;
|
||||
font: inherit;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.actions-container .al-btn:hover {
|
||||
background: color-mix(in oklab, var(--c-btn, var(--c-panel)) 85%, var(--acid) 15%);
|
||||
border-color: color-mix(in oklab, var(--acid) 40%, var(--c-border-strong));
|
||||
}
|
||||
|
||||
.actions-container .al-btn.warn {
|
||||
background: linear-gradient(180deg, color-mix(in oklab, var(--warning, #f59e0b) 22%, var(--c-btn, var(--c-panel))), var(--c-btn, var(--c-panel)));
|
||||
color: var(--warning, #f59e0b);
|
||||
border-color: color-mix(in oklab, var(--warning, #f59e0b) 45%, var(--c-border));
|
||||
}
|
||||
|
||||
/* ── Main area + center panel (fills viewport remainder) ── */
|
||||
.actions-container #actionsLauncher {
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.actions-container .panel {
|
||||
background: var(--grad-card, var(--c-panel));
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: var(--radius, 12px);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.actions-container .center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ── Toolbar (split selector) ── */
|
||||
.actions-container .toolbar2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid var(--c-border);
|
||||
background: var(--c-panel);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.actions-container .spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.actions-container .seg {
|
||||
display: flex;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
.actions-container .seg button {
|
||||
background: var(--c-panel);
|
||||
color: var(--muted);
|
||||
padding: 4px 10px;
|
||||
border: none;
|
||||
border-right: 1px solid var(--c-border);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.actions-container .seg button:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.actions-container .seg button.active {
|
||||
color: var(--ink-invert, #000);
|
||||
background: linear-gradient(90deg, var(--acid-2, var(--acid)), color-mix(in oklab, var(--acid-2, var(--acid)) 60%, white));
|
||||
}
|
||||
|
||||
/* ── Multi-console grid (fills remaining height) ── */
|
||||
.actions-container .multiConsole {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
min-height: 0;
|
||||
grid-auto-flow: row;
|
||||
grid-auto-rows: 1fr;
|
||||
grid-template-rows: repeat(var(--rows, 1), 1fr);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.actions-container .split-1 { grid-template-columns: 1fr; }
|
||||
.actions-container .split-2 { grid-template-columns: 1fr 1fr; }
|
||||
.actions-container .split-3 { grid-template-columns: 1fr 1fr 1fr; }
|
||||
.actions-container .split-4 { grid-template-columns: 1fr 1fr; }
|
||||
|
||||
/* ── Console pane ── */
|
||||
.actions-container .pane {
|
||||
position: relative;
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 10px;
|
||||
background: var(--grad-console, var(--c-panel));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.actions-container .paneHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
border-bottom: 1px solid var(--c-border);
|
||||
background: linear-gradient(180deg, color-mix(in oklab, var(--acid-2, var(--acid)) 6%, transparent), transparent);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.actions-container .paneTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.actions-container .paneTitle .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.actions-container .paneIcon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.actions-container .titleBlock {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.actions-container .titleLine strong {
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.actions-container .metaLine {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.actions-container .metaLine .chip {
|
||||
position: static;
|
||||
transform: none;
|
||||
border: 1px solid var(--c-border-strong);
|
||||
background: var(--c-chip-bg, color-mix(in oklab, var(--c-panel) 80%, transparent));
|
||||
color: var(--muted);
|
||||
padding: 1px 6px;
|
||||
border-radius: 999px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.actions-container .paneBtns {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.actions-container .paneBtns .al-btn {
|
||||
padding: 3px 7px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* ── Log output ── */
|
||||
.actions-container .paneLog {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 4px 8px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
||||
font-size: 11px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.actions-container .logline {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
padding: 2px 4px;
|
||||
line-height: 1.35;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.actions-container .logline.info { color: #bfefff; }
|
||||
.actions-container .logline.ok { color: #9ff7c5; }
|
||||
.actions-container .logline.warn { color: #ffd27a; }
|
||||
.actions-container .logline.err { color: #ff99b3; }
|
||||
.actions-container .logline.dim { color: #6a8596; }
|
||||
|
||||
/* ── Pane drag highlight ── */
|
||||
.actions-container .paneHighlight {
|
||||
box-shadow:
|
||||
0 0 0 2px var(--acid-2, var(--acid)),
|
||||
0 0 20px color-mix(in oklab, var(--acid-2, var(--acid)) 40%, transparent) inset;
|
||||
animation: al-hi 700ms ease-out 1;
|
||||
}
|
||||
|
||||
@keyframes al-hi {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.005); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* ── Arguments tab ── */
|
||||
.actions-container .section {
|
||||
padding: 10px;
|
||||
border-bottom: 1px dashed var(--c-border);
|
||||
}
|
||||
|
||||
.actions-container .h {
|
||||
font-weight: 800;
|
||||
letter-spacing: .5px;
|
||||
color: var(--acid-2, var(--acid));
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.actions-container .sub {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.actions-container .builder {
|
||||
padding: 10px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.actions-container .field {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.actions-container .label {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.actions-container .ctl,
|
||||
.actions-container .select,
|
||||
.actions-container .range {
|
||||
background: var(--c-panel);
|
||||
color: var(--ink);
|
||||
border: 1px solid var(--c-border-strong);
|
||||
border-radius: var(--control-r, 8px);
|
||||
padding: 6px 10px;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.actions-container .ctl:focus,
|
||||
.actions-container .select:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px color-mix(in oklab, var(--acid) 55%, transparent) inset;
|
||||
}
|
||||
|
||||
.actions-container .chips {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.actions-container .chip2 {
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: var(--c-chip-bg, color-mix(in oklab, var(--c-panel) 80%, transparent));
|
||||
border: 1px solid var(--c-border-hi, var(--c-border-strong));
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
transition: .15s;
|
||||
}
|
||||
|
||||
.actions-container .chip2:hover {
|
||||
border-color: color-mix(in oklab, var(--acid) 50%, var(--c-border));
|
||||
color: var(--acid);
|
||||
}
|
||||
|
||||
/* ── Mobile responsive ── */
|
||||
@media (max-width: 860px) {
|
||||
.actions-container.page-with-sidebar {
|
||||
height: calc(100vh - var(--h-topbar, 56px) - var(--h-bottombar, 56px) - 12px);
|
||||
max-height: calc(100vh - var(--h-topbar, 56px) - var(--h-bottombar, 56px) - 12px);
|
||||
}
|
||||
|
||||
.actions-container #actionsLauncher {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.actions-container .toolbar2 {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.actions-container .paneHeader {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.actions-container .paneBtns {
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.actions-container .paneBtns .al-btn {
|
||||
padding: 3px 6px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.actions-container .multiConsole {
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Per-Pane Focus ===== */
|
||||
.actions-container .pane.paneFocused {
|
||||
border-color: color-mix(in oklab, var(--acid) 40%, transparent);
|
||||
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--acid) 15%, transparent);
|
||||
}
|
||||
|
||||
.args-pane-label {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--acid);
|
||||
padding: 4px 0 6px;
|
||||
border-bottom: 1px solid var(--c-border);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
/* ===== Custom Scripts Split Layout ===== */
|
||||
.actions-container #tab-actions.al-split-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.actions-container .al-builtins-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.actions-container .al-custom-section {
|
||||
flex-shrink: 0;
|
||||
max-height: 180px;
|
||||
border-top: 1px dashed var(--c-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.actions-container .al-custom-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.actions-container .al-section-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 10px;
|
||||
background: color-mix(in oklab, var(--c-panel-2, var(--c-panel)) 50%, transparent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.actions-container .al-section-title {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--muted);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -1622,514 +1622,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
ACTIONS LAUNCHER (.actions-container)
|
||||
========================================================================== */
|
||||
.actions-container #actionsLauncher {
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--gap-3, 10px);
|
||||
}
|
||||
|
||||
.actions-container .panel {
|
||||
background: var(--grad-card, var(--c-panel));
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: var(--radius, 14px);
|
||||
box-shadow: var(--elev, 0 10px 30px var(--acid-1a, #00ff9a1a), inset 0 0 0 1px var(--acid-22, #00ff9a22));
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.actions-container .sideheader {
|
||||
padding: 10px 10px 6px;
|
||||
border-bottom: 1px dashed var(--c-border);
|
||||
}
|
||||
|
||||
.actions-container .al-side-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.actions-container .al-side-meta .sidetitle {
|
||||
color: var(--acid);
|
||||
font-weight: 800;
|
||||
letter-spacing: .05em;
|
||||
}
|
||||
|
||||
.actions-container .tabs-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.actions-container .tab-btn {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
padding: 6px 10px;
|
||||
border-radius: 10px;
|
||||
background: var(--c-pill-bg);
|
||||
border: 1px solid var(--c-border);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.actions-container .tab-btn.active {
|
||||
background: var(--grad-chip-selected);
|
||||
outline: 2px solid color-mix(in oklab, var(--acid) 55%, transparent);
|
||||
outline-offset: 0;
|
||||
}
|
||||
|
||||
.actions-container .al-search {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.actions-container .al-input {
|
||||
flex: 1;
|
||||
background: var(--c-panel);
|
||||
border: 1px solid var(--c-border-strong);
|
||||
color: var(--ink);
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--control-r, 10px);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.actions-container .al-input:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px color-mix(in oklab, var(--acid) 55%, transparent) inset;
|
||||
}
|
||||
|
||||
.actions-container .sidecontent {
|
||||
padding: 8px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.actions-container .al-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.actions-container .al-row {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 84px 1fr;
|
||||
gap: 12px;
|
||||
padding: 10px;
|
||||
background: var(--c-panel-2);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: transform .15s ease, border-color .15s ease, box-shadow .15s ease;
|
||||
}
|
||||
|
||||
.actions-container .al-row:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: color-mix(in oklab, var(--accent) 25%, var(--c-border));
|
||||
box-shadow: 0 10px 26px var(--glow-weak);
|
||||
}
|
||||
|
||||
.actions-container .al-row.selected {
|
||||
outline: 2px solid color-mix(in oklab, var(--acid) 35%, transparent);
|
||||
box-shadow: 0 12px 30px color-mix(in oklab, var(--acid) 25%, transparent);
|
||||
}
|
||||
|
||||
.actions-container .al-row .ic {
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 12px;
|
||||
background: var(--c-panel);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.actions-container .ic-img {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.actions-container .al-row>div:nth-child(2) {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.actions-container .name {
|
||||
font-weight: 800;
|
||||
color: var(--acid-2);
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.actions-container .desc {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.actions-container .al-row .chip {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: calc(84px/2 + 10px);
|
||||
transform: translateX(-50%);
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--c-border);
|
||||
background: var(--c-chip-bg);
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.actions-container .chip.ok {
|
||||
color: var(--ok);
|
||||
border-color: color-mix(in oklab, var(--ok) 60%, transparent);
|
||||
}
|
||||
|
||||
.actions-container .chip.err {
|
||||
color: var(--danger);
|
||||
border-color: color-mix(in oklab, var(--danger) 60%, transparent);
|
||||
}
|
||||
|
||||
.actions-container .chip.run {
|
||||
color: var(--acid);
|
||||
border-color: color-mix(in oklab, var(--acid) 60%, transparent);
|
||||
}
|
||||
|
||||
.actions-container .center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.actions-container .toolbar2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid var(--c-border);
|
||||
background: var(--c-panel);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.actions-container .seg {
|
||||
display: flex;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
.actions-container .seg button {
|
||||
background: var(--c-panel);
|
||||
color: var(--muted);
|
||||
padding: 8px 10px;
|
||||
border: none;
|
||||
border-right: 1px solid var(--c-border);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.actions-container .seg button:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.actions-container .seg button.active {
|
||||
color: var(--ink-invert);
|
||||
background: linear-gradient(90deg, var(--acid-2), color-mix(in oklab, var(--acid-2) 60%, white));
|
||||
}
|
||||
|
||||
.actions-container .al-btn {
|
||||
background: var(--c-btn);
|
||||
color: var(--ink);
|
||||
border: 1px solid var(--c-border-strong);
|
||||
border-radius: var(--control-r, 10px);
|
||||
padding: 8px 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
transition: .18s;
|
||||
box-shadow: var(--elev);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.actions-container .al-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-hover);
|
||||
}
|
||||
|
||||
.actions-container .al-btn.warn {
|
||||
background: linear-gradient(180deg, color-mix(in oklab, var(--warning) 28%, var(--c-btn)), var(--c-btn));
|
||||
color: var(--warning);
|
||||
border-color: color-mix(in oklab, var(--warning) 55%, var(--c-border));
|
||||
}
|
||||
|
||||
.actions-container .multiConsole {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
grid-auto-flow: row;
|
||||
grid-auto-rows: 1fr;
|
||||
grid-template-rows: repeat(var(--rows, 1), 1fr);
|
||||
}
|
||||
|
||||
.actions-container .split-1 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.actions-container .split-2 {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.actions-container .split-3 {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
|
||||
.actions-container .split-4 {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.actions-container .pane {
|
||||
position: relative;
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 12px;
|
||||
background: var(--grad-console);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: inset 0 0 0 1px var(--c-border-muted);
|
||||
}
|
||||
|
||||
.actions-container .paneHeader {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid var(--c-border);
|
||||
background: linear-gradient(180deg, color-mix(in oklab, var(--acid-2) 8%, transparent), transparent);
|
||||
}
|
||||
|
||||
.actions-container .paneTitle {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto 1fr;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.actions-container .paneTitle .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.actions-container .paneIcon {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
opacity: .95;
|
||||
}
|
||||
|
||||
.actions-container .titleBlock {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.actions-container .titleLine strong {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.actions-container .metaLine {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.actions-container .metaLine .chip {
|
||||
border: 1px solid var(--c-border-strong);
|
||||
background: var(--c-chip-bg);
|
||||
color: var(--muted);
|
||||
padding: 3px 8px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.actions-container .paneBtns {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.actions-container .paneBtns .al-btn {
|
||||
padding: 6px 8px;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.actions-container .paneLog {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 6px 8px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
||||
font-size: .92rem;
|
||||
}
|
||||
|
||||
.actions-container .logline {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
padding: 4px 6px;
|
||||
line-height: 1.32;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.actions-container .logline.info {
|
||||
color: #bfefff;
|
||||
}
|
||||
|
||||
.actions-container .logline.ok {
|
||||
color: #9ff7c5;
|
||||
}
|
||||
|
||||
.actions-container .logline.warn {
|
||||
color: #ffd27a;
|
||||
}
|
||||
|
||||
.actions-container .logline.err {
|
||||
color: #ff99b3;
|
||||
}
|
||||
|
||||
.actions-container .logline.dim {
|
||||
color: #6a8596;
|
||||
}
|
||||
|
||||
.actions-container .paneHighlight {
|
||||
box-shadow: 0 0 0 2px var(--acid-2), 0 0 24px color-mix(in oklab, var(--acid-2) 55%, transparent) inset, 0 0 40px color-mix(in oklab, var(--acid-2) 35%, transparent);
|
||||
animation: al-hi 900ms ease-out 1;
|
||||
}
|
||||
|
||||
@keyframes al-hi {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.actions-container .section {
|
||||
padding: 12px;
|
||||
border-bottom: 1px dashed var(--c-border);
|
||||
}
|
||||
|
||||
.actions-container .h {
|
||||
font-weight: 800;
|
||||
letter-spacing: .5px;
|
||||
color: var(--acid-2);
|
||||
}
|
||||
|
||||
.actions-container .sub {
|
||||
color: var(--muted);
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.actions-container .builder {
|
||||
padding: 12px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.actions-container .field {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.actions-container .label {
|
||||
font-size: .85rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.actions-container .ctl,
|
||||
.actions-container .select,
|
||||
.actions-container .range {
|
||||
background: var(--c-panel);
|
||||
color: var(--ink);
|
||||
border: 1px solid var(--c-border-strong);
|
||||
border-radius: var(--control-r, 10px);
|
||||
padding: 10px 12px;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.actions-container .ctl:focus,
|
||||
.actions-container .select:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px color-mix(in oklab, var(--acid) 55%, transparent) inset;
|
||||
}
|
||||
|
||||
.actions-container .chips {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.actions-container .chip2 {
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
background: var(--c-chip-bg);
|
||||
border: 1px solid var(--c-border-hi);
|
||||
cursor: pointer;
|
||||
transition: .18s;
|
||||
}
|
||||
|
||||
.actions-container .chip2:hover {
|
||||
box-shadow: 0 0 0 1px var(--c-border-hi) inset, 0 8px 22px var(--glow-weak);
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.actions-container #actionsLauncher {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.actions-container .toolbar2 {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.actions-container .paneHeader {
|
||||
grid-template-columns: 1fr;
|
||||
row-gap: 8px;
|
||||
}
|
||||
|
||||
.actions-container .paneBtns {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.actions-container .paneBtns .al-btn {
|
||||
padding: 5px 6px;
|
||||
font-size: .85rem;
|
||||
}
|
||||
}
|
||||
/* Actions launcher styles moved to actions.css */
|
||||
|
||||
/* ==========================================================================
|
||||
ACTIONS STUDIO (.studio-container)
|
||||
|
||||
277
web/css/pages/plugins.css
Normal file
277
web/css/pages/plugins.css
Normal file
@@ -0,0 +1,277 @@
|
||||
/* ==========================================================================
|
||||
PLUGINS PAGE
|
||||
========================================================================== */
|
||||
|
||||
.plugins-page {
|
||||
padding: 1rem;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.plugins-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.plugins-header h1 {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 800;
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.plugins-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.plugins-actions .btn {
|
||||
padding: 5px 14px;
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 6px;
|
||||
background: var(--c-panel);
|
||||
color: var(--ink);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.plugins-actions .btn:hover { border-color: var(--acid); color: var(--acid); }
|
||||
.plugins-actions .btn-primary {
|
||||
background: color-mix(in oklab, var(--acid) 15%, transparent);
|
||||
border-color: var(--acid);
|
||||
color: var(--acid);
|
||||
}
|
||||
.plugins-actions .btn-primary:hover { background: color-mix(in oklab, var(--acid) 25%, transparent); }
|
||||
|
||||
.plugins-count {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
.plugins-empty {
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
padding: 2rem;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.plugins-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* ===== Plugin card ===== */
|
||||
.plugin-card {
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 10px;
|
||||
background: var(--c-panel);
|
||||
padding: 12px 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
transition: border-color .15s;
|
||||
}
|
||||
.plugin-card:hover { border-color: var(--c-border-strong); }
|
||||
.plugin-card.plugin-disabled { opacity: .55; }
|
||||
|
||||
.plugin-card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.plugin-card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.plugin-card-title strong {
|
||||
font-size: 13px;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.plugin-type-badge {
|
||||
display: inline-block;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
background: color-mix(in oklab, var(--acid) 15%, transparent);
|
||||
color: var(--acid);
|
||||
}
|
||||
.badge-action { background: color-mix(in oklab, #3b82f6 15%, transparent); color: #3b82f6; }
|
||||
.badge-notifier { background: color-mix(in oklab, #f59e0b 15%, transparent); color: #f59e0b; }
|
||||
.badge-enricher { background: color-mix(in oklab, #8b5cf6 15%, transparent); color: #8b5cf6; }
|
||||
.badge-exporter { background: color-mix(in oklab, #06b6d4 15%, transparent); color: #06b6d4; }
|
||||
.badge-widget { background: color-mix(in oklab, #ec4899 15%, transparent); color: #ec4899; }
|
||||
|
||||
.plugin-status {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.plugin-status.status-loaded { background: color-mix(in oklab, #22c55e 18%, transparent); color: #22c55e; }
|
||||
.plugin-status.status-disabled { background: color-mix(in oklab, var(--muted) 15%, transparent); color: var(--muted); }
|
||||
.plugin-status.status-error { background: color-mix(in oklab, #ef4444 18%, transparent); color: #ef4444; }
|
||||
.plugin-status.status-missing { background: color-mix(in oklab, #f59e0b 18%, transparent); color: #f59e0b; }
|
||||
|
||||
.plugin-card-info { font-size: 12px; }
|
||||
.plugin-desc { color: var(--ink); margin: 0 0 4px; line-height: 1.4; }
|
||||
.plugin-meta { color: var(--muted); font-size: 11px; display: flex; gap: 8px; }
|
||||
|
||||
.plugin-hooks {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
.hook-badge {
|
||||
padding: 1px 6px;
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.plugin-error {
|
||||
padding: 6px 8px;
|
||||
background: color-mix(in oklab, #ef4444 10%, transparent);
|
||||
border: 1px solid color-mix(in oklab, #ef4444 30%, transparent);
|
||||
border-radius: 5px;
|
||||
color: #fca5a5;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.plugin-deps-warn {
|
||||
padding: 4px 8px;
|
||||
background: color-mix(in oklab, #f59e0b 10%, transparent);
|
||||
border: 1px solid color-mix(in oklab, #f59e0b 30%, transparent);
|
||||
border-radius: 5px;
|
||||
color: #fcd34d;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.plugin-card-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.plugin-card-actions button {
|
||||
padding: 4px 10px;
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 5px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.plugin-card-actions button:hover { color: var(--ink); border-color: var(--c-border-strong); }
|
||||
.plugin-card-actions .btn-danger { color: #ef4444; border-color: color-mix(in oklab, #ef4444 40%, transparent); }
|
||||
.plugin-card-actions .btn-danger:hover { background: color-mix(in oklab, #ef4444 12%, transparent); }
|
||||
|
||||
/* ===== Plugin toggle ===== */
|
||||
.plugin-toggle {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.plugin-toggle input { opacity: 0; width: 0; height: 0; position: absolute; }
|
||||
.plugin-toggle-track {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 10px;
|
||||
background: var(--c-border);
|
||||
transition: background .2s;
|
||||
}
|
||||
.plugin-toggle-track::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
border-radius: 50%;
|
||||
background: var(--ink);
|
||||
transition: transform .2s;
|
||||
}
|
||||
.plugin-toggle input:checked + .plugin-toggle-track { background: var(--acid); }
|
||||
.plugin-toggle input:checked + .plugin-toggle-track::before { transform: translateX(16px); }
|
||||
|
||||
/* ===== Config modal ===== */
|
||||
.plugin-config-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.plugin-config-modal {
|
||||
width: min(600px, 90vw);
|
||||
max-height: 80vh;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--c-border-strong);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 56px rgba(0,0,0,.6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.plugin-config-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--c-border);
|
||||
font-weight: 700;
|
||||
}
|
||||
.plugin-config-body {
|
||||
padding: 14px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.plugin-config-field label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
.plugin-config-field input,
|
||||
.plugin-config-field select,
|
||||
.plugin-config-field textarea {
|
||||
padding: 5px 8px;
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 5px;
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
.plugin-config-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
padding: 10px 14px;
|
||||
border-top: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
/* Hidden file input for plugin install */
|
||||
.plugins-page input[type="file"] { display: none; }
|
||||
@@ -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); }
|
||||
|
||||
|
||||
@@ -1,493 +1,9 @@
|
||||
/* ==========================================================================
|
||||
ZOMBIELAND (C2 Module) CSS
|
||||
ZOMBIELAND — C2 Dashboard (pages/ copy)
|
||||
NOTE: The authoritative version lives at /web/css/zombieland.css and is
|
||||
loaded dynamically by zombieland.js via loadStylesheet().
|
||||
This file is kept in sync for consistency but is NOT imported in pages.css
|
||||
because the JS handles its own stylesheet lifecycle.
|
||||
========================================================================== */
|
||||
|
||||
/* Main layout constraints */
|
||||
.zombieland-container.page-with-sidebar {
|
||||
height: calc(100vh - var(--h-topbar, 56px) - var(--h-bottombar, 56px) - 24px);
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.zl-sidebar.page-sidebar {
|
||||
width: 260px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--grad-card);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 12px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.zl-main.page-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Sidebar structure */
|
||||
.zl-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid var(--c-border);
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-item .stat-value {
|
||||
font-weight: bold;
|
||||
color: var(--acid);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.stat-item .stat-label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.zl-toolbar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* Modals */
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--grad-card);
|
||||
border: 1px solid var(--c-border);
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
/* Main Grid Layout */
|
||||
.zl-main-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
/* Takes available space except logs */
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.zl-console-panel,
|
||||
.zl-agents-panel,
|
||||
.zl-logs-panel {
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 12px;
|
||||
background: var(--c-panel);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Log Panel underneath */
|
||||
.zl-logs-panel {
|
||||
height: 150px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.zl-panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-bottom: 1px solid var(--c-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.zl-panel-title {
|
||||
font-weight: bold;
|
||||
font-size: 0.9rem;
|
||||
color: var(--acid);
|
||||
}
|
||||
|
||||
.zl-quickbar {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.quick-cmd {
|
||||
background: transparent;
|
||||
border: 1px solid var(--c-border-strong);
|
||||
color: var(--muted);
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.quick-cmd:hover {
|
||||
color: var(--acid);
|
||||
border-color: var(--acid);
|
||||
}
|
||||
|
||||
.zl-console-output,
|
||||
.zl-logs-output {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.8rem;
|
||||
background: #020406;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
.zl-console-input-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border-top: 1px solid var(--c-border);
|
||||
background: var(--c-panel-2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.zl-target-select,
|
||||
.zl-cmd-input,
|
||||
.zl-search-input {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
border: 1px solid var(--c-border-strong);
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.zl-cmd-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.zl-toolbar-left {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
max-width: 200px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.zl-search-input {
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
padding: 4px 20px 4px 8px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.zl-search-clear {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
color: var(--muted);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.zl-agents-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Agent Card Styles */
|
||||
.zl-agent-card {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
transition: 0.2s ease-out;
|
||||
}
|
||||
|
||||
.zl-agent-card.selected {
|
||||
border-color: var(--acid);
|
||||
background: rgba(0, 255, 160, 0.05);
|
||||
}
|
||||
|
||||
.zl-agent-card:hover {
|
||||
border-color: var(--c-border-hi);
|
||||
}
|
||||
|
||||
.zl-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.zl-card-identity {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.zl-card-hostname {
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.zl-card-id {
|
||||
font-size: 0.7rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.zl-pill {
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: bold;
|
||||
background: #222;
|
||||
}
|
||||
|
||||
.zl-pill.online {
|
||||
color: #00ffa0;
|
||||
background: rgba(0, 255, 160, 0.1);
|
||||
}
|
||||
|
||||
.zl-pill.idle {
|
||||
color: #ffcc00;
|
||||
background: rgba(255, 204, 0, 0.1);
|
||||
}
|
||||
|
||||
.zl-pill.offline {
|
||||
color: #ff3333;
|
||||
background: rgba(255, 51, 51, 0.1);
|
||||
}
|
||||
|
||||
/* ECG Animation */
|
||||
.zl-ecg-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ecg {
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
max-width: 140px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 6px;
|
||||
border: 1px solid #111;
|
||||
}
|
||||
|
||||
.ecg-wrapper {
|
||||
display: flex;
|
||||
width: 300%;
|
||||
animation: ecg-slide linear infinite;
|
||||
}
|
||||
|
||||
.ecg svg {
|
||||
width: 33.33%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ecg path {
|
||||
fill: none;
|
||||
stroke-width: 1.5;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.ecg.green path {
|
||||
stroke: #00ffa0;
|
||||
filter: drop-shadow(0 0 2px #00ffa0);
|
||||
}
|
||||
|
||||
.ecg.yellow path {
|
||||
stroke: #ffcc00;
|
||||
filter: drop-shadow(0 0 2px #ffcc00);
|
||||
}
|
||||
|
||||
.ecg.orange path {
|
||||
stroke: #ff8800;
|
||||
filter: drop-shadow(0 0 2px #ff8800);
|
||||
}
|
||||
|
||||
.ecg.red path {
|
||||
stroke: #ff3333;
|
||||
}
|
||||
|
||||
.ecg.flat .ecg-wrapper {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes ecg-slide {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(-33.33%);
|
||||
}
|
||||
}
|
||||
|
||||
.zl-ecg-counter {
|
||||
font-size: 0.7rem;
|
||||
color: var(--muted);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.zl-card-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.75rem;
|
||||
color: #ccc;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.zl-card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Console output items */
|
||||
.console-line {
|
||||
margin-bottom: 4px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.console-time {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.console-type {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.console-type.tx {
|
||||
color: var(--acid);
|
||||
}
|
||||
|
||||
.console-type.rx {
|
||||
color: #00aaff;
|
||||
}
|
||||
|
||||
.console-type.info {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.console-type.error {
|
||||
color: #ff3333;
|
||||
}
|
||||
|
||||
.console-type.success {
|
||||
color: #00ffa0;
|
||||
}
|
||||
|
||||
.console-target {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.console-content pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.zl-log-line {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* Mobile Optimization */
|
||||
@media (max-width: 900px) {
|
||||
.zombieland-container.page-with-sidebar {
|
||||
height: auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.zombieland-container .zl-sidebar {
|
||||
width: 100%;
|
||||
max-height: none;
|
||||
flex-shrink: 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.zl-stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.zl-toolbar {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.zl-main-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.zl-console-panel {
|
||||
height: 350px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.zl-agents-panel {
|
||||
height: 350px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.zl-console-input-row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.zl-target-select,
|
||||
.zl-cmd-input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.zl-card-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.zl-card-info {
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
/* Intentionally empty — see /web/css/zombieland.css */
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user