Client v2.4.0 - UI enhancements. Dark Mode, task queuing, cancel/modify queued tasks, change agent & dashboard background images, change agent font+fontSize+fontColor, zoom, dashboard functionality enhancements, Agent command history option to prevent laggy agent window statups, agent window now shows loading when loading command history
@@ -0,0 +1,546 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#1e1e1e">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Fira Code', monospace;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #1e1e1e;
|
||||
color: #c5c5c5;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.settings-container {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
background-color: #2d2d2d;
|
||||
border: 1px solid #555;
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
|
||||
height: fit-content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
border-bottom: 1px solid #555;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.settings-title {
|
||||
color: #e0e0e0;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.settings-subtitle {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
display: block;
|
||||
color: #e0e0e0;
|
||||
font-size: 13px;
|
||||
margin-bottom: 6px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.control-group select,
|
||||
.control-group input[type="range"],
|
||||
.control-group input[type="color"] {
|
||||
width: 100%;
|
||||
background-color: #3a3a3a;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
color: #e0e0e0;
|
||||
padding: 8px;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.control-group select {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 6px;
|
||||
background: linear-gradient(to right, #666, #333);
|
||||
outline: none;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: #999;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 8px rgba(153, 153, 153, 0.6);
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: #999;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
box-shadow: 0 0 8px rgba(153, 153, 153, 0.6);
|
||||
}
|
||||
|
||||
.value-display {
|
||||
text-align: center;
|
||||
color: #888;
|
||||
font-size: 11px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.zoom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
background-color: #3a3a3a;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.zoom-btn {
|
||||
background: linear-gradient(135deg, #555, #666);
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.zoom-btn:hover {
|
||||
background: linear-gradient(135deg, #666, #777);
|
||||
box-shadow: 0 0 12px rgba(153, 153, 153, 0.4);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.zoom-btn:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.zoom-level {
|
||||
color: #e0e0e0;
|
||||
font-weight: bold;
|
||||
min-width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.background-image-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.background-image-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
background-color: #3a3a3a;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.background-image-preview span {
|
||||
flex: 1;
|
||||
color: #888;
|
||||
font-size: 11px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.buttons-container {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 25px;
|
||||
border-top: 1px solid #555;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.btn:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #666, #777);
|
||||
color: #fff;
|
||||
border: 1px solid #555;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, #777, #888);
|
||||
box-shadow: 0 6px 12px rgba(153, 153, 153, 0.4);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: linear-gradient(135deg, #4a4a4a, #5a5a5a);
|
||||
color: #fff;
|
||||
border: 1px solid #333;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: linear-gradient(135deg, #5a5a5a, #6a6a6a);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: linear-gradient(135deg, #333, #444);
|
||||
color: #ccc;
|
||||
border: 1px solid #666;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: linear-gradient(135deg, #444, #555);
|
||||
color: #fff;
|
||||
box-shadow: 0 6px 12px rgba(153, 153, 153, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Smaller buttons for background image controls */
|
||||
.background-image-preview .btn,
|
||||
.background-image-controls .btn {
|
||||
padding: 6px 12px;
|
||||
font-size: 10px;
|
||||
min-width: 80px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="settings-container">
|
||||
<div class="control-group">
|
||||
<label for="fontFamilyControl">Font Family</label>
|
||||
<select id="fontFamilyControl" onchange="updatePreview()">
|
||||
<option value="'Fira Code', monospace">Fira Code</option>
|
||||
<option value="'Courier New', monospace">Courier New</option>
|
||||
<option value="'Monaco', monospace">Monaco</option>
|
||||
<option value="'Menlo', monospace">Menlo</option>
|
||||
<option value="'Consolas', monospace">Consolas</option>
|
||||
<option value="'DejaVu Sans Mono', monospace">DejaVu Sans Mono</option>
|
||||
<option value="'Liberation Mono', monospace">Liberation Mono</option>
|
||||
<option value="'Source Code Pro', monospace">Source Code Pro</option>
|
||||
<option value="'Ubuntu Mono', monospace">Ubuntu Mono</option>
|
||||
<option value="'Roboto Mono', monospace">Roboto Mono</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="fontSizeControl">Font Size</label>
|
||||
<input type="range" id="fontSizeControl" min="8" max="28" value="16" onchange="updatePreview()">
|
||||
<div class="value-display" id="fontSizeValue">16px</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="fontColorControl">Font Color</label>
|
||||
<input type="color" id="fontColorControl" value="#c5c5c5" onchange="updatePreview()">
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="agentBackgroundImageControl">Agent Background Image</label>
|
||||
<div class="background-image-controls">
|
||||
<button class="btn btn-secondary" onclick="selectAgentBackgroundImage()">Browse Images...</button>
|
||||
<div class="background-image-preview" id="agentBackgroundImagePreview">
|
||||
<span id="agentBackgroundImagePath">No image selected</span>
|
||||
<button class="btn btn-danger" onclick="clearAgentBackgroundImage()" id="clearAgentImageBtn" style="display: none;">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="historyLengthControl">Command History Length</label>
|
||||
<input type="range" id="historyLengthControl" min="10" max="1010" value="100" step="10" onchange="updatePreview()">
|
||||
<div class="value-display" id="historyLengthValue">100 lines from log file on startup</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="zoomControl">Zoom Level</label>
|
||||
<div class="zoom-controls">
|
||||
<button class="zoom-btn" onclick="adjustZoom(-0.1)">-</button>
|
||||
<span class="zoom-level" id="zoomLevel">100%</span>
|
||||
<button class="zoom-btn" onclick="adjustZoom(0.1)">+</button>
|
||||
</div>
|
||||
<div class="value-display">Use Ctrl++ and Ctrl+- for quick zoom adjustment</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons-container">
|
||||
<button class="btn btn-secondary" onclick="resetToDefaults()">Reset to Defaults</button>
|
||||
<button class="btn btn-primary" onclick="applySettings()">Apply Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
// Settings variables
|
||||
let currentZoom = 1.0;
|
||||
let currentSettings = {
|
||||
fontFamily: "'Fira Code', monospace",
|
||||
fontSize: 16,
|
||||
fontColor: "#c5c5c5",
|
||||
zoom: 1.0,
|
||||
agentBackgroundImage: null,
|
||||
historyLength: 100
|
||||
};
|
||||
|
||||
// Initialize settings when window loads
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
loadSettings();
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
function adjustZoom(delta) {
|
||||
currentZoom = Math.max(0.5, Math.min(2.0, currentZoom + delta));
|
||||
document.getElementById('zoomLevel').textContent = Math.round(currentZoom * 100) + '%';
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
function updatePreview() {
|
||||
const fontSize = document.getElementById('fontSizeControl').value;
|
||||
const fontFamily = document.getElementById('fontFamilyControl').value;
|
||||
const fontColor = document.getElementById('fontColorControl').value;
|
||||
const historyLength = document.getElementById('historyLengthControl').value;
|
||||
|
||||
// Update display
|
||||
document.getElementById('fontSizeValue').textContent = fontSize + 'px';
|
||||
|
||||
// Handle infinite history length display
|
||||
if (parseInt(historyLength) >= 1010) {
|
||||
document.getElementById('historyLengthValue').textContent = '∞ (all lines) from log file on startup';
|
||||
} else {
|
||||
document.getElementById('historyLengthValue').textContent = historyLength + ' lines from log file on startup';
|
||||
}
|
||||
|
||||
// Apply preview to this window
|
||||
document.body.style.fontFamily = fontFamily;
|
||||
document.body.style.fontSize = fontSize + 'px';
|
||||
document.body.style.color = fontColor;
|
||||
|
||||
// Update current settings
|
||||
currentSettings = {
|
||||
fontFamily: fontFamily,
|
||||
fontSize: parseInt(fontSize),
|
||||
fontColor: fontColor,
|
||||
zoom: currentZoom,
|
||||
agentBackgroundImage: currentSettings.agentBackgroundImage,
|
||||
historyLength: parseInt(historyLength) >= 1010 ? -1 : parseInt(historyLength) // Use -1 for infinite
|
||||
};
|
||||
}
|
||||
|
||||
function applySettings() {
|
||||
console.log('=== APPLY SETTINGS CLICKED ===');
|
||||
console.log('Current settings to apply:', currentSettings);
|
||||
|
||||
// Send settings to parent agent window (this will also save to file)
|
||||
console.log('Sending apply-agent-settings IPC message...');
|
||||
ipcRenderer.send('apply-agent-settings', currentSettings);
|
||||
console.log('IPC message sent successfully');
|
||||
|
||||
// If history length changed, trigger reload of logs in agent window
|
||||
console.log('Sending reload-history IPC message...');
|
||||
ipcRenderer.send('reload-agent-history', currentSettings.historyLength);
|
||||
|
||||
// Close window
|
||||
console.log('Closing settings window...');
|
||||
closeWindow();
|
||||
}
|
||||
|
||||
function resetToDefaults() {
|
||||
currentZoom = 1.0;
|
||||
document.getElementById('fontSizeControl').value = 16;
|
||||
document.getElementById('fontFamilyControl').value = "'Fira Code', monospace";
|
||||
document.getElementById('fontColorControl').value = "#c5c5c5";
|
||||
document.getElementById('historyLengthControl').value = 100;
|
||||
document.getElementById('zoomLevel').textContent = '100%';
|
||||
|
||||
// Reset agent background image
|
||||
currentSettings.agentBackgroundImage = null;
|
||||
updateAgentBackgroundImageDisplay();
|
||||
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
// Load settings from customize.js file
|
||||
ipcRenderer.invoke('get-customize-settings').then(settings => {
|
||||
if (settings) {
|
||||
console.log('Loading customize settings from file:', settings);
|
||||
|
||||
if (settings.zoom) {
|
||||
currentZoom = settings.zoom;
|
||||
document.getElementById('zoomLevel').textContent = Math.round(currentZoom * 100) + '%';
|
||||
}
|
||||
|
||||
if (settings.fontSize) {
|
||||
document.getElementById('fontSizeControl').value = settings.fontSize;
|
||||
}
|
||||
|
||||
if (settings.fontFamily) {
|
||||
document.getElementById('fontFamilyControl').value = settings.fontFamily;
|
||||
}
|
||||
|
||||
if (settings.fontColor) {
|
||||
document.getElementById('fontColorControl').value = settings.fontColor;
|
||||
}
|
||||
|
||||
if (settings.agentBackgroundImage) {
|
||||
currentSettings.agentBackgroundImage = settings.agentBackgroundImage;
|
||||
}
|
||||
|
||||
if (settings.historyLength !== undefined) {
|
||||
// Handle infinite case (-1) by setting slider to max value
|
||||
const sliderValue = settings.historyLength === -1 ? 1010 : settings.historyLength;
|
||||
document.getElementById('historyLengthControl').value = sliderValue;
|
||||
}
|
||||
|
||||
currentSettings = settings;
|
||||
updateAgentBackgroundImageDisplay();
|
||||
updatePreview();
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Error loading customize settings:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function selectAgentBackgroundImage() {
|
||||
console.log('=== SELECT AGENT BACKGROUND IMAGE CLICKED ===');
|
||||
// Send IPC message to open file dialog
|
||||
ipcRenderer.invoke('select-background-image').then(imagePath => {
|
||||
if (imagePath) {
|
||||
console.log('Selected agent background image path:', imagePath);
|
||||
currentSettings.agentBackgroundImage = imagePath;
|
||||
updateAgentBackgroundImageDisplay();
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Error selecting agent background image:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function clearAgentBackgroundImage() {
|
||||
console.log('=== CLEAR AGENT BACKGROUND IMAGE CLICKED ===');
|
||||
currentSettings.agentBackgroundImage = null;
|
||||
updateAgentBackgroundImageDisplay();
|
||||
}
|
||||
|
||||
function updateAgentBackgroundImageDisplay() {
|
||||
const pathSpan = document.getElementById('agentBackgroundImagePath');
|
||||
const clearBtn = document.getElementById('clearAgentImageBtn');
|
||||
|
||||
if (currentSettings.agentBackgroundImage) {
|
||||
// Show just the filename, not the full path
|
||||
const filename = currentSettings.agentBackgroundImage.split(/[\\/]/).pop();
|
||||
pathSpan.textContent = filename;
|
||||
clearBtn.style.display = 'inline-block';
|
||||
} else {
|
||||
pathSpan.textContent = 'No image selected';
|
||||
clearBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function closeWindow() {
|
||||
ipcRenderer.send('close-agent-settings-window');
|
||||
}
|
||||
|
||||
// Keyboard shortcuts for zoom
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'c') {
|
||||
ipcRenderer.send('copy');
|
||||
event.preventDefault();
|
||||
}
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
|
||||
ipcRenderer.send('paste');
|
||||
event.preventDefault();
|
||||
}
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'x') {
|
||||
ipcRenderer.send('cut');
|
||||
event.preventDefault();
|
||||
}
|
||||
// Ctrl++ for zoom in
|
||||
if (event.ctrlKey && (event.key === '+' || event.key === '=')) {
|
||||
event.preventDefault();
|
||||
adjustZoom(0.1);
|
||||
}
|
||||
|
||||
// Ctrl+- for zoom out
|
||||
if (event.ctrlKey && event.key === '-') {
|
||||
event.preventDefault();
|
||||
adjustZoom(-0.1);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,19 +2,25 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#1e1e1e">
|
||||
<meta name="msapplication-navbutton-color" content="#1e1e1e">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<title>Agent Terminal</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Fira Code', monospace;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
padding: 20px 20px 20px 20px;
|
||||
background-color: #1e1e1e;
|
||||
color: #c5c5c5;
|
||||
height: calc(100vh - 40px); /* Adjust height to add bottom margin */
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden; /* Prevent body from scrolling */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
@@ -22,15 +28,15 @@
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
.console {
|
||||
background-image: url('./assets/images/agent.png');
|
||||
/* background-image: url('./assets/images/agent.png'); */
|
||||
background-size: contain; /* Fit the image within the element */
|
||||
background-repeat: no-repeat; /* Prevent repeating */
|
||||
background-position: center; /* Center the image */
|
||||
height: 100vh; /* Full viewport height */
|
||||
width: 100%;
|
||||
background-color: #191919;
|
||||
background-color: transparent;
|
||||
color: #ffffff;
|
||||
padding: 10px;
|
||||
padding: 10px 10px 10px 10px;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
border-radius: 4px;
|
||||
@@ -40,6 +46,22 @@
|
||||
position: relative;
|
||||
white-space: pre-wrap;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.console::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
}
|
||||
.console::-webkit-scrollbar-track {
|
||||
background: #1a1a1a;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.console::-webkit-scrollbar-thumb {
|
||||
background: #333333;
|
||||
border-radius: 6px;
|
||||
border: 2px solid #1a1a1a;
|
||||
}
|
||||
.console::-webkit-scrollbar-thumb:hover {
|
||||
background: #555555;
|
||||
}
|
||||
.input {
|
||||
display: flex;
|
||||
@@ -87,88 +109,95 @@
|
||||
color: #ffffff;
|
||||
}
|
||||
table.vampire-matrix {
|
||||
width: 100%; /* Make the table consume the entire window width */
|
||||
border-collapse: collapse;
|
||||
background-color: #1c1c1c;
|
||||
box-shadow: 0 0 10px rgba(0, 255, 64, 0.5);
|
||||
}
|
||||
width: 100%; /* Make the table consume the entire window width */
|
||||
border-collapse: collapse;
|
||||
background-color: #030303;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
table.vampire-matrix th,
|
||||
table.vampire-matrix td {
|
||||
border: 1px solid #333;
|
||||
padding: 10px 15px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
word-wrap: break-word; /* Ensure content wraps within the cell */
|
||||
}
|
||||
table.vampire-matrix th,
|
||||
table.vampire-matrix td {
|
||||
border: 1px solid #333;
|
||||
padding: 10px 15px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
word-wrap: break-word; /* Ensure content wraps within the cell */
|
||||
}
|
||||
|
||||
table.vampire-matrix thead th {
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
table.vampire-matrix thead th {
|
||||
background-color: #0a0a0a;
|
||||
color: #fff;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
table.vampire-matrix tbody tr {
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
table.vampire-matrix tbody tr {
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
table.vampire-matrix tbody tr:hover {
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
table.vampire-matrix tbody tr:hover {
|
||||
background-color: #080808;
|
||||
}
|
||||
|
||||
table.vampire-matrix tbody tr:nth-child(even) {
|
||||
background-color: #232323;
|
||||
}
|
||||
table.vampire-matrix tbody tr:nth-child(even) {
|
||||
background-color: #050505;
|
||||
}
|
||||
|
||||
table.vampire-matrix tbody tr:nth-child(odd) {
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
table.vampire-matrix tbody tr:nth-child(odd) {
|
||||
background-color: #020202;
|
||||
}
|
||||
|
||||
table.vampire-matrix td {
|
||||
color: #fff; /* Change text color to white */
|
||||
}
|
||||
table.vampire-matrix td {
|
||||
color: #fff; /* Change text color to white */
|
||||
}
|
||||
|
||||
table.vampire-matrix th {
|
||||
border-bottom: 2px solid #444;
|
||||
}
|
||||
table.vampire-matrix th {
|
||||
border-bottom: 2px solid #444;
|
||||
}
|
||||
|
||||
table.vampire-matrix tbody td:first-child {
|
||||
border-left: 2px solid #444;
|
||||
}
|
||||
table.vampire-matrix tbody td:first-child {
|
||||
border-left: 2px solid #444;
|
||||
}
|
||||
|
||||
table.vampire-matrix tbody td:last-child {
|
||||
border-right: 2px solid #444;
|
||||
}
|
||||
table.vampire-matrix tbody td:last-child {
|
||||
border-right: 2px solid #444;
|
||||
}
|
||||
|
||||
tr {
|
||||
height: 20px;
|
||||
max-height: 20px; /* Set your desired max-height here */
|
||||
overflow: hidden;
|
||||
}
|
||||
tr > td {
|
||||
max-height: inherit; /* Apply max height to each cell */
|
||||
overflow: hidden; /* Ensure content overflow is hidden in each cell */
|
||||
}
|
||||
tr {
|
||||
height: 20px;
|
||||
max-height: 20px; /* Set your desired max-height here */
|
||||
overflow: hidden;
|
||||
}
|
||||
tr > td {
|
||||
max-height: inherit; /* Apply max height to each cell */
|
||||
overflow: hidden; /* Ensure content overflow is hidden in each cell */
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #0a0a0a;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- <h1>Agent Terminal</h1> -->
|
||||
|
||||
<div class="agentstatus" id="agentstatus">
|
||||
<table id="agentTable" class="vampire-matrix" height="20px">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Agent ID</td>
|
||||
<td>Hostname</td>
|
||||
<td>Username</td>
|
||||
<td>Process</td>
|
||||
<td>PID</td>
|
||||
<td>IP</td>
|
||||
<td>Arch</td>
|
||||
<td>Platform</td>
|
||||
<td>Checkin</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table id="agentTable" class="vampire-matrix" height="20px">
|
||||
<tbody>
|
||||
<tr id="agentDataRow">
|
||||
<td>Loading..</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="console" id="consoleOutput"></div>
|
||||
<div class="input">
|
||||
@@ -178,5 +207,143 @@ tr > td {
|
||||
<div id="commandDropdown"></div>
|
||||
</div>
|
||||
<script src="agent.js"></script>
|
||||
<script>
|
||||
// Function to apply font settings to this agent window
|
||||
function applyFontSettings(settings) {
|
||||
console.log('=== APPLYING FONT SETTINGS ===');
|
||||
console.log('Settings received:', settings);
|
||||
|
||||
const elementsToStyle = [
|
||||
document.body,
|
||||
document.getElementById('consoleOutput'),
|
||||
document.getElementById('consoleInput'),
|
||||
...document.querySelectorAll('.vampire-matrix td'),
|
||||
...document.querySelectorAll('.vampire-matrix th'),
|
||||
...document.querySelectorAll('.vampire-matrix')
|
||||
];
|
||||
|
||||
console.log('Elements to style:', elementsToStyle.length);
|
||||
|
||||
elementsToStyle.forEach((element, index) => {
|
||||
if (element) {
|
||||
console.log(`Styling element ${index}:`, element.tagName, element.className);
|
||||
if (settings.fontFamily) {
|
||||
element.style.fontFamily = settings.fontFamily;
|
||||
console.log(`Set fontFamily to: ${settings.fontFamily}`);
|
||||
}
|
||||
if (settings.fontSize) {
|
||||
element.style.fontSize = settings.fontSize + 'px';
|
||||
console.log(`Set fontSize to: ${settings.fontSize}px`);
|
||||
}
|
||||
if (settings.fontColor) {
|
||||
element.style.color = settings.fontColor;
|
||||
console.log(`Set fontColor to: ${settings.fontColor}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Apply zoom if specified
|
||||
if (settings.zoom && settings.zoom !== 1.0) {
|
||||
console.log('Applying zoom:', settings.zoom);
|
||||
const elementsToZoom = [
|
||||
document.getElementById('consoleOutput'),
|
||||
document.querySelector('.agentstatus')
|
||||
];
|
||||
|
||||
elementsToZoom.forEach(element => {
|
||||
if (element) {
|
||||
element.style.transform = `scale(${settings.zoom})`;
|
||||
element.style.transformOrigin = 'top left';
|
||||
console.log('Applied zoom to:', element.id || element.className);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Apply agent background image if specified
|
||||
if (settings.agentBackgroundImage) {
|
||||
console.log('Applying agent background image:', settings.agentBackgroundImage);
|
||||
document.body.style.backgroundImage = `url('${settings.agentBackgroundImage}')`;
|
||||
document.body.style.backgroundSize = 'cover';
|
||||
document.body.style.backgroundRepeat = 'no-repeat';
|
||||
document.body.style.backgroundPosition = 'center';
|
||||
document.body.style.backgroundAttachment = 'fixed';
|
||||
} else {
|
||||
console.log('Clearing agent background image');
|
||||
document.body.style.backgroundImage = '';
|
||||
document.body.style.backgroundSize = '';
|
||||
document.body.style.backgroundRepeat = '';
|
||||
document.body.style.backgroundPosition = '';
|
||||
document.body.style.backgroundAttachment = '';
|
||||
}
|
||||
|
||||
console.log('=== FONT SETTINGS APPLIED ===');
|
||||
}
|
||||
|
||||
// Handle font settings from separate settings window
|
||||
ipcRenderer.on('apply-font-settings', (event, settings) => {
|
||||
console.log('=== RECEIVED APPLY-FONT-SETTINGS IPC ===');
|
||||
console.log('Event:', event);
|
||||
console.log('Settings:', settings);
|
||||
applyFontSettings(settings);
|
||||
});
|
||||
|
||||
// Load saved settings on page load
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
// Load settings from customize.js file
|
||||
ipcRenderer.invoke('get-customize-settings').then(settings => {
|
||||
if (settings) {
|
||||
console.log('Loading customize settings from file:', settings);
|
||||
applyFontSettings(settings);
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Error loading customize settings:', error);
|
||||
});
|
||||
});
|
||||
|
||||
// Add test keyboard shortcut (F5 to test applying settings)
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'F5') {
|
||||
event.preventDefault();
|
||||
console.log('=== F5 PRESSED - TESTING SETTINGS ===');
|
||||
const testSettings = {
|
||||
fontFamily: "'Courier New', monospace",
|
||||
fontSize: 20,
|
||||
fontColor: "#ff0000",
|
||||
zoom: 1.2
|
||||
};
|
||||
console.log('Applying test settings:', testSettings);
|
||||
applyFontSettings(testSettings);
|
||||
}
|
||||
|
||||
// Zoom hotkeys for agent window
|
||||
if (event.ctrlKey && (event.key === '+' || event.key === '=')) {
|
||||
event.preventDefault();
|
||||
// Get current settings and increase zoom
|
||||
ipcRenderer.invoke('get-customize-settings').then(settings => {
|
||||
if (settings) {
|
||||
const newZoom = Math.min(2.0, (settings.zoom || 1.0) + 0.1);
|
||||
const updatedSettings = { ...settings, zoom: newZoom };
|
||||
applyFontSettings(updatedSettings);
|
||||
// Save updated settings
|
||||
ipcRenderer.send('apply-agent-settings', updatedSettings);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (event.ctrlKey && event.key === '-') {
|
||||
event.preventDefault();
|
||||
// Get current settings and decrease zoom
|
||||
ipcRenderer.invoke('get-customize-settings').then(settings => {
|
||||
if (settings) {
|
||||
const newZoom = Math.max(0.5, (settings.zoom || 1.0) - 0.1);
|
||||
const updatedSettings = { ...settings, zoom: newZoom };
|
||||
applyFontSettings(updatedSettings);
|
||||
// Save updated settings
|
||||
ipcRenderer.send('apply-agent-settings', updatedSettings);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -31,7 +31,7 @@ const commands = [
|
||||
'mv', 'sleep', 'cp', 'load',
|
||||
'upload', 'download', 'scexec', 'scan',
|
||||
'exit', 'assembly', 'help', 'dns','cd',
|
||||
'bof'
|
||||
'bof', 'link', 'unlink'
|
||||
];
|
||||
|
||||
global.commandHistory = [];
|
||||
@@ -86,10 +86,6 @@ const commandDetails = [
|
||||
{ name: "download", help: "Download a file from remote agent box to local operator box /\r\n\tdownload [remote_path]\r\n\tdownload /agent/src/file\r\n\t- Get from View > Downloads\r\n" },
|
||||
{ name: "link", help: "Peer-to-peer connect to a Loki TCP agent\r\n\tlink <hostname> <port>\r\n\tlink localhost 3000\r\n" },
|
||||
{ name: "unlink", help: "Disconnect from a Loki TCP agent\r\n\tunlink <hostname>\r\n\tunlink localhost\r\n" },
|
||||
//{ name: "socks", help: "socks <url> \r\n" +
|
||||
// " - Connect to a SOCKS5 server.\r\n" +
|
||||
// " Examples:\r\n" +
|
||||
// " socks xyz.cloudfront.net:443/ws/agent/09f21e3b6e247b8a6a6ee2472f5\r\n" },
|
||||
{ name: "scan", help: "scan <host> [-p<ports>] \r\n" +
|
||||
" - The target host or CIDR range to scan.\r\n" +
|
||||
" - Options:\r\n" +
|
||||
@@ -201,6 +197,7 @@ class Task {
|
||||
this.uploadChannel = 'u-' + Math.random().toString(36).substring(2, 14);
|
||||
this.command = command;
|
||||
this.taskid = Math.random().toString(36).substring(2, 14);
|
||||
this.status = 'starting';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,21 +439,26 @@ async function printToConsole(message, logToFileFlag = true) {
|
||||
}
|
||||
}
|
||||
|
||||
function loadPreviousLogs() {
|
||||
function loadPreviousLogs(customHistoryLength = null) {
|
||||
try {
|
||||
let agent_log_name = `${pid_log}-${user_log}-${agentid_log}.log`;
|
||||
const logFile = path.join(directories.logDir, agent_log_name);
|
||||
log(`logfile : ${logFile}`);
|
||||
|
||||
if (fs.existsSync(logFile)) {
|
||||
log(`Loading previous logs from ${logFile}`);
|
||||
const logs = fs.readFileSync(logFile, 'utf8');
|
||||
logs.split('\n').forEach(line => {
|
||||
let cleanedLine = line.replace(/^\[.*?\]\s*\[.*?\]\s*/, '').trim();
|
||||
if (cleanedLine) {
|
||||
printToConsole(cleanedLine, false);
|
||||
}
|
||||
});
|
||||
if (customHistoryLength !== null) {
|
||||
// Use provided history length (for immediate reload)
|
||||
loadLogsWithLength(logFile, customHistoryLength);
|
||||
} else {
|
||||
// Get history length setting (default to 100)
|
||||
ipcRenderer.invoke('get-customize-settings').then(settings => {
|
||||
const historyLength = settings?.historyLength || 100;
|
||||
loadLogsWithLength(logFile, historyLength);
|
||||
}).catch(error => {
|
||||
log(`Error getting history length setting, using default: ${error}`);
|
||||
loadLogsWithLength(logFile, 100);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
log(`No existing log file found for ${logFile}`);
|
||||
}
|
||||
@@ -465,6 +467,34 @@ function loadPreviousLogs() {
|
||||
}
|
||||
}
|
||||
|
||||
function loadLogsWithLength(logFile, historyLength) {
|
||||
try {
|
||||
const logs = fs.readFileSync(logFile, 'utf8');
|
||||
const allLines = logs.split('\n');
|
||||
const nonEmptyLines = allLines.filter(line => line.trim().length > 0);
|
||||
|
||||
let linesToLoad;
|
||||
if (historyLength === -1) {
|
||||
// Load all lines (infinite)
|
||||
linesToLoad = nonEmptyLines;
|
||||
log(`Loading all ${nonEmptyLines.length} lines from ${logFile}`);
|
||||
} else {
|
||||
// Load last N lines
|
||||
linesToLoad = nonEmptyLines.slice(-historyLength);
|
||||
log(`Loading last ${linesToLoad.length} of ${nonEmptyLines.length} lines from ${logFile}`);
|
||||
}
|
||||
|
||||
linesToLoad.forEach(line => {
|
||||
let cleanedLine = line.replace(/^\[.*?\]\s*\[.*?\]\s*/, '').trim();
|
||||
if (cleanedLine) {
|
||||
printToConsole(cleanedLine, false);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
log(`Error in loadLogsWithLength: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function clearConsole() {
|
||||
const consoleOutput = document.getElementById('consoleOutput');
|
||||
consoleOutput.innerHTML = '';
|
||||
@@ -539,16 +569,37 @@ window.addEventListener('DOMContentLoaded', async () => {
|
||||
containerName = agent_object.container;
|
||||
containerKey = agent_object.aes;
|
||||
containerBlob = agent_object.blobs;
|
||||
//agent = JSON.parse(agent_object);
|
||||
global.agent = agent_object;
|
||||
|
||||
document.title = `${agent_object.agentid.toUpperCase()} | ${agent_object.hostname.toUpperCase()} | ${agent_object.username.toUpperCase()} | ${agent_object.IP}`;
|
||||
document.title = "Loki Agent";
|
||||
|
||||
// Immediately populate the agent table with the received data
|
||||
populateAgentTable(agent_object);
|
||||
|
||||
// Enable input immediately since we have agent data
|
||||
global.inputload = true;
|
||||
window_agentid = agent_object.agentid;
|
||||
|
||||
// Set up logging variables
|
||||
agentid_log = agent_object.agentid || '';
|
||||
user_log = agent_object.username || '';
|
||||
pid_log = agent_object.PID || '';
|
||||
|
||||
// Load command history
|
||||
if (global.historyload === false && agent_object.hostname) {
|
||||
log(`Loading previous command history for ${agent_object.hostname}`);
|
||||
global.historyload = true;
|
||||
loadCommandHistory(agent_object.hostname);
|
||||
loadPreviousLogs();
|
||||
}
|
||||
|
||||
const input = document.getElementById('consoleInput');
|
||||
input.focus();
|
||||
input.selectionStart = input.selectionEnd = input.value.length;
|
||||
|
||||
log(`agent.js | container-data | Successfully initialized agent window for ${agent_object.agentid}`);
|
||||
} catch (error) {
|
||||
log(error);
|
||||
log(`container-data error: ${error} ${error.stack}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -562,6 +613,7 @@ window.addEventListener('DOMContentLoaded', async () => {
|
||||
|
||||
ipcRenderer.on('command-result', (event, result) => {
|
||||
log(`[AGENT][IPC] command-result : ${result}`);
|
||||
log(`[AGENT][IPC] command-result : ${JSON.stringify(result)}`);
|
||||
printToConsole(result);
|
||||
});
|
||||
|
||||
@@ -611,6 +663,16 @@ window.addEventListener('DOMContentLoaded', async () => {
|
||||
ipcRenderer.send('cut');
|
||||
event.preventDefault();
|
||||
}
|
||||
// Ctrl++ for zoom in
|
||||
if (event.ctrlKey && (event.key === '+' || event.key === '=')) {
|
||||
event.preventDefault();
|
||||
adjustZoom(0.1);
|
||||
}
|
||||
// Ctrl+- for zoom out
|
||||
if (event.ctrlKey && event.key === '-') {
|
||||
event.preventDefault();
|
||||
adjustZoom(-0.1);
|
||||
}
|
||||
});
|
||||
|
||||
// Send message to main process to add test command menu item
|
||||
@@ -620,50 +682,116 @@ window.addEventListener('DOMContentLoaded', async () => {
|
||||
ipcRenderer.on('execute-test-command', () => {
|
||||
executeCommandTests();
|
||||
});
|
||||
|
||||
// Listen for history reload from settings window
|
||||
ipcRenderer.on('reload-history', (event, historyLength) => {
|
||||
log(`=== RECEIVED RELOAD-HISTORY IPC ===`);
|
||||
log(`New history length: ${historyLength}`);
|
||||
|
||||
// Clear the console and reload with new history length
|
||||
clearConsole();
|
||||
loadPreviousLogs(historyLength);
|
||||
});
|
||||
});
|
||||
|
||||
function populateAgentTable(agentData) {
|
||||
try {
|
||||
const agentDataRow = document.getElementById('agentDataRow');
|
||||
if (!agentDataRow) {
|
||||
log('populateAgentTable: agentDataRow element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!agentData) {
|
||||
log('populateAgentTable: No agent data provided');
|
||||
return;
|
||||
}
|
||||
|
||||
log(`populateAgentTable: Populating table with agent data:`, {
|
||||
agentid: agentData.agentid,
|
||||
hostname: agentData.hostname,
|
||||
username: agentData.username,
|
||||
platform: agentData.platform
|
||||
});
|
||||
|
||||
// Determine platform name
|
||||
let platformName = agentData.platform || 'Unknown';
|
||||
if (agentData.platform === "darwin") {
|
||||
platformName = "macOS";
|
||||
} else if (agentData.platform === "win32") {
|
||||
platformName = "Windows";
|
||||
} else if (agentData.platform === "linux") {
|
||||
platformName = "Linux";
|
||||
}
|
||||
|
||||
// Process filename from full path
|
||||
let fileName = agentData.Process || '';
|
||||
if (fileName) {
|
||||
fileName = fileName.trim();
|
||||
if (fileName.includes("\\") && !fileName.includes("/")) {
|
||||
fileName = path.win32.basename(fileName);
|
||||
} else if (fileName.includes("/") && !fileName.includes("\\")) {
|
||||
fileName = path.posix.basename(fileName);
|
||||
} else {
|
||||
fileName = fileName.split(/[/\\]/).pop();
|
||||
}
|
||||
}
|
||||
|
||||
// Update table cells with defensive checks
|
||||
if (agentDataRow.cells.length >= 9) {
|
||||
agentDataRow.cells[0].textContent = agentData.agentid || '-';
|
||||
agentDataRow.cells[1].textContent = agentData.hostname || '-';
|
||||
agentDataRow.cells[2].textContent = agentData.username || '-';
|
||||
agentDataRow.cells[3].textContent = fileName || '-';
|
||||
agentDataRow.cells[4].textContent = agentData.PID || '-';
|
||||
agentDataRow.cells[5].textContent = agentData.IP || '-';
|
||||
agentDataRow.cells[6].textContent = agentData.arch || '-';
|
||||
agentDataRow.cells[7].textContent = platformName || '-';
|
||||
agentDataRow.cells[8].textContent = agentData.checkin ? timeDifference(agentData.checkin) : 'Just connected';
|
||||
} else {
|
||||
log(`populateAgentTable: Table row doesn't have enough cells: ${agentDataRow.cells.length}`);
|
||||
}
|
||||
|
||||
// Set logging variables
|
||||
agentid_log = agentData.agentid || '';
|
||||
user_log = agentData.username || '';
|
||||
pid_log = agentData.PID || '';
|
||||
|
||||
log(`populateAgentTable: Successfully populated table for agent ${agentData.agentid}`);
|
||||
} catch (error) {
|
||||
log(`populateAgentTable error: ${error} ${error.stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTable() {
|
||||
try {
|
||||
const agentTable = document.getElementById('agentTable').getElementsByTagName('tbody')[0];
|
||||
if (!global.agent || !global.agent.agentid) {
|
||||
log('updateTable: No agent data available yet');
|
||||
return;
|
||||
}
|
||||
|
||||
let agentid = global.agent.agentid;
|
||||
window_agentid = agentid;
|
||||
let agentcheckin = await ipcRenderer.invoke('get-agent-checkin', agentid);
|
||||
|
||||
if (agentcheckin) {
|
||||
//log(`agentcheckin : ${agentcheckin}`);
|
||||
agentcheckin = JSON.parse(agentcheckin);
|
||||
|
||||
const filePath = agentcheckin.Process.trim();
|
||||
let fileName;
|
||||
if (filePath.includes("\\") && !filePath.includes("/")) {
|
||||
fileName = path.win32.basename(filePath);
|
||||
} else if (filePath.includes("/") && !filePath.includes("\\")) {
|
||||
fileName = path.posix.basename(filePath);
|
||||
} else {
|
||||
fileName = filePath.split(/[/\\]/).pop();
|
||||
}
|
||||
for (let row of agentTable.rows) {
|
||||
let platformName = agentcheckin.platform;
|
||||
if (global.agent.platform === "darwin") {
|
||||
platformName = "macOS";
|
||||
} else if (global.agent.platform === "win32") {
|
||||
platformName = "Windows";
|
||||
} else if (global.agent.platform === "linux") {
|
||||
platformName = "Linux";
|
||||
|
||||
// Try to get fresh checkin data, but don't block if it fails
|
||||
try {
|
||||
let agentcheckin = await ipcRenderer.invoke('get-agent-checkin', agentid);
|
||||
if (agentcheckin) {
|
||||
agentcheckin = JSON.parse(agentcheckin);
|
||||
populateAgentTable(agentcheckin);
|
||||
|
||||
if (global.historyload === false) {
|
||||
log(`Loading previous command history`);
|
||||
global.historyload = true;
|
||||
loadCommandHistory(global.agent.hostname);
|
||||
loadPreviousLogs();
|
||||
}
|
||||
row.cells[0].textContent = agentcheckin.agentid;
|
||||
row.cells[1].textContent = agentcheckin.hostname;
|
||||
row.cells[2].textContent = agentcheckin.username;
|
||||
row.cells[3].textContent = fileName;
|
||||
row.cells[4].textContent = agentcheckin.PID;
|
||||
row.cells[5].textContent = agentcheckin.IP;
|
||||
row.cells[6].textContent = agentcheckin.arch;
|
||||
row.cells[7].textContent = platformName;
|
||||
row.cells[8].textContent = timeDifference(agentcheckin.checkin);
|
||||
agentid_log = agentcheckin.agentid;
|
||||
user_log = agentcheckin.username;
|
||||
pid_log = agentcheckin.PID;
|
||||
global.inputload = true;
|
||||
}
|
||||
} catch (checkinError) {
|
||||
log(`updateTable: Failed to get checkin data, using existing agent data: ${checkinError}`);
|
||||
// Fallback to using the original agent data
|
||||
populateAgentTable(global.agent);
|
||||
if (global.historyload === false) {
|
||||
log(`Loading previous command history`);
|
||||
global.historyload = true;
|
||||
@@ -673,7 +801,7 @@ async function updateTable() {
|
||||
global.inputload = true;
|
||||
}
|
||||
} catch (error) {
|
||||
log(`${error} ${error.stack}`);
|
||||
log(`updateTable error: ${error} ${error.stack}`);
|
||||
}
|
||||
}
|
||||
setInterval(updateTable, 1000);
|
||||
@@ -711,10 +839,26 @@ async function format_ls_output(filesAndFolders) {
|
||||
}
|
||||
|
||||
ipcRenderer.on('command-output', async (event, command_response) => {
|
||||
log(`[AGENT][IPC] command-output : ${command_response.output}`);
|
||||
log(`[AGENT][IPC] command : ${command_response.command}`);
|
||||
log(`[AGENT][IPC] taskid : ${command_response.taskid}`);
|
||||
log(`[AGENT][IPC] status : ${command_response.status}`);
|
||||
try {
|
||||
if (command_response.command.startsWith('ls')) {
|
||||
let resultBuffer = await format_ls_output(command_response.output);
|
||||
printToConsole(resultBuffer);
|
||||
// Check if output is JSON before parsing
|
||||
let isJson = false;
|
||||
try {
|
||||
JSON.parse(command_response.output);
|
||||
isJson = true;
|
||||
} catch (e) {
|
||||
isJson = false;
|
||||
}
|
||||
if (isJson) {
|
||||
let resultBuffer = await format_ls_output(command_response.output);
|
||||
printToConsole(resultBuffer);
|
||||
} else {
|
||||
printToConsole(command_response.output);
|
||||
}
|
||||
} else if (command_response.command.startsWith('bof ')) {
|
||||
if (command_response.command.startsWith('bof ')) {
|
||||
printToConsole(command_response.output.replace(/</g, '<').replace(/>/g, '>'));
|
||||
@@ -725,7 +869,7 @@ ipcRenderer.on('command-output', async (event, command_response) => {
|
||||
printToConsole(command_response.output);
|
||||
}
|
||||
} catch (error) {
|
||||
log(`Error in ipcRender(delete-old-container): ${error.message}\r\n${error.stack}`);
|
||||
log(`[!][AGENT][IPC][command-output] Error: ${error.message}\r\n${error.stack}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -735,7 +879,7 @@ ipcRenderer.on('window-closing', async () => {
|
||||
log(`agentid: ${window_agentid}`);
|
||||
ipcRenderer.send('force-close', window_agentid);
|
||||
} catch (error) {
|
||||
log(`Error in ipcRender(delete-old-container): ${error.message}\r\n${error.stack}`);
|
||||
log(`[!][AGENT][IPC][window-closing] Error: ${error.message}\r\n${error.stack}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -779,20 +923,18 @@ function moveCursorByWord(input, direction) {
|
||||
}
|
||||
|
||||
const testCommands = [
|
||||
{ command: "sleep 1 0"},
|
||||
{ command: "sleep 0 0"},
|
||||
{ command: "cd C:\\TEMP"},
|
||||
{ command: "pwd"},
|
||||
{ command: "cd C:\\TEMP"},
|
||||
{ command: "ls \"C:\\Program Files (x86)\\Microsoft\""},
|
||||
{ command: "ls C:\\\\Program\\ Files\\ (x86)\\\\Microsoft"},
|
||||
{ command: "ls 'C:\\Program Files (x86)\\Microsoft'"},
|
||||
{ command: "ls 'C:/Program Files (x86)/Microsoft'"},
|
||||
{ command: "dns reverse 1.1.1.1"},
|
||||
{ command: "dns lookup google.com"},
|
||||
{ command: "bof /Users/bobby/CS/SA/dir/dir.x64.o go z \"C:\\Users\\user\\Desktop\""},
|
||||
{ command: "bof /dev/dir.x64.o go z \"C:\\TEMP\\"},
|
||||
{ command: "spawn powershell.exe -c \"echo testing > C:\\TEMP\\testing.txt\""},
|
||||
{ command: "ls"},
|
||||
{ command: "pwd"},
|
||||
{ command: "cat testing.txt"},
|
||||
{ command: "mv testing.txt testing2.txt"},
|
||||
{ command: "ls .\\"},
|
||||
@@ -800,10 +942,10 @@ const testCommands = [
|
||||
{ command: "ls ./"},
|
||||
{ command: "download testing3.txt"},
|
||||
{ command: "cd"},
|
||||
{ command: "upload /Users/bobby/Loki/downloads/testing3.txt ./"},
|
||||
{ command: "upload /TEMP/testing3.txt ./"},
|
||||
{ command: "scan 127.0.0.1 -p22,80,445,8080"},
|
||||
{ command: "assembly /Users/bobby/Rubeus_v4.0_InspirationOpinion.exe klist"},
|
||||
{ command: "scexec /Users/bobby/popcalc.bin"}
|
||||
{ command: "assembly /dev/Rubeus_v4.0_InspirationOpinion.exe klist"},
|
||||
{ command: "scexec /dev/popcalc.bin"}
|
||||
];
|
||||
|
||||
async function executeCommandTests() {
|
||||
|
||||
|
Before Width: | Height: | Size: 427 KiB After Width: | Height: | Size: 836 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 5.2 MiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 276 KiB |
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 385 KiB After Width: | Height: | Size: 385 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,313 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#1e1e1e">
|
||||
<title>Dashboard Settings</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Fira Code', monospace;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #1e1e1e;
|
||||
color: #c5c5c5;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.settings-container {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
background-color: #2d2d2d;
|
||||
border: 1px solid #555;
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
|
||||
height: fit-content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
border-bottom: 1px solid #555;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.settings-title {
|
||||
color: #e0e0e0;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.settings-subtitle {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
display: block;
|
||||
color: #e0e0e0;
|
||||
font-size: 13px;
|
||||
margin-bottom: 6px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.background-image-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.background-image-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
background-color: #3a3a3a;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.background-image-preview span {
|
||||
flex: 1;
|
||||
color: #888;
|
||||
font-size: 11px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.buttons-container {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 25px;
|
||||
border-top: 1px solid #555;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.btn:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #666, #777);
|
||||
color: #fff;
|
||||
border: 1px solid #555;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, #777, #888);
|
||||
box-shadow: 0 6px 12px rgba(153, 153, 153, 0.4);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: linear-gradient(135deg, #4a4a4a, #5a5a5a);
|
||||
color: #fff;
|
||||
border: 1px solid #333;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: linear-gradient(135deg, #5a5a5a, #6a6a6a);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: linear-gradient(135deg, #333, #444);
|
||||
color: #ccc;
|
||||
border: 1px solid #666;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: linear-gradient(135deg, #444, #555);
|
||||
color: #fff;
|
||||
box-shadow: 0 6px 12px rgba(153, 153, 153, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Smaller buttons for background image controls */
|
||||
.background-image-preview .btn,
|
||||
.background-image-controls .btn {
|
||||
padding: 6px 12px;
|
||||
font-size: 10px;
|
||||
min-width: 80px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="settings-container">
|
||||
|
||||
<div class="control-group">
|
||||
<label for="backgroundImageControl">Background Image</label>
|
||||
<div class="background-image-controls">
|
||||
<button class="btn btn-secondary" onclick="selectBackgroundImage()">Browse Images...</button>
|
||||
<div class="background-image-preview" id="backgroundImagePreview">
|
||||
<span id="backgroundImagePath">No image selected</span>
|
||||
<button class="btn btn-danger" onclick="clearBackgroundImage()" id="clearImageBtn" style="display: none;">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons-container">
|
||||
<button class="btn btn-secondary" onclick="resetToDefaults()">Reset to Defaults</button>
|
||||
<button class="btn btn-primary" onclick="applySettings()">Apply Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
// Settings variables
|
||||
let currentSettings = {
|
||||
backgroundImage: null
|
||||
};
|
||||
|
||||
// Initialize settings when window loads
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
loadSettings();
|
||||
});
|
||||
|
||||
function selectBackgroundImage() {
|
||||
console.log('=== SELECT BACKGROUND IMAGE CLICKED ===');
|
||||
// Send IPC message to open file dialog
|
||||
ipcRenderer.invoke('select-background-image').then(imagePath => {
|
||||
if (imagePath) {
|
||||
console.log('Selected image path:', imagePath);
|
||||
currentSettings.backgroundImage = imagePath;
|
||||
updateBackgroundImageDisplay();
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Error selecting background image:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function clearBackgroundImage() {
|
||||
console.log('=== CLEAR BACKGROUND IMAGE CLICKED ===');
|
||||
currentSettings.backgroundImage = null;
|
||||
updateBackgroundImageDisplay();
|
||||
}
|
||||
|
||||
function updateBackgroundImageDisplay() {
|
||||
const pathSpan = document.getElementById('backgroundImagePath');
|
||||
const clearBtn = document.getElementById('clearImageBtn');
|
||||
|
||||
if (currentSettings.backgroundImage) {
|
||||
// Show just the filename, not the full path
|
||||
const filename = currentSettings.backgroundImage.split(/[\\/]/).pop();
|
||||
pathSpan.textContent = filename;
|
||||
clearBtn.style.display = 'inline-block';
|
||||
} else {
|
||||
pathSpan.textContent = 'No image selected';
|
||||
clearBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function applySettings() {
|
||||
console.log('=== APPLY DASHBOARD SETTINGS CLICKED ===');
|
||||
console.log('Current settings to apply:', currentSettings);
|
||||
|
||||
// Send settings to update dashboard and save to file
|
||||
console.log('Sending apply-dashboard-settings IPC message...');
|
||||
ipcRenderer.send('apply-dashboard-settings', currentSettings);
|
||||
console.log('IPC message sent successfully');
|
||||
|
||||
// Close window
|
||||
console.log('Closing dashboard settings window...');
|
||||
closeWindow();
|
||||
}
|
||||
|
||||
function resetToDefaults() {
|
||||
// Reset background image
|
||||
currentSettings.backgroundImage = null;
|
||||
updateBackgroundImageDisplay();
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
// Load settings from customize.js file
|
||||
ipcRenderer.invoke('get-customize-settings').then(settings => {
|
||||
if (settings) {
|
||||
console.log('Loading customize settings from file:', settings);
|
||||
|
||||
if (settings.backgroundImage) {
|
||||
currentSettings.backgroundImage = settings.backgroundImage;
|
||||
}
|
||||
|
||||
updateBackgroundImageDisplay();
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Error loading customize settings:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function closeWindow() {
|
||||
ipcRenderer.send('close-dashboard-settings-window');
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'c') {
|
||||
ipcRenderer.send('copy');
|
||||
event.preventDefault();
|
||||
}
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
|
||||
ipcRenderer.send('paste');
|
||||
event.preventDefault();
|
||||
}
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'x') {
|
||||
ipcRenderer.send('cut');
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>]+++[========> Loki C2 Dashboard <========]+++[</title>
|
||||
<title>Loki Dashboard</title>
|
||||
<style>
|
||||
|
||||
body {
|
||||
@@ -12,22 +12,44 @@ body {
|
||||
background-repeat: no-repeat; /* Prevent repeating */
|
||||
background-position: center; /* Center the image */
|
||||
height: 100vh; /* Full viewport height */
|
||||
background-color: #121212;
|
||||
background-color: #000000;
|
||||
color: #e0e0e0;
|
||||
font-family: 'Arial', sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: top;
|
||||
align-items: flex-start;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
overflow: hidden; /* Prevent scroll bars */
|
||||
overflow: auto; /* Allow both vertical and horizontal scrolling */
|
||||
}
|
||||
|
||||
/* Dark mode scrollbar for the body */
|
||||
body::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-track {
|
||||
background: #0a0a0a;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-thumb {
|
||||
background: #333333;
|
||||
border-radius: 6px;
|
||||
border: 2px solid #0a0a0a;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-thumb:hover {
|
||||
background: #555555;
|
||||
}
|
||||
|
||||
table.vampire-matrix {
|
||||
width: 100%; /* Make the table consume the entire window width */
|
||||
min-width: 1200px; /* Minimum width to ensure all columns are readable */
|
||||
border-collapse: collapse;
|
||||
background-color: #1c1c1c;
|
||||
box-shadow: 0 0 10px rgba(0, 255, 98, 0.5);
|
||||
box-shadow: 0 0 10px rgba(131, 131, 131, 0.5);
|
||||
max-height: none; /* Allow table to grow beyond viewport */
|
||||
}
|
||||
|
||||
table.vampire-matrix th,
|
||||
@@ -46,11 +68,25 @@ table.vampire-matrix thead th {
|
||||
}
|
||||
|
||||
table.vampire-matrix tbody tr {
|
||||
transition: background-color 0.3s;
|
||||
transition: background-color 0.2s ease, border-left-color 0.2s ease;
|
||||
cursor: pointer;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
table.vampire-matrix tbody tr:hover {
|
||||
background-color: #2a2a2a;
|
||||
table.vampire-matrix tbody tr:hover,
|
||||
table.vampire-matrix tbody tr.row-hovered {
|
||||
background-color: #2a2a2a !important;
|
||||
border-left-color: #06883e !important;
|
||||
}
|
||||
|
||||
table.vampire-matrix tbody tr:active {
|
||||
background-color: #333333 !important;
|
||||
}
|
||||
|
||||
/* Hide the Container column (2nd column) */
|
||||
table.vampire-matrix th:nth-child(2),
|
||||
table.vampire-matrix td:nth-child(2) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
table.vampire-matrix tbody tr:nth-child(even) {
|
||||
@@ -87,6 +123,124 @@ tr > td {
|
||||
overflow: hidden; /* Ensure content overflow is hidden in each cell */
|
||||
}
|
||||
|
||||
/* Dark mode context menu styling */
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
background-color: #2d2d2d;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
|
||||
padding: 4px 0;
|
||||
min-width: 150px;
|
||||
z-index: 9999;
|
||||
font-family: 'Arial', sans-serif;
|
||||
font-size: 14px;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
border: none;
|
||||
background: none;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.context-menu-item:hover {
|
||||
background-color: #404040;
|
||||
}
|
||||
|
||||
.context-menu-item:active {
|
||||
background-color: #4a4a4a;
|
||||
}
|
||||
|
||||
.context-menu-separator {
|
||||
height: 1px;
|
||||
background-color: #555;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
/* Override Electron's native context menu when possible */
|
||||
::-webkit-context-menu {
|
||||
background-color: #2d2d2d !important;
|
||||
border: 1px solid #555 !important;
|
||||
border-radius: 4px !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6) !important;
|
||||
}
|
||||
|
||||
::-webkit-context-menu-item {
|
||||
background-color: #2d2d2d !important;
|
||||
color: #e0e0e0 !important;
|
||||
padding: 8px 16px !important;
|
||||
}
|
||||
|
||||
::-webkit-context-menu-item:hover {
|
||||
background-color: #404040 !important;
|
||||
}
|
||||
|
||||
/* Additional dark scrollbar for any dropdowns */
|
||||
*::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background: #555;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background: #666;
|
||||
}
|
||||
|
||||
.logo-watermark {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: auto;
|
||||
height: 60px;
|
||||
opacity: 0.6;
|
||||
z-index: 1000;
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.logo-watermark:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.repo-watermark {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
width: auto;
|
||||
height: 60px;
|
||||
opacity: 0.6;
|
||||
z-index: 1000;
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.repo-watermark:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Hide logos when window height is too small */
|
||||
@media (max-height: 120px) {
|
||||
.logo-watermark,
|
||||
.repo-watermark {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -110,6 +264,161 @@ tr > td {
|
||||
<!-- Container rows will be added here by renderer.js -->
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Logo watermark -->
|
||||
<img src="./assets/images/logo1.png" alt="Loki Logo" class="logo-watermark" onclick="openTwitterProfile()">
|
||||
|
||||
<!-- Repo watermark -->
|
||||
<img src="./assets/images/repo.png" alt="Repo Logo" class="repo-watermark" onclick="openGitHubRepo()">
|
||||
|
||||
<script src="dashboard.js"></script>
|
||||
<script>
|
||||
const { shell } = require('electron');
|
||||
|
||||
// Function to open GitHub repository
|
||||
function openGitHubRepo() {
|
||||
shell.openExternal('https://github.com/boku7/loki');
|
||||
}
|
||||
|
||||
// Function to open Twitter/X profile
|
||||
function openTwitterProfile() {
|
||||
shell.openExternal('https://x.com/0xBoku');
|
||||
}
|
||||
|
||||
// Function to apply background image to dashboard
|
||||
function applyBackgroundImage(settings) {
|
||||
if (settings && settings.backgroundImage) {
|
||||
console.log('Applying background image:', settings.backgroundImage);
|
||||
document.body.style.backgroundImage = `url('${settings.backgroundImage}')`;
|
||||
} else {
|
||||
console.log('Clearing background image');
|
||||
document.body.style.backgroundImage = "url('./assets/images/background.png')";
|
||||
}
|
||||
}
|
||||
|
||||
// Handle font settings from agent settings window
|
||||
ipcRenderer.on('apply-font-settings', (event, settings) => {
|
||||
console.log('=== RECEIVED APPLY-FONT-SETTINGS IPC IN DASHBOARD ===');
|
||||
console.log('Settings:', settings);
|
||||
applyBackgroundImage(settings);
|
||||
});
|
||||
|
||||
// Logo visibility management based on window height and width
|
||||
function manageLogoVisibility() {
|
||||
const logoWatermark = document.querySelector('.logo-watermark');
|
||||
const repoWatermark = document.querySelector('.repo-watermark');
|
||||
const minHeightNeeded = 140; // 60px logo + 40px padding + some buffer
|
||||
|
||||
function checkLogoVisibility() {
|
||||
const windowHeight = window.innerHeight;
|
||||
const windowWidth = window.innerWidth;
|
||||
|
||||
// Hide both logos if window height is too small
|
||||
const shouldHideForHeight = windowHeight < minHeightNeeded;
|
||||
|
||||
let shouldHideRepoForWidth = false;
|
||||
|
||||
// Calculate actual overlap positions if both logos exist
|
||||
if (logoWatermark && repoWatermark && !shouldHideForHeight) {
|
||||
// Ensure logos are visible first to get accurate measurements
|
||||
logoWatermark.style.display = 'block';
|
||||
repoWatermark.style.display = 'block';
|
||||
|
||||
// Get actual logo dimensions and positions
|
||||
const logoRect = logoWatermark.getBoundingClientRect();
|
||||
const repoRect = repoWatermark.getBoundingClientRect();
|
||||
|
||||
// Calculate positions:
|
||||
// Left logo: positioned at left: 20px, so its right edge is at (20px + width)
|
||||
// Right logo: positioned at right: 20px, so its left edge is at (windowWidth - 20px - width)
|
||||
const repoRightEdge = 20 + repoRect.width;
|
||||
const logoLeftEdge = windowWidth - 20 - logoRect.width;
|
||||
|
||||
// Hide repo logo if its right edge would touch or overlap the right logo's left edge
|
||||
shouldHideRepoForWidth = repoRightEdge >= logoLeftEdge;
|
||||
}
|
||||
|
||||
if (logoWatermark) {
|
||||
logoWatermark.style.display = shouldHideForHeight ? 'none' : 'block';
|
||||
}
|
||||
if (repoWatermark) {
|
||||
// Hide repo logo if either height is too small OR width would cause overlap
|
||||
repoWatermark.style.display = (shouldHideForHeight || shouldHideRepoForWidth) ? 'none' : 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Check on load and resize
|
||||
checkLogoVisibility();
|
||||
window.addEventListener('resize', checkLogoVisibility);
|
||||
}
|
||||
|
||||
// Persistent row hover management
|
||||
function setupPersistentHover() {
|
||||
const table = document.getElementById('containerTable');
|
||||
if (!table) return;
|
||||
|
||||
const tbody = table.querySelector('tbody');
|
||||
|
||||
// Add event listeners for persistent hover
|
||||
tbody.addEventListener('mouseenter', (e) => {
|
||||
if (e.target.tagName === 'TD') {
|
||||
const row = e.target.closest('tr');
|
||||
if (row && row.parentElement === tbody) {
|
||||
// Remove hover class from all other rows first
|
||||
tbody.querySelectorAll('.row-hovered').forEach(otherRow => {
|
||||
if (otherRow !== row) {
|
||||
otherRow.classList.remove('row-hovered');
|
||||
}
|
||||
});
|
||||
// Add hover class to current row
|
||||
row.classList.add('row-hovered');
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
|
||||
tbody.addEventListener('mouseleave', (e) => {
|
||||
if (e.target.tagName === 'TD') {
|
||||
const row = e.target.closest('tr');
|
||||
if (row && row.parentElement === tbody) {
|
||||
// Check if mouse is still within the table area
|
||||
const rect = table.getBoundingClientRect();
|
||||
const mouseX = e.clientX;
|
||||
const mouseY = e.clientY;
|
||||
|
||||
if (mouseX < rect.left || mouseX > rect.right ||
|
||||
mouseY < rect.top || mouseY > rect.bottom) {
|
||||
row.classList.remove('row-hovered');
|
||||
}
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
|
||||
// Clean up hover states when mouse leaves table entirely
|
||||
table.addEventListener('mouseleave', () => {
|
||||
tbody.querySelectorAll('.row-hovered').forEach(row => {
|
||||
row.classList.remove('row-hovered');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Load saved settings on page load
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
// Setup persistent hover system
|
||||
setupPersistentHover();
|
||||
|
||||
// Setup logo visibility management
|
||||
manageLogoVisibility();
|
||||
|
||||
// Load settings from customize.js file
|
||||
ipcRenderer.invoke('get-customize-settings').then(settings => {
|
||||
if (settings) {
|
||||
console.log('Loading customize settings for dashboard:', settings);
|
||||
applyBackgroundImage(settings);
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Error loading customize settings in dashboard:', error);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -37,6 +37,30 @@ function timeDifference(oldTimestamp) {
|
||||
return result.trim().replace(/,\s*$/, ''); // Remove trailing comma and space
|
||||
}
|
||||
|
||||
// Function to parse time strings back to milliseconds for sorting
|
||||
function parseTimeStringToMilliseconds(timeString) {
|
||||
if (!timeString || timeString.trim() === '') return 0;
|
||||
|
||||
let totalMs = 0;
|
||||
const msInSecond = 1000;
|
||||
const msInMinute = msInSecond * 60;
|
||||
const msInHour = msInMinute * 60;
|
||||
const msInDay = msInHour * 24;
|
||||
|
||||
// Parse patterns like "1d, 3h, 5m, 2s" or just "11s"
|
||||
const dayMatch = timeString.match(/(\d+)d/);
|
||||
const hourMatch = timeString.match(/(\d+)h/);
|
||||
const minuteMatch = timeString.match(/(\d+)m/);
|
||||
const secondMatch = timeString.match(/(\d+)s/);
|
||||
|
||||
if (dayMatch) totalMs += parseInt(dayMatch[1]) * msInDay;
|
||||
if (hourMatch) totalMs += parseInt(hourMatch[1]) * msInHour;
|
||||
if (minuteMatch) totalMs += parseInt(minuteMatch[1]) * msInMinute;
|
||||
if (secondMatch) totalMs += parseInt(secondMatch[1]) * msInSecond;
|
||||
|
||||
return totalMs;
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
let sortState = { column: null, order: 'none' };
|
||||
|
||||
@@ -173,6 +197,7 @@ window.addEventListener('DOMContentLoaded', async () => {
|
||||
try {
|
||||
let agentcheckins;
|
||||
agentcheckins = await ipcRenderer.invoke('get-containers');
|
||||
// log(`[DASHBOARD] agentcheckins : ${agentcheckins}`);
|
||||
if (agentcheckins != 0) {
|
||||
const agents = JSON.parse(agentcheckins);
|
||||
let agent_index = 0;
|
||||
@@ -204,29 +229,27 @@ window.addEventListener('DOMContentLoaded', async () => {
|
||||
} else if (agent.platform === "linux") {
|
||||
platformName = "Linux";
|
||||
}
|
||||
thisrow.cells[0].textContent = agent.agentid;
|
||||
thisrow.cells[1].textContent = agent.container;
|
||||
thisrow.cells[2].textContent = agent.hostname;
|
||||
thisrow.cells[3].textContent = agent.username;
|
||||
thisrow.cells[4].textContent = fileName;
|
||||
thisrow.cells[5].textContent = agent.PID;
|
||||
thisrow.cells[6].textContent = agent.IP;
|
||||
thisrow.cells[7].textContent = agent.arch;
|
||||
thisrow.cells[8].textContent = platformName; // Set formatted platform name
|
||||
thisrow.cells[9].textContent = agent.mode;
|
||||
thisrow.cells[10].textContent = timeDifference(agent.checkin);
|
||||
thisrow.replaceWith(thisrow.cloneNode(true)); // Remove previous listeners
|
||||
let table = document.getElementById('containerTable').getElementsByTagName('tbody')[0];
|
||||
for (let row of table.rows) {
|
||||
if (row.cells[0].textContent == agent.agentid) {
|
||||
thisrow = row;
|
||||
break;
|
||||
}
|
||||
// Update cell content only if it has changed
|
||||
if (thisrow.cells[0].textContent !== agent.agentid) thisrow.cells[0].textContent = agent.agentid;
|
||||
if (thisrow.cells[1].textContent !== agent.container) thisrow.cells[1].textContent = agent.container;
|
||||
if (thisrow.cells[2].textContent !== agent.hostname) thisrow.cells[2].textContent = agent.hostname;
|
||||
if (thisrow.cells[3].textContent !== agent.username) thisrow.cells[3].textContent = agent.username;
|
||||
if (thisrow.cells[4].textContent !== fileName) thisrow.cells[4].textContent = fileName;
|
||||
if (thisrow.cells[5].textContent !== agent.PID) thisrow.cells[5].textContent = agent.PID;
|
||||
if (thisrow.cells[6].textContent !== agent.IP) thisrow.cells[6].textContent = agent.IP;
|
||||
if (thisrow.cells[7].textContent !== agent.arch) thisrow.cells[7].textContent = agent.arch;
|
||||
if (thisrow.cells[8].textContent !== platformName) thisrow.cells[8].textContent = platformName;
|
||||
if (thisrow.cells[9].textContent !== agent.mode) thisrow.cells[9].textContent = agent.mode;
|
||||
if (thisrow.cells[10].textContent !== timeDifference(agent.checkin)) thisrow.cells[10].textContent = timeDifference(agent.checkin);
|
||||
|
||||
// Only add click listener if this is a new row or if listener was removed
|
||||
if (isnewrow || !thisrow.hasAttribute('data-click-listener')) {
|
||||
thisrow.addEventListener('click', () => {
|
||||
console.log("Row clicked!");
|
||||
ipcRenderer.send('open-container-window', agent.agentid);
|
||||
});
|
||||
thisrow.setAttribute('data-click-listener', 'true');
|
||||
}
|
||||
thisrow.addEventListener('click', () => {
|
||||
console.log("Row clicked!"); // Debugging: Check if it logs multiple times
|
||||
ipcRenderer.send('open-container-window', agent.agentid);
|
||||
}, { once: true }); // Ensures it only triggers once per element
|
||||
}
|
||||
});
|
||||
updateTableSort();
|
||||
@@ -287,7 +310,15 @@ window.addEventListener('DOMContentLoaded', async () => {
|
||||
rows.sort((a, b) => {
|
||||
const aText = a.querySelector(`td:nth-child(${getColumnIndex(sortState.column)})`).textContent.trim();
|
||||
const bText = b.querySelector(`td:nth-child(${getColumnIndex(sortState.column)})`).textContent.trim();
|
||||
return sortState.order === 'asc' ? aText.localeCompare(bText) : bText.localeCompare(aText);
|
||||
|
||||
// Special handling for checkin column (time sorting)
|
||||
if (sortState.column === 'checkin') {
|
||||
const aTime = parseTimeStringToMilliseconds(aText);
|
||||
const bTime = parseTimeStringToMilliseconds(bText);
|
||||
return sortState.order === 'asc' ? aTime - bTime : bTime - aTime;
|
||||
} else {
|
||||
return sortState.order === 'asc' ? aText.localeCompare(bText) : bText.localeCompare(aText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
{
|
||||
"name": "loki",
|
||||
"version": "2.0.0",
|
||||
"version": "2.4.0",
|
||||
"main": "./kernel.js",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"pack": "electron-builder --dir",
|
||||
"dist": "electron-builder"
|
||||
},
|
||||
"keywords": ["C2","Electron","Red Team","adversary simulation","https://x.com/0xBoku","https://www.linkedin.com/in/bobby-cooke/"],
|
||||
"keywords": ["C2","Electron","Red Team","Adversary Simulation","https://x.com/0xBoku","https://www.linkedin.com/in/bobby-cooke/"],
|
||||
"author": "Bobby Cooke",
|
||||
"homepage": "https://0xBoku.com",
|
||||
"license": "BSL",
|
||||
@@ -23,12 +22,11 @@
|
||||
"copyright": "© 2025 Bobby Cooke",
|
||||
"asar": true,
|
||||
"files": [
|
||||
"kernel.js",
|
||||
"*.html",
|
||||
"*.js",
|
||||
"*.css",
|
||||
"assets/**",
|
||||
"!node_modules/electron-builder/**"
|
||||
"!node_modules/**"
|
||||
],
|
||||
"mac": {
|
||||
"icon": "assets/mac/icon.icns",
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Task Queue</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', monospace;
|
||||
background-color: #18191a;
|
||||
color: #e0e0e0;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #232323;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.agent-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.agent-id {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #e0e0e0;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.agent-details {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.queued, .processing, .completed, .error {
|
||||
/* Keep status colors for numbers */
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: #232323;
|
||||
border: 1px solid #333;
|
||||
color: #e0e0e0;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
background-color: #18191a;
|
||||
color: #666;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn.danger {
|
||||
background-color: #333;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.btn.danger:hover {
|
||||
background-color: #444;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
background-color: #232323;
|
||||
border: 1px solid #333;
|
||||
color: #e0e0e0;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
border: 1px solid #232323;
|
||||
border-radius: 4px;
|
||||
background-color: #202124;
|
||||
}
|
||||
|
||||
.task-item {
|
||||
border-bottom: 1px solid #232323;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
background-color: #232323;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.task-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.task-item.processing {
|
||||
background-color: #28251a;
|
||||
border-left: 4px solid #ff9800;
|
||||
}
|
||||
|
||||
.task-info {
|
||||
flex-grow: 1;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.task-command {
|
||||
font-family: 'Fira Mono', 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
color: #e0e0e0;
|
||||
word-break: break-all;
|
||||
margin-bottom: 5px;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.task-meta {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.task-status {
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-starting { background-color: #2196f3; }
|
||||
.status-queued { background-color: #ffeb3b; color: #000; }
|
||||
.status-processing { background-color: #ff9800; color: #000; }
|
||||
.status-completed { background-color: #4caf50; }
|
||||
.status-error { background-color: #f44336; }
|
||||
|
||||
.task-actions {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background-color: transparent;
|
||||
border: 1px solid #333;
|
||||
color: #e0e0e0;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.action-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.edit-input {
|
||||
background-color: #232323;
|
||||
border: 1px solid #333;
|
||||
color: #e0e0e0;
|
||||
padding: 5px;
|
||||
font-family: 'Fira Mono', 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.no-tasks {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.queue-position {
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 5px;
|
||||
background-color: #333;
|
||||
color: #e0e0e0;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div class="agent-info">
|
||||
<span class="agent-id" id="agentId">Loading...</span>
|
||||
<span class="agent-details" id="agentDetails"></span>
|
||||
</div>
|
||||
<div class="stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number queued" id="queuedCount">0</span>
|
||||
<span class="stat-label">Queued</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-number processing" id="processingCount">0</span>
|
||||
<span class="stat-label">Processing</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-number completed" id="completedCount">0</span>
|
||||
<span class="stat-label">Completed</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-number error" id="errorCount">0</span>
|
||||
<span class="stat-label">Errors</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button class="btn" onclick="refreshTasks()">🔄 Refresh</button>
|
||||
<select class="filter-select" id="statusFilter" onchange="filterTasks()">
|
||||
<option value="all">All Tasks</option>
|
||||
<option value="starting">Starting</option>
|
||||
<option value="queued">Queued</option>
|
||||
<option value="processing">Processing</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="error">Errors</option>
|
||||
</select>
|
||||
<button class="btn danger" onclick="clearCompleted()">🗑️ Clear Completed</button>
|
||||
</div>
|
||||
|
||||
<div class="task-list" id="taskList">
|
||||
<div class="no-tasks">No tasks found</div>
|
||||
</div>
|
||||
|
||||
<script src="task-queue.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,357 @@
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
let currentAgent = null;
|
||||
let allTasks = [];
|
||||
let filteredTasks = [];
|
||||
let currentFilter = 'all';
|
||||
let isEditing = false;
|
||||
let expandedTaskId = null; // Track which task is expanded
|
||||
|
||||
// Initialize when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
setupEventListeners();
|
||||
|
||||
// Auto-refresh every 2 seconds, but only if not editing
|
||||
setInterval(() => {
|
||||
if (!isEditing) {
|
||||
refreshTasks();
|
||||
}
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
// Listen for agent data from main process
|
||||
ipcRenderer.on('agent-data', (event, agent) => {
|
||||
currentAgent = agent;
|
||||
updateAgentInfo();
|
||||
refreshTasks();
|
||||
});
|
||||
|
||||
// Listen for real-time task updates
|
||||
ipcRenderer.on('task-removed', (event, taskId) => {
|
||||
removeTaskFromList(taskId);
|
||||
refreshDisplay();
|
||||
});
|
||||
|
||||
ipcRenderer.on('task-modified', (event, task) => {
|
||||
updateTaskInList(task);
|
||||
refreshDisplay();
|
||||
});
|
||||
|
||||
ipcRenderer.on('tasks-reordered', (event, tasks) => {
|
||||
allTasks = tasks || [];
|
||||
refreshDisplay();
|
||||
});
|
||||
|
||||
function setupEventListeners() {
|
||||
// Filter change
|
||||
document.getElementById('statusFilter').addEventListener('change', filterTasks);
|
||||
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'F5' || (event.ctrlKey && event.key === 'r')) {
|
||||
event.preventDefault();
|
||||
refreshTasks();
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
// Cancel any active editing
|
||||
cancelAllEditing();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateAgentInfo() {
|
||||
if (!currentAgent) return;
|
||||
|
||||
document.getElementById('agentId').textContent = currentAgent.agentid || 'Unknown';
|
||||
|
||||
const details = [
|
||||
currentAgent.hostname || 'Unknown Host',
|
||||
currentAgent.username || 'Unknown User',
|
||||
Array.isArray(currentAgent.IP) ? currentAgent.IP.join(', ') : (currentAgent.IP || 'Unknown IP')
|
||||
].join(' • ');
|
||||
|
||||
document.getElementById('agentDetails').textContent = details;
|
||||
}
|
||||
|
||||
async function refreshTasks() {
|
||||
if (!currentAgent) return;
|
||||
|
||||
try {
|
||||
const tasks = await ipcRenderer.invoke('get-agent-tasks', currentAgent.agentid);
|
||||
allTasks = tasks || [];
|
||||
refreshDisplay();
|
||||
} catch (error) {
|
||||
console.error('Error fetching tasks:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function refreshDisplay() {
|
||||
updateStats();
|
||||
filterTasks();
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
const stats = {
|
||||
starting: 0,
|
||||
queued: 0,
|
||||
processing: 0,
|
||||
completed: 0,
|
||||
error: 0
|
||||
};
|
||||
|
||||
allTasks.forEach(task => {
|
||||
if (stats.hasOwnProperty(task.status)) {
|
||||
stats[task.status]++;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('queuedCount').textContent = stats.queued;
|
||||
document.getElementById('processingCount').textContent = stats.processing;
|
||||
document.getElementById('completedCount').textContent = stats.completed;
|
||||
document.getElementById('errorCount').textContent = stats.error;
|
||||
}
|
||||
|
||||
function filterTasks() {
|
||||
const filter = document.getElementById('statusFilter').value;
|
||||
currentFilter = filter;
|
||||
|
||||
if (filter === 'all') {
|
||||
filteredTasks = [...allTasks];
|
||||
} else {
|
||||
filteredTasks = allTasks.filter(task => task.status === filter);
|
||||
}
|
||||
|
||||
renderTasks();
|
||||
}
|
||||
|
||||
function renderTasks() {
|
||||
const taskList = document.getElementById('taskList');
|
||||
|
||||
if (filteredTasks.length === 0) {
|
||||
taskList.innerHTML = '<div class="no-tasks">No tasks found</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
taskList.innerHTML = '';
|
||||
|
||||
filteredTasks.forEach((task, index) => {
|
||||
const taskElement = createTaskElement(task, index);
|
||||
taskList.appendChild(taskElement);
|
||||
// Add click handler for completed or error tasks to expand/collapse
|
||||
if (task.status === 'completed' || task.status === 'error') {
|
||||
taskElement.addEventListener('click', (e) => {
|
||||
// Only expand/collapse if not clicking a button
|
||||
if (e.target.classList.contains('action-btn')) return;
|
||||
if (expandedTaskId === task.taskid) {
|
||||
expandedTaskId = null;
|
||||
} else {
|
||||
expandedTaskId = task.taskid;
|
||||
}
|
||||
renderTasks();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createTaskElement(task, index) {
|
||||
const taskItem = document.createElement('div');
|
||||
taskItem.className = `task-item ${task.status === 'processing' ? 'processing' : ''}`;
|
||||
taskItem.dataset.taskId = task.taskid;
|
||||
|
||||
const queuePosition = currentFilter === 'queued' ? index + 1 : null;
|
||||
|
||||
let outputSection = '';
|
||||
if ((task.status === 'completed' || task.status === 'error') && expandedTaskId === task.taskid) {
|
||||
outputSection = `<div class="task-output" style="margin-top:10px; background:#18191a; border-radius:4px; padding:10px; color:#bdbdbd; font-size:13px; white-space:pre-wrap;">${escapeHtml(task.output || (task.status === 'error' ? 'No error output' : 'No output'))}</div>`;
|
||||
}
|
||||
|
||||
taskItem.innerHTML = `
|
||||
${queuePosition ? `<div class="queue-position">${queuePosition}</div>` : ''}
|
||||
<div class="task-info">
|
||||
<div class="task-command" id="command-${task.taskid}">${escapeHtml(task.command)}</div>
|
||||
<div class="task-meta">
|
||||
<span class="task-status status-${task.status}">${task.status}</span>
|
||||
<span>ID: ${task.taskid}</span>
|
||||
<span>Output: ${task.outputChannel || 'N/A'}</span>
|
||||
<span>Upload: ${task.uploadChannel || 'N/A'}</span>
|
||||
</div>
|
||||
${outputSection}
|
||||
</div>
|
||||
<div class="task-actions">
|
||||
${createActionButtons(task)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
return taskItem;
|
||||
}
|
||||
|
||||
function createActionButtons(task) {
|
||||
const isProcessing = task.status === 'processing';
|
||||
const isCompleted = task.status === 'completed';
|
||||
const isError = task.status === 'error';
|
||||
const isQueued = task.status === 'queued';
|
||||
const canModify = !isProcessing && !isCompleted && !isError;
|
||||
let buttons = '';
|
||||
|
||||
// No edit or remove for error tasks
|
||||
if (isError) {
|
||||
return buttons;
|
||||
}
|
||||
|
||||
if (canModify) {
|
||||
buttons += `<button class="action-btn" onclick="editTask('${task.taskid}')" title="Edit Command">✏️</button>`;
|
||||
}
|
||||
|
||||
// For queued tasks, show a red X for removal (no prompt)
|
||||
if (isQueued) {
|
||||
buttons += `<button class="action-btn" style="color:#f44336;" onclick="removeTaskNoPrompt('${task.taskid}')" title="Remove Task">❌</button>`;
|
||||
} else if (canModify) {
|
||||
// For other modifiable tasks (shouldn't happen, but fallback)
|
||||
buttons += `<button class="action-btn" onclick="removeTask('${task.taskid}')" title="Remove Task">��️</button>`;
|
||||
}
|
||||
|
||||
if (isProcessing) {
|
||||
buttons += `<span class="action-btn" style="opacity: 0.5;" title="Processing...">⏳</span>`;
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
async function editTask(taskId) {
|
||||
const task = allTasks.find(t => t.taskid === taskId);
|
||||
if (!task) return;
|
||||
|
||||
isEditing = true;
|
||||
|
||||
const commandElement = document.getElementById(`command-${taskId}`);
|
||||
const originalCommand = task.command;
|
||||
|
||||
// Create input field
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.className = 'edit-input';
|
||||
input.value = originalCommand;
|
||||
|
||||
// Replace command text with input
|
||||
commandElement.innerHTML = '';
|
||||
commandElement.appendChild(input);
|
||||
input.focus();
|
||||
input.select();
|
||||
|
||||
// Handle save/cancel
|
||||
const saveEdit = async () => {
|
||||
const newCommand = input.value.trim();
|
||||
if (newCommand && newCommand !== originalCommand) {
|
||||
try {
|
||||
const result = await ipcRenderer.invoke('modify-task', currentAgent.agentid, taskId, newCommand);
|
||||
if (result.success) {
|
||||
task.command = newCommand;
|
||||
commandElement.textContent = newCommand;
|
||||
} else {
|
||||
alert(`Error: ${result.message}`);
|
||||
commandElement.textContent = originalCommand;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error modifying task:', error);
|
||||
alert('Error modifying task');
|
||||
commandElement.textContent = originalCommand;
|
||||
}
|
||||
} else {
|
||||
commandElement.textContent = originalCommand;
|
||||
}
|
||||
isEditing = false;
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
commandElement.textContent = originalCommand;
|
||||
isEditing = false;
|
||||
};
|
||||
|
||||
input.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
saveEdit();
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
cancelEdit();
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener('blur', saveEdit);
|
||||
}
|
||||
|
||||
// Remove task immediately, no prompt
|
||||
async function removeTaskNoPrompt(taskId) {
|
||||
try {
|
||||
const result = await ipcRenderer.invoke('remove-task', currentAgent.agentid, taskId);
|
||||
if (result.success) {
|
||||
removeTaskFromList(taskId);
|
||||
refreshDisplay();
|
||||
} else {
|
||||
alert(`Error: ${result.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error removing task:', error);
|
||||
alert('Error removing task');
|
||||
}
|
||||
}
|
||||
|
||||
async function moveTask(taskId, direction) {
|
||||
try {
|
||||
const result = await ipcRenderer.invoke('reorder-task', currentAgent.agentid, taskId, direction);
|
||||
if (!result.success) {
|
||||
alert(`Error: ${result.message}`);
|
||||
}
|
||||
// The tasks-reordered event will handle the UI update
|
||||
} catch (error) {
|
||||
console.error('Error moving task:', error);
|
||||
alert('Error moving task');
|
||||
}
|
||||
}
|
||||
|
||||
// Remove completed and error tasks
|
||||
async function clearCompleted() {
|
||||
const completedOrErrorTasks = allTasks.filter(task => task.status === 'completed' || task.status === 'error');
|
||||
if (completedOrErrorTasks.length === 0) {
|
||||
alert('No completed or error tasks to clear');
|
||||
return;
|
||||
}
|
||||
if (!confirm(`Are you sure you want to remove ${completedOrErrorTasks.length} completed/error task(s)?`)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
for (const task of completedOrErrorTasks) {
|
||||
await ipcRenderer.invoke('remove-task', currentAgent.agentid, task.taskid);
|
||||
}
|
||||
refreshTasks();
|
||||
} catch (error) {
|
||||
console.error('Error clearing completed/error tasks:', error);
|
||||
alert('Error clearing completed/error tasks');
|
||||
}
|
||||
}
|
||||
|
||||
function removeTaskFromList(taskId) {
|
||||
allTasks = allTasks.filter(task => task.taskid !== taskId);
|
||||
}
|
||||
|
||||
function updateTaskInList(updatedTask) {
|
||||
const index = allTasks.findIndex(task => task.taskid === updatedTask.taskid);
|
||||
if (index !== -1) {
|
||||
allTasks[index] = updatedTask;
|
||||
}
|
||||
}
|
||||
|
||||
function cancelAllEditing() {
|
||||
if (isEditing) {
|
||||
const activeInput = document.querySelector('.edit-input');
|
||||
if (activeInput) {
|
||||
activeInput.dispatchEvent(new Event('blur'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||