From e175523b8225fb6945d46252c4834519db4e60be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Thu, 18 Jun 2026 23:17:30 +0800 Subject: [PATCH] Add files via upload --- web/static/css/c2.css | 1225 ++++++++++++++++++++++++++++++++++-- web/static/css/style.css | 12 + web/static/i18n/en-US.json | 41 +- web/static/i18n/zh-CN.json | 41 +- web/static/js/c2.js | 881 ++++++++++++++++++++++---- web/templates/index.html | 7 +- 6 files changed, 2019 insertions(+), 188 deletions(-) diff --git a/web/static/css/c2.css b/web/static/css/c2.css index cd71cf82..98961460 100644 --- a/web/static/css/c2.css +++ b/web/static/css/c2.css @@ -72,6 +72,7 @@ } #page-c2-payloads select.form-control, +#page-c2-sessions select.form-control, .c2-modal select.form-control { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2364748b' d='M2.5 4.5L6 8l3.5-3.5'/%3E%3C/svg%3E"); background-repeat: no-repeat; @@ -83,6 +84,7 @@ /* 原生下拉:避免 appearance:none 在部分浏览器中导致 select 无法正常展开 */ #page-c2-payloads select.form-control.c2-native-select, +#page-c2-sessions select.form-control.c2-native-select, .c2-modal select.form-control.c2-native-select { appearance: auto; -webkit-appearance: menulist-button; @@ -432,63 +434,511 @@ overflow: hidden; } -.c2-session-sidebar { - width: 320px; +.c2-session-sidebar-wrap { + width: 300px; + min-width: 260px; + max-width: 340px; + flex-shrink: 0; + display: flex; + flex-direction: column; border-right: 1px solid var(--c2-border); - overflow-y: auto; background: var(--c2-surface-alt); - padding: 16px; +} + +.c2-sessions-toolbar { + flex-shrink: 0; + padding: 8px 10px; + border-bottom: 1px solid var(--c2-border); + display: flex; + flex-direction: column; + gap: 6px; +} + +.c2-sessions-filter-row { + display: flex; + gap: 6px; +} + +.c2-sessions-toolbar .form-control { + width: 100%; + min-width: 0; + font-size: 12px; + padding: 5px 8px; + line-height: 1.3; +} + +.c2-sessions-toolbar-meta { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + color: var(--c2-text-dim); + min-height: 18px; + flex-wrap: wrap; +} + +.c2-sessions-select-all-label { + display: inline-flex; + align-items: center; + gap: 4px; + cursor: pointer; + user-select: none; + flex-shrink: 0; +} + +.c2-sessions-quick-links { + display: inline-flex; + gap: 6px; + margin-left: auto; + flex-shrink: 0; +} + +.c2-sessions-quick-links button { + border: none; + background: none; + padding: 0; + font-size: 11px; + color: var(--c2-accent); + cursor: pointer; +} + +.c2-sessions-quick-links button:hover { + text-decoration: underline; +} + +.c2-sessions-count { + font-size: 11px; + color: var(--c2-text-muted); + white-space: nowrap; +} + +.c2-session-sidebar { + flex: 1; + min-height: 0; + overflow-y: auto; + padding: 8px 10px; +} + +.c2-session-list-empty { + padding: 20px 8px; + text-align: center; + font-size: 12px; + color: var(--c2-text-dim); + line-height: 1.5; +} + +.c2-session-main { + flex: 1; + min-width: 0; + min-height: 0; + overflow: hidden; + padding: 12px 16px 16px; + background: linear-gradient(180deg, #f8fafc 0%, #ffffff 180px); + display: flex; + flex-direction: column; +} + +.c2-session-detail { + max-width: 1400px; + margin: 0 auto; + width: 100%; + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; +} + +.c2-session-header-bar { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid var(--c2-border); + flex-shrink: 0; +} + +/* Session detail hero header */ +.c2-session-hero { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 12px; + margin-bottom: 10px; + border: 1px solid var(--c2-border); + border-radius: var(--c2-radius-xs); + background: var(--c2-surface); + box-shadow: none; + flex-shrink: 0; +} + +.c2-session-hero__main { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; + flex: 1; +} + +.c2-session-hero__text { + min-width: 0; + flex: 1; +} + +.c2-session-hero__title-row { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 4px; +} + +.c2-session-hero__title { + font-size: 16px; + font-weight: 800; + color: var(--c2-text); + margin: 0; + line-height: 1.2; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; +} + +.c2-session-hero__sub { + font-size: 11px; + color: var(--c2-text-dim); + font-family: var(--c2-mono); + margin-bottom: 4px; +} + +.c2-session-hero__chips { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +.c2-session-hero-chip { + font-size: 10px; + font-weight: 600; + padding: 1px 7px; + border-radius: 999px; + background: var(--c2-surface); + border: 1px solid var(--c2-border); + color: var(--c2-text-dim); + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.c2-session-hero-chip.is-mono { + font-family: var(--c2-mono); + font-size: 10px; + color: var(--c2-accent); + background: var(--c2-accent-dim); + border-color: rgba(59, 130, 246, 0.2); +} + +.c2-session-hero-root { + font-size: 10px; + font-weight: 800; + padding: 3px 8px; + border-radius: 999px; + background: var(--c2-amber-dim); + color: #b45309; + letter-spacing: 0.04em; +} + +.c2-session-avatar { + width: 36px; + height: 36px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: -0.03em; + flex-shrink: 0; + border: 1px solid var(--c2-border); + font-family: var(--c2-mono); +} + +.c2-session-avatar--darwin { + background: linear-gradient(145deg, #e0e7ff 0%, #c7d2fe 100%); + color: #4338ca; + border-color: #c7d2fe; +} + +.c2-session-avatar--windows { + background: linear-gradient(145deg, #dbeafe 0%, #93c5fd 100%); + color: #1d4ed8; + border-color: #bfdbfe; +} + +.c2-session-avatar--linux { + background: linear-gradient(145deg, #fef3c7 0%, #fcd34d 100%); + color: #b45309; + border-color: #fde68a; +} + +.c2-session-avatar--default { + background: var(--c2-surface-alt); + color: var(--c2-text-dim); +} + +.c2-session-hero__side { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + flex-shrink: 0; +} + +.c2-session-hero__heartbeat { + display: flex; + align-items: center; + gap: 6px; + padding: 3px 8px; + border-radius: 999px; + background: var(--c2-surface-alt); + border: 1px solid var(--c2-border); + font-size: 10px; +} + +.c2-session-hero__heartbeat-label { + color: var(--c2-text-muted); + font-weight: 600; +} + +.c2-session-hero__heartbeat-value { + color: var(--c2-green); + font-weight: 700; + font-family: var(--c2-mono); +} + +.c2-session-main-empty { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: 48px 32px; + color: var(--c2-text-dim); +} + +.c2-session-main-empty__icon { + width: 64px; + height: 64px; + border-radius: 18px; + margin-bottom: 16px; + background: var(--c2-accent-dim); + border: 1px dashed rgba(59, 130, 246, 0.35); + position: relative; +} + +.c2-session-main-empty__icon::before, +.c2-session-main-empty__icon::after { + content: ''; + position: absolute; + background: var(--c2-accent); + opacity: 0.35; + border-radius: 2px; +} + +.c2-session-main-empty__icon::before { + left: 50%; + top: 18px; + width: 22px; + height: 3px; + margin-left: -11px; +} + +.c2-session-main-empty__icon::after { + left: 50%; + top: 26px; + width: 16px; + height: 3px; + margin-left: -8px; +} + +.c2-session-main-empty h3 { + margin: 0 0 8px; + font-size: 18px; + font-weight: 700; + color: var(--c2-text); +} + +.c2-session-main-empty p { + margin: 0; + font-size: 13px; + line-height: 1.6; + max-width: 320px; } .c2-session-list { display: flex; flex-direction: column; gap: 6px; } .c2-session-item { - padding: 14px 16px; - border-radius: var(--c2-radius-sm); + position: relative; + padding: 0; + border-radius: 10px; cursor: pointer; - transition: all 0.15s ease; - border: 1.5px solid transparent; + transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease; + border: 1px solid var(--c2-border); background: var(--c2-surface); + overflow: hidden; +} + +.c2-session-item::before { + content: ''; + position: absolute; + left: 0; + top: 10px; + bottom: 10px; + width: 3px; + border-radius: 0 3px 3px 0; + background: var(--c2-border); + transition: background 0.15s ease; +} + +.c2-session-item[data-status="active"]::before { background: var(--c2-green); } +.c2-session-item[data-status="sleeping"]::before { background: var(--c2-amber); } +.c2-session-item[data-status="dead"]::before { background: var(--c2-red); } + +.c2-session-item-check { + position: absolute; + left: 12px; + top: 14px; + cursor: pointer; + z-index: 1; +} + +.c2-session-item-body { + padding: 9px 10px 8px 34px; } .c2-session-item:hover { border-color: var(--c2-border-hover); - background: var(--c2-surface); box-shadow: var(--c2-shadow-sm); } .c2-session-item.active { - background: var(--c2-accent-dim); + background: linear-gradient(135deg, rgba(59, 130, 246, 0.06) 0%, rgba(59, 130, 246, 0.02) 100%); border-color: var(--c2-accent); - box-shadow: 0 0 0 1px var(--c2-accent); + box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.15), var(--c2-shadow-sm); } .c2-session-header { display: flex; justify-content: space-between; + align-items: flex-start; + gap: 8px; + margin-bottom: 0; +} + +.c2-session-host-row { + display: flex; align-items: center; - margin-bottom: 6px; + gap: 8px; + min-width: 0; + flex: 1; +} + +.c2-session-live-dot { + width: 7px; + height: 7px; + border-radius: 50%; + flex-shrink: 0; + background: var(--c2-text-muted); +} + +.c2-session-live-dot.active { + background: var(--c2-green); + box-shadow: 0 0 0 3px var(--c2-green-dim); + animation: c2-pulse-dot 2.2s ease-in-out infinite; +} + +.c2-session-live-dot.sleeping { background: var(--c2-amber); } +.c2-session-live-dot.dead { background: var(--c2-red); opacity: 0.7; } + +@keyframes c2-pulse-dot { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.6; transform: scale(0.92); } } .c2-session-host { font-weight: 700; - font-size: 14px; + font-size: 13px; color: var(--c2-text); + line-height: 1.35; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .c2-session-status { font-size: 10px; padding: 2px 8px; - border-radius: var(--c2-radius-xs); + border-radius: 999px; text-transform: uppercase; - letter-spacing: 0.5px; + letter-spacing: 0.4px; font-weight: 700; + flex-shrink: 0; } .c2-session-status.active { background: var(--c2-green-dim); color: var(--c2-green); } .c2-session-status.sleeping { background: var(--c2-amber-dim); color: var(--c2-amber); } .c2-session-status.dead { background: var(--c2-red-dim); color: var(--c2-red); } +.c2-session-chips { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 6px; +} + +.c2-session-chips--sub { margin-top: 4px; } + +.c2-session-chip { + font-size: 10px; + font-weight: 600; + padding: 2px 7px; + border-radius: 999px; + background: var(--c2-surface-alt); + color: var(--c2-text-dim); + border: 1px solid var(--c2-border); + line-height: 1.4; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.c2-session-chip--mono { + font-family: var(--c2-mono); + font-size: 10px; + letter-spacing: -0.02em; +} + +.c2-session-chip--warn { + background: var(--c2-amber-dim); + color: #b45309; + border-color: transparent; +} + +.c2-session-chip--dim { + font-weight: 500; + color: var(--c2-text-muted); + background: transparent; + border-color: transparent; + padding-left: 0; + padding-right: 0; +} + .c2-session-meta { font-size: 12px; color: var(--c2-text-dim); @@ -505,8 +955,8 @@ } .c2-session-item-time { - font-size: 10px; - opacity: 0.5; + font-size: 11px; + color: var(--c2-text-muted); flex: 1; min-width: 0; } @@ -534,24 +984,6 @@ background: var(--c2-surface); } -.c2-session-main { - flex: 1; - overflow-y: auto; - padding: 24px; - background: var(--c2-surface); -} - -.c2-session-detail { max-width: 1400px; margin: 0 auto; } - -.c2-session-header-bar { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 24px; - padding-bottom: 20px; - border-bottom: 1px solid var(--c2-border); -} - .c2-session-title h3 { font-size: 22px; margin-bottom: 6px; @@ -564,10 +996,16 @@ .c2-session-badge { font-size: 10px; padding: 3px 10px; - border-radius: var(--c2-radius-xs); + border-radius: 999px; font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; } +.c2-session-badge.active { background: var(--c2-green-dim); color: var(--c2-green); } +.c2-session-badge.sleeping { background: var(--c2-amber-dim); color: var(--c2-amber); } +.c2-session-badge.dead { background: var(--c2-red-dim); color: var(--c2-red); } + .c2-session-subtitle { font-size: 13px; color: var(--c2-text-dim); @@ -580,20 +1018,138 @@ .c2-session-tabs { display: flex; gap: 2px; - margin-bottom: 24px; + margin-bottom: 12px; border-bottom: 1.5px solid var(--c2-border); padding-bottom: 0; + flex-shrink: 0; +} + +.c2-session-tabs--pills { + gap: 4px; + padding: 3px; + margin-bottom: 10px; + border: 1px solid var(--c2-border); + border-radius: var(--c2-radius-sm); + background: var(--c2-surface-alt); + border-bottom: 1px solid var(--c2-border); +} + +.c2-session-tabs--pills .c2-session-tab { + flex: 1; + text-align: center; + padding: 6px 10px; + border-radius: var(--c2-radius-xs); + margin-bottom: 0; + border: 1px solid transparent; + background: transparent; + transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease; +} + +.c2-session-tabs--pills .c2-session-tab:hover { + background: rgba(255, 255, 255, 0.7); + color: var(--c2-text); +} + +.c2-session-tabs--pills .c2-session-tab.active { + background: var(--c2-surface); + color: var(--c2-accent); + box-shadow: var(--c2-shadow-sm); + border-color: var(--c2-border); +} + +.c2-session-tab-content { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; +} + +.c2-tab-panel { display: none; } +.c2-tab-panel.active { + flex: 1; + min-height: 0; +} + +#c2-tab-terminal.active { + display: flex; + flex-direction: column; +} + +.c2-tab-panel.active:not(#c2-tab-terminal) { + display: block; + overflow-y: auto; +} + +#c2-tab-tasks.active { + padding: 0; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.c2-tab-panel--card.active { + overflow-y: auto; +} + +.c2-file-panel { + border: 1px solid var(--c2-border); + border-radius: var(--c2-radius-sm); + background: var(--c2-surface); + box-shadow: var(--c2-shadow-sm); + overflow: hidden; +} + +.c2-file-panel .c2-file-toolbar { + margin-bottom: 0; + border-radius: 0; + border: none; + border-bottom: 1px solid var(--c2-border); +} + +.c2-file-panel .c2-file-list { + padding: 0 8px 8px; +} + +#c2-session-tasks-list { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; } .c2-session-tab { padding: 10px 20px; cursor: pointer; - transition: all 0.15s; font-weight: 600; font-size: 13px; color: var(--c2-text-dim); border-bottom: 2px solid transparent; margin-bottom: -1.5px; + appearance: none; + -webkit-appearance: none; + background: transparent; + border-top: none; + border-left: none; + border-right: none; + font-family: inherit; + line-height: 1.35; + outline: none; + -webkit-tap-highlight-color: transparent; + user-select: none; + transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease; +} + +.c2-session-tab:focus { + outline: none; +} + +.c2-session-tab:focus-visible { + outline: 2px solid rgba(59, 130, 246, 0.45); + outline-offset: 1px; +} + +.c2-session-tab:active { + transform: none; } .c2-session-tab:hover { @@ -605,24 +1161,33 @@ border-bottom-color: var(--c2-accent); } -.c2-tab-panel { display: none; } -.c2-tab-panel.active { display: block; } - /* ============================================================================ Terminal ============================================================================ */ .c2-terminal-container { - background: #0f172a; + background: #0d1117; border-radius: var(--c2-radius) var(--c2-radius) 0 0; - height: 480px; + flex: 1; + min-height: 200px; overflow: hidden; position: relative; border: 1.5px solid #1e293b; border-bottom: none; } +.c2-terminal-container .xterm { + height: 100%; + padding: 4px 6px; + box-sizing: border-box; +} + +.c2-terminal-container .xterm-viewport { + overflow-y: auto !important; +} + .c2-terminal-toolbar { + flex-shrink: 0; display: flex; gap: 8px; padding: 10px 16px; @@ -887,6 +1452,253 @@ .c2-task-item-compact:hover { background: var(--c2-surface-alt); } +/* Session detail — task history panel */ +.c2-session-tasks-panel { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + height: 100%; + border: 1px solid var(--c2-border); + border-radius: var(--c2-radius-sm); + background: var(--c2-surface); + overflow: hidden; + box-shadow: var(--c2-shadow-sm); +} + +.c2-session-tasks-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px 16px; + border-bottom: 1px solid var(--c2-border); + background: linear-gradient(180deg, var(--c2-surface-alt) 0%, var(--c2-surface) 100%); + flex-shrink: 0; +} + +.c2-session-tasks-toolbar-title { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.c2-session-tasks-heading { + font-size: 14px; + font-weight: 700; + color: var(--c2-text); +} + +.c2-session-tasks-count { + font-size: 11px; + font-weight: 600; + color: var(--c2-text-dim); + padding: 2px 8px; + border-radius: 999px; + background: var(--c2-surface-alt); + border: 1px solid var(--c2-border); + white-space: nowrap; +} + +.c2-session-tasks-refresh { + flex-shrink: 0; +} + +.c2-session-tasks-rows { + flex: 1; + min-height: 0; + overflow-y: auto; + padding: 8px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.c2-session-task-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 10px 12px; + border: 1px solid var(--c2-border); + border-radius: var(--c2-radius-xs); + background: var(--c2-surface); + transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease; +} + +.c2-session-task-row:hover { + border-color: var(--c2-border-hover); + background: var(--c2-surface-alt); + box-shadow: var(--c2-shadow-sm); +} + +.c2-session-task-row.is-pending { + border-color: rgba(59, 130, 246, 0.25); + background: linear-gradient(90deg, rgba(59, 130, 246, 0.04) 0%, var(--c2-surface) 40%); +} + +.c2-session-task-row__main { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; + flex: 1; +} + +.c2-session-task-row__cmd { + min-width: 0; + flex: 1; +} + +.c2-session-task-command { + display: block; + font-family: var(--c2-mono); + font-size: 12px; + color: var(--c2-text); + line-height: 1.45; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.c2-session-task-command--muted { + color: var(--c2-text-muted); + font-family: inherit; +} + +.c2-session-task-row__meta { + display: flex; + align-items: center; + gap: 10px; + flex-shrink: 0; +} + +.c2-session-task-duration { + font-family: var(--c2-mono); + font-size: 11px; + color: var(--c2-text-muted); + min-width: 42px; + text-align: right; +} + +.c2-session-task-time { + font-size: 11px; + color: var(--c2-text-muted); + min-width: 52px; + text-align: right; + white-space: nowrap; +} + +.c2-session-task-view { + min-width: 52px; +} + +.c2-task-type-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 44px; + padding: 3px 8px; + border-radius: var(--c2-radius-xs); + font-size: 10px; + font-weight: 700; + text-transform: lowercase; + letter-spacing: 0.02em; + flex-shrink: 0; + font-family: var(--c2-mono); +} + +.c2-task-type-badge--shell { + background: var(--c2-accent-dim); + color: var(--c2-accent); +} + +.c2-task-type-badge--fs { + background: var(--c2-purple-dim); + color: var(--c2-purple); +} + +.c2-task-type-badge--control { + background: var(--c2-amber-dim); + color: #b45309; +} + +.c2-task-type-badge--default { + background: #f1f5f9; + color: var(--c2-text-dim); +} + +.c2-empty-inline { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: 48px 24px; + color: var(--c2-text-dim); + flex: 1; +} + +.c2-empty-inline__icon { + width: 44px; + height: 44px; + border-radius: 12px; + margin-bottom: 12px; + background: var(--c2-accent-dim); + border: 1px dashed rgba(59, 130, 246, 0.35); + position: relative; +} + +.c2-empty-inline__icon::after { + content: ''; + position: absolute; + left: 50%; + top: 50%; + width: 14px; + height: 14px; + margin: -7px 0 0 -7px; + border: 2px solid var(--c2-accent); + border-radius: 3px; + opacity: 0.55; +} + +.c2-empty-inline__text { + font-size: 13px; + line-height: 1.5; + max-width: 280px; +} + +.c2-task-status-dot.running, +.c2-task-status-dot.sent, +.c2-task-status-dot.queued { + animation: c2-task-pulse 1.4s ease-in-out infinite; +} + +@keyframes c2-task-pulse { + 0%, 100% { opacity: 1; box-shadow: 0 0 0 0 transparent; } + 50% { opacity: 0.65; box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.12); } +} + +.c2-task-status-dot.queued { animation-name: c2-task-pulse-muted; } +@keyframes c2-task-pulse-muted { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.45; } +} + +@media (max-width: 1100px) { + .c2-session-task-row { + flex-direction: column; + align-items: stretch; + gap: 10px; + } + + .c2-session-task-row__meta { + justify-content: flex-end; + flex-wrap: wrap; + } +} + .c2-task-status-dot { width: 8px; height: 8px; @@ -1139,9 +1951,123 @@ } /* ============================================================================ - Info Grid + Session Info Panel (compact) ============================================================================ */ +.c2-session-info-panel { + display: flex; + flex-direction: column; + gap: 8px; + padding: 0; + max-width: 920px; +} + +.c2-session-info-block { + border: 1px solid var(--c2-border); + border-radius: var(--c2-radius-xs); + background: var(--c2-surface); + overflow: hidden; +} + +.c2-session-info-block__head { + padding: 5px 10px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.03em; + color: var(--c2-text-dim); + background: var(--c2-surface-alt); + border-bottom: 1px solid var(--c2-border); +} + +.c2-session-info-dl { + margin: 0; + display: flex; + flex-direction: column; +} + +.c2-session-info-dl__row { + display: grid; + grid-template-columns: 100px minmax(0, 1fr); + gap: 10px; + align-items: center; + padding: 4px 10px; + border-bottom: 1px solid var(--c2-border); + min-height: 26px; +} + +.c2-session-info-dl__row--full { + grid-template-columns: 100px minmax(0, 1fr); +} + +.c2-session-info-dl__row:last-child { + border-bottom: none; +} + +.c2-session-info-dl__label { + margin: 0; + font-size: 11px; + font-weight: 600; + color: var(--c2-text-muted); + line-height: 1.3; + white-space: nowrap; +} + +.c2-session-info-dl__value { + margin: 0; + font-size: 12px; + font-weight: 500; + color: var(--c2-text); + line-height: 1.35; + word-break: break-all; +} + +.c2-session-info-dl__value.is-mono { + font-family: var(--c2-mono); + font-size: 11px; +} + +.c2-session-info-dl__value.is-accent { + color: var(--c2-accent); + font-weight: 600; +} + +.c2-session-info-dl__value.is-warn { + color: #b45309; + font-weight: 700; +} + +.c2-session-info-block--note .c2-session-info-note { + padding: 6px 10px 8px; + font-size: 12px; + line-height: 1.5; + color: var(--c2-text-dim); + min-height: 0; + white-space: pre-wrap; + word-break: break-word; +} + +@media (max-width: 900px) { + .c2-session-hero { + flex-direction: column; + } + + .c2-session-hero__side { + width: 100%; + flex-direction: row; + align-items: center; + justify-content: space-between; + } + + .c2-session-info-dl { + display: flex; + } + + .c2-session-info-dl__row { + grid-template-columns: 84px minmax(0, 1fr); + } +} + +/* Legacy info grid (profiles etc.) */ .c2-info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); @@ -1438,6 +2364,209 @@ border-radius: 0 0 16px 16px; } +/* Sleep settings modal */ +.c2-modal.c2-modal--sleep { + max-width: 460px; + padding: 0; + overflow: hidden; +} + +.c2-sleep-modal { + display: flex; + flex-direction: column; +} + +.c2-sleep-modal__header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + padding: 18px 20px 14px; + border-bottom: 1px solid var(--c2-border); + background: linear-gradient(135deg, rgba(59, 130, 246, 0.08) 0%, var(--c2-surface) 70%); +} + +.c2-sleep-modal__title-wrap { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} + +.c2-sleep-modal__icon { + width: 40px; + height: 40px; + border-radius: 12px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + color: var(--c2-accent); + background: var(--c2-accent-dim); + border: 1px solid rgba(59, 130, 246, 0.25); +} + +.c2-sleep-modal__icon svg { + width: 22px; + height: 22px; + display: block; +} + +.c2-sleep-modal__title { + margin: 0; + font-size: 17px; + font-weight: 800; + color: var(--c2-text); + line-height: 1.25; +} + +.c2-sleep-modal__host { + margin: 4px 0 0; + font-size: 12px; + color: var(--c2-text-dim); + font-family: var(--c2-mono); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 280px; +} + +.c2-sleep-modal__current { + padding: 8px 20px; + font-size: 12px; + font-weight: 600; + color: var(--c2-text-dim); + background: var(--c2-surface-alt); + border-bottom: 1px solid var(--c2-border); +} + +.c2-sleep-modal__body { + padding: 16px 20px 12px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.c2-sleep-field__head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 8px; +} + +.c2-sleep-field__head label { + font-size: 13px; + font-weight: 600; + color: var(--c2-text); +} + +.c2-sleep-field__value { + font-size: 12px; + font-weight: 700; + color: var(--c2-accent); + font-family: var(--c2-mono); +} + +.c2-sleep-field__control { + display: grid; + grid-template-columns: 1fr 72px; + gap: 10px; + align-items: center; +} + +.c2-sleep-range { + width: 100%; + height: 6px; + accent-color: var(--c2-accent); + cursor: pointer; +} + +.c2-sleep-number { + width: 100%; + padding: 6px 8px; + font-size: 13px; + font-family: var(--c2-mono); + font-weight: 600; + text-align: center; + color: var(--c2-text); + background: var(--c2-surface); + border: 1px solid var(--c2-border); + border-radius: var(--c2-radius-xs); + outline: none; +} + +.c2-sleep-number:focus { + border-color: var(--c2-accent); + box-shadow: 0 0 0 3px var(--c2-accent-dim); +} + +.c2-sleep-presets { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; + margin-top: 10px; +} + +.c2-sleep-presets__label { + font-size: 11px; + font-weight: 600; + color: var(--c2-text-muted); + margin-right: 2px; +} + +.c2-sleep-preset { + border: 1px solid var(--c2-border); + background: var(--c2-surface); + color: var(--c2-text-dim); + font-size: 11px; + font-weight: 700; + font-family: var(--c2-mono); + padding: 4px 10px; + border-radius: 999px; + cursor: pointer; + transition: all 0.15s ease; +} + +.c2-sleep-preset:hover { + border-color: var(--c2-accent); + color: var(--c2-accent); +} + +.c2-sleep-preset.is-active { + background: var(--c2-accent-dim); + border-color: var(--c2-accent); + color: var(--c2-accent); +} + +.c2-sleep-modal__preview { + padding: 10px 12px; + border-radius: var(--c2-radius-xs); + background: linear-gradient(90deg, rgba(16, 185, 129, 0.08), rgba(59, 130, 246, 0.06)); + border: 1px solid rgba(59, 130, 246, 0.18); + font-size: 12px; + font-weight: 600; + color: var(--c2-text); + font-family: var(--c2-mono); +} + +.c2-sleep-modal__hint { + margin: 0; + font-size: 11px; + line-height: 1.55; + color: var(--c2-text-muted); +} + +.c2-sleep-modal__footer { + display: flex; + justify-content: flex-end; + gap: 10px; + padding: 14px 20px 18px; + border-top: 1px solid var(--c2-border); + background: var(--c2-surface-alt); +} + /* ============================================================================ Toolbar ============================================================================ */ @@ -1456,10 +2585,12 @@ ============================================================================ */ @media (max-width: 768px) { - .c2-session-layout { flex-direction: column; } - .c2-session-sidebar { + .c2-session-layout { flex-direction: column; height: auto; min-height: calc(100vh - 140px); } + .c2-session-sidebar-wrap { width: 100%; - max-height: 220px; + max-width: none; + min-width: 0; + max-height: 260px; border-right: none; border-bottom: 1px solid var(--c2-border); } diff --git a/web/static/css/style.css b/web/static/css/style.css index cc9ef7fe..6c912f95 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -444,6 +444,18 @@ body { align-items: center; } +.page-header-actions .btn-danger, +.page-header-actions .btn-secondary, +.page-header-actions .btn-primary { + padding: 7px 16px; + font-size: 0.875rem; + line-height: 1.25; + display: inline-flex; + align-items: center; + justify-content: center; + box-sizing: border-box; +} + .page-content { flex: 1; overflow-y: auto; diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 3a39089d..c38ee450 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -2691,6 +2691,11 @@ }, "c2": { "clipboardCopied": "Copied to clipboard", + "common": { + "justNow": "Just now", + "minutesAgo": "{{n}}m ago", + "hoursAgo": "{{n}}h ago" + }, "fmt": { "durationMs": "{{n}}ms", "durationSec": "{{n}}s", @@ -2748,6 +2753,8 @@ "bindHintExternal": "Use 0.0.0.0 to allow external access", "callbackHost": "Callback host (optional)", "callbackHostHint": "Public IP or hostname stored for payloads/beacons; separate from bind address. If empty, payload generation falls back to bind address / auto-detect.", + "allowLegacyShell": "Allow unencrypted classic reverse shell (lab only)", + "allowLegacyShellHint": "Off by default. When enabled, raw bash/nc TCP connections register sessions and are vulnerable to internet scanners; use encrypted Beacon builds for production.", "malleableProfile": "Malleable Profile", "malleableProfileHint": "Optional; HTTP/HTTPS Beacon response headers and traffic disguise. Stop and start the listener again for changes to take effect.", "malleableProfileNone": "None", @@ -2825,10 +2832,22 @@ "infoFirstSeen": "First seen", "infoLastCheckin": "Last check-in", "infoNote": "Note", + "infoNoteEmpty": "No notes", + "infoSectionIdentity": "Identity", + "infoSectionSystem": "System", + "infoSectionNetwork": "Network & beacon", + "infoSectionTimeline": "Timeline", + "infoSectionNote": "Notes", "adminYes": "Yes", "adminNo": "No", "promptSleepSeconds": "Sleep interval (seconds)", "promptJitterPercent": "Jitter percent (0–100)", + "sleepModalHint": "Saves to the server and queues a sleep task. The implant applies it on the next task poll; later check-ins keep this config.", + "sleepModalTitle": "Beacon interval", + "sleepModalCurrent": "Current {{sec}}s · jitter {{jitter}}%", + "sleepModalPreview": "Estimated {{min}} – {{max}} s", + "sleepModalPresets": "Presets", + "toastSleepInvalid": "Sleep interval must be at least 1 second", "toastSleepUpdated": "Sleep settings updated", "confirmExitSession": "Send exit command to this session?", "confirmDeleteSession": "Remove this session and related tasks/files from the server? (Does not send exit to the implant; use Kill Session to exit the agent.)", @@ -2846,7 +2865,25 @@ "termWaitFinish": "Please wait for the current command to finish", "termCtrlC": "Remote interrupt is not supported in this version", "termQueued": "[Command queued — will run after the current task completes]", - "clearTerminal": "Clear" + "clearTerminal": "Clear", + "batchDelete": "Delete selected", + "deleteFiltered": "Delete filtered", + "selectAll": "Select all", + "filterAllStatus": "All statuses", + "filterAllListeners": "All listeners", + "filterSearchPlaceholder": "Search hostname / user / IP", + "filterApply": "Filter", + "filterReset": "Reset", + "filterSuspicious": "Likely false positives", + "filterCount": "{{n}} total, {{selected}} selected", + "emptyFilter": "No sessions match the current filters", + "listEmpty": "No sessions", + "selectPromptTitle": "Select a session", + "selectPromptHint": "Click a session in the list on the left to view terminal, files, and tasks.", + "confirmBatchDelete": "Delete {{n}} selected session(s)? Related tasks and file records will be removed.", + "confirmDeleteFiltered": "Delete all {{n}} session(s) in the current filter results?", + "toastSelectFirst": "Select at least one session to delete", + "toastBatchDeleted": "Deleted {{n}} session(s)" }, "tasks": { "title": "Task Management", @@ -2869,6 +2906,8 @@ "pending": "Pending", "emptyAll": "No tasks yet", "emptySession": "No tasks for this session", + "sessionTaskHistory": "Task history", + "sessionTaskCount": "{{n}} tasks", "colTask": "Task", "colSession": "Session", "colType": "Type", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 5a0fbaf7..1802d63e 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -2679,6 +2679,11 @@ }, "c2": { "clipboardCopied": "已复制到剪贴板", + "common": { + "justNow": "刚刚", + "minutesAgo": "{{n}} 分钟前", + "hoursAgo": "{{n}} 小时前" + }, "fmt": { "durationMs": "{{n}}ms", "durationSec": "{{n}}秒", @@ -2736,6 +2741,8 @@ "bindHintExternal": "使用 0.0.0.0 允许外部访问", "callbackHost": "回连地址(可选)", "callbackHostHint": "公网 IP 或域名,写入配置供 Payload/Beacon 使用;与「绑定地址」分离。不填则生成 Payload 时按绑定地址或自动探测。", + "allowLegacyShell": "允许未加密经典反弹 Shell(内网实验)", + "allowLegacyShellHint": "默认关闭。开启后 bash/nc 等裸 TCP 连接可登记会话,公网易被扫描器误连;生产环境请使用「生成 Beacon」加密上线。", "malleableProfile": "Malleable Profile", "malleableProfileHint": "可选;用于 HTTP/HTTPS Beacon 服务端响应头等流量伪装。修改后需停止并重新启动监听器才会生效。", "malleableProfileNone": "不使用", @@ -2813,10 +2820,22 @@ "infoFirstSeen": "首次上线", "infoLastCheckin": "上次心跳", "infoNote": "备注", + "infoNoteEmpty": "暂无备注", + "infoSectionIdentity": "身份信息", + "infoSectionSystem": "系统环境", + "infoSectionNetwork": "网络与信标", + "infoSectionTimeline": "时间线", + "infoSectionNote": "备注", "adminYes": "是", "adminNo": "否", "promptSleepSeconds": "Sleep 间隔(秒)", "promptJitterPercent": "抖动百分比(0–100)", + "sleepModalHint": "保存后将写入服务端并下发 sleep 任务;植入体在下次拉取任务后生效,同时后续心跳会同步该配置。", + "sleepModalTitle": "心跳配置", + "sleepModalCurrent": "当前 {{sec}} 秒 · 抖动 {{jitter}}%", + "sleepModalPreview": "预计间隔 {{min}} – {{max}} 秒", + "sleepModalPresets": "快捷", + "toastSleepInvalid": "Sleep 间隔至少为 1 秒", "toastSleepUpdated": "Sleep 设置已更新", "confirmExitSession": "向该会话发送退出指令?", "confirmDeleteSession": "从服务器删除此会话及其关联任务与文件记录?(不会向植入体发送退出;若需退出目标进程请使用「终止会话」。)", @@ -2834,7 +2853,25 @@ "termWaitFinish": "请等待当前命令执行完成", "termCtrlC": "当前版本暂不支持中断远程命令", "termQueued": "[命令已加入队列,将在当前任务完成后执行]", - "clearTerminal": "清屏" + "clearTerminal": "清屏", + "batchDelete": "批量删除", + "deleteFiltered": "删除筛选结果", + "selectAll": "全选", + "filterAllStatus": "全部状态", + "filterAllListeners": "全部监听器", + "filterSearchPlaceholder": "搜索主机名 / 用户 / IP", + "filterApply": "筛选", + "filterReset": "重置", + "filterSuspicious": "疑似误报", + "filterCount": "共 {{n}} 条,已选 {{selected}}", + "emptyFilter": "没有符合筛选条件的会话", + "listEmpty": "暂无会话", + "selectPromptTitle": "选择会话", + "selectPromptHint": "在左侧列表中点击一个会话,查看终端、文件与任务详情。", + "confirmBatchDelete": "确定删除选中的 {{n}} 个会话?关联任务与文件记录将一并清除。", + "confirmDeleteFiltered": "确定删除当前筛选结果中的全部 {{n}} 个会话?", + "toastSelectFirst": "请先勾选要删除的会话", + "toastBatchDeleted": "已删除 {{n}} 个会话" }, "tasks": { "title": "任务管理", @@ -2857,6 +2894,8 @@ "pending": "待处理", "emptyAll": "暂无任务", "emptySession": "该会话暂无任务", + "sessionTaskHistory": "任务历史", + "sessionTaskCount": "共 {{n}} 条", "colTask": "任务", "colSession": "会话", "colType": "类型", diff --git a/web/static/js/c2.js b/web/static/js/c2.js index c7849af6..7e36b0a1 100644 --- a/web/static/js/c2.js +++ b/web/static/js/c2.js @@ -21,6 +21,7 @@ profiles: [], selectedSessionId: null, selectedListenerId: null, + sessionFilter: { status: '', listener_id: '', search: '', suspicious: false }, eventSource: null, // xterm 相关 terminalInstance: null, @@ -146,6 +147,94 @@ return s.substring(0, maxLen - 1) + '\u2026'; } + function taskTypeCategory(type) { + const t = String(type || '').toLowerCase(); + if (t === 'shell' || t === 'exec') return 'shell'; + if (t === 'ls' || t === 'cd' || t === 'pwd' || t === 'download' || t === 'upload') return 'fs'; + if (t === 'sleep' || t === 'exit' || t === 'kill_proc') return 'control'; + return 'default'; + } + + function formatRelativeTime(value) { + const ms = value ? new Date(value).getTime() : 0; + if (!Number.isFinite(ms) || ms <= 0) return ''; + const diff = Date.now() - ms; + if (diff < 60000) return c2t('c2.common.justNow'); + if (diff < 3600000) return c2t('c2.common.minutesAgo', { n: Math.floor(diff / 60000) }); + if (diff < 86400000) return c2t('c2.common.hoursAgo', { n: Math.floor(diff / 3600000) }); + return formatTime(value); + } + + function sessionOsKey(os) { + const o = String(os || '').toLowerCase(); + if (o.includes('darwin') || o === 'macos' || o === 'mac') return 'darwin'; + if (o.includes('win')) return 'windows'; + if (o.includes('linux') || o.includes('unix')) return 'linux'; + return 'default'; + } + + function sessionOsAvatarLabel(os) { + const k = sessionOsKey(os); + if (k === 'darwin') return 'mac'; + if (k === 'windows') return 'win'; + if (k === 'linux') return 'nix'; + return String(os || '?').substring(0, 3).toLowerCase(); + } + + function sessionInfoRow(label, value, opts) { + const valueClasses = ['c2-session-info-dl__value']; + if (opts && opts.mono) valueClasses.push('is-mono'); + if (opts && opts.accent) valueClasses.push('is-accent'); + if (opts && opts.warn) valueClasses.push('is-warn'); + const rowCls = opts && opts.full ? ' c2-session-info-dl__row--full' : ''; + return ` +
+
${escapeHtml(label)}
+
${escapeHtml(value == null || value === '' ? '-' : String(value))}
+
`; + } + + function renderSessionInfoPanel(s, adminVal, sleepLine) { + const lastCheckin = formatTime(s.lastCheckIn); + const lastRel = formatRelativeTime(s.lastCheckIn); + const checkinDisplay = lastRel ? `${lastCheckin} (${lastRel})` : lastCheckin; + return ` +
+
+
${escapeHtml(c2t('c2.sessions.infoSectionIdentity'))}
+
+ ${sessionInfoRow(c2t('c2.sessions.infoSessionId'), s.id, { mono: true, accent: true, full: true })} + ${sessionInfoRow(c2t('c2.sessions.infoImplantUuid'), s.implantUuid, { mono: true, full: true })} + ${sessionInfoRow(c2t('c2.sessions.infoHostname'), s.hostname)} + ${sessionInfoRow(c2t('c2.sessions.infoUsername'), s.username)} +
+
+
+
${escapeHtml(c2t('c2.sessions.infoSectionSystem'))}
+
+ ${sessionInfoRow(c2t('c2.sessions.infoOs'), s.os)} + ${sessionInfoRow(c2t('c2.sessions.infoArch'), s.arch, { mono: true })} + ${sessionInfoRow(c2t('c2.sessions.infoPid'), s.pid, { mono: true })} + ${sessionInfoRow(c2t('c2.sessions.infoProcess'), s.processName || '-', { mono: true })} + ${sessionInfoRow(c2t('c2.sessions.infoAdmin'), adminVal, { warn: s.isAdmin, full: true })} +
+
+
+
${escapeHtml(c2t('c2.sessions.infoSectionNetwork'))}
+
+ ${sessionInfoRow(c2t('c2.sessions.infoInternalIp'), s.internalIp || '-', { mono: true, accent: true })} + ${sessionInfoRow(c2t('c2.sessions.infoSleep'), sleepLine)} + ${sessionInfoRow(c2t('c2.sessions.infoFirstSeen'), formatTime(s.firstSeenAt), { mono: true })} + ${sessionInfoRow(c2t('c2.sessions.infoLastCheckin'), checkinDisplay, { mono: true, accent: true })} +
+
+
+
${escapeHtml(c2t('c2.sessions.infoSectionNote'))}
+
${escapeHtml(s.note || c2t('c2.sessions.infoNoteEmpty'))}
+
+
`; + } + // ============================================================================ // 工具函数 // ============================================================================ @@ -325,8 +414,11 @@ C2.loadListeners(); break; case 'c2-sessions': - C2.loadSessions(); - C2.ensureListenersLoaded(); + C2.renderSessionToolbar(); + C2.ensureListenersLoaded().then(function() { + C2.renderSessionToolbar(); + C2.loadSessions(); + }); break; case 'c2-tasks': C2.loadTasks(); @@ -473,6 +565,16 @@ } }; + C2.getListenerConfig = function(l) { + if (!l) return {}; + try { + var raw = l.configJson != null ? l.configJson : '{}'; + return typeof raw === 'string' ? JSON.parse(raw || '{}') : (raw || {}); + } catch (e) { + return {}; + } + }; + C2.showCreateListenerModal = function() { const modal = document.getElementById('c2-modal'); const content = document.getElementById('c2-modal-content'); @@ -541,6 +643,13 @@ + + ${lt === 'tcp_reverse' ? ` +
+ +
${escapeHtml(c2t('c2.listeners.allowLegacyShellHint'))}
+
` : ''}