1
0
mirror of https://github.com/boku7/Loki synced 2026-06-06 15:24:27 +00:00

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

This commit is contained in:
Bobby Cooke
2025-06-17 09:14:20 -07:00
parent 1c06ac1ed7
commit fd93514887
19 changed files with 3147 additions and 247 deletions
+546
View File
@@ -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>
+238 -71
View File
@@ -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>
+207 -65
View File
@@ -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, '&lt;').replace(/>/g, '&gt;'));
@@ -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() {
Regular → Executable
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 427 KiB

After

Width:  |  Height:  |  Size: 836 KiB

Regular → Executable
View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Regular → Executable
View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Regular → Executable
View File

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 155 KiB

Regular → Executable
View File

Before

Width:  |  Height:  |  Size: 385 KiB

After

Width:  |  Height:  |  Size: 385 KiB

Regular → Executable
View File

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

+313
View File
@@ -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>
+317 -8
View File
@@ -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>
+54 -23
View File
@@ -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);
}
});
}
+828 -75
View File
File diff suppressed because it is too large Load Diff
+3 -5
View File
@@ -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",
+284
View File
@@ -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>
+357
View File
@@ -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;
}