Merge pull request 'chore(release): 0.7.0' (#18) from release/0.7.0 into main
Reviewed-on: #18
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ body {
|
|||||||
background-color: var(--ctp-base);
|
background-color: var(--ctp-base);
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
color: var(--ctp-text);
|
color: var(--ctp-text);
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fi {
|
.fi {
|
||||||
@@ -62,15 +63,13 @@ main {
|
|||||||
aside {
|
aside {
|
||||||
width: 20rem;
|
width: 20rem;
|
||||||
background-color: var(--ctp-base);
|
background-color: var(--ctp-base);
|
||||||
border-right: 2px solid var(--ctp-mantle);
|
border-right: 4px solid var(--ctp-mantle);
|
||||||
}
|
}
|
||||||
|
|
||||||
#content {
|
#content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
header a {
|
header a {
|
||||||
|
|||||||
163
src/judas_server/web/static/js/panel.js
Normal file
163
src/judas_server/web/static/js/panel.js
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
$(document).ready(function () {
|
||||||
|
const socket = io();
|
||||||
|
|
||||||
|
const showNotify = (message) => {
|
||||||
|
$("#notify").stop().fadeIn();
|
||||||
|
$("#notify").text(message);
|
||||||
|
setTimeout(() => {
|
||||||
|
$("#notify").fadeOut();
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadClientDetails = (clientId) => {
|
||||||
|
fetch(`/client/${clientId}`)
|
||||||
|
.then((response) => response.text())
|
||||||
|
.then((html) => {
|
||||||
|
$("#content").html(html);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error fetching client details:", error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// load client_details for the client specified in the URL
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const clientId = urlParams.get("client");
|
||||||
|
if (clientId) {
|
||||||
|
loadClientDetails(clientId);
|
||||||
|
$(`#client-list a[href="?client=${clientId}"]`).addClass("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#notify").hide();
|
||||||
|
|
||||||
|
socket.on("connect", () => {
|
||||||
|
console.log("Connected to server");
|
||||||
|
$("#no-connection-message").hide();
|
||||||
|
showNotify("Connected to server");
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("disconnect", () => {
|
||||||
|
console.log("Disconnected from server");
|
||||||
|
$("#no-connection-message").show();
|
||||||
|
showNotify("Disconnected from server");
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("update_data", (data) => {
|
||||||
|
const clientList = $("#client-list");
|
||||||
|
const existingItems = {};
|
||||||
|
|
||||||
|
// Index current <li> by clientId
|
||||||
|
clientList.children("li").each(function () {
|
||||||
|
const a = $(this).find("a");
|
||||||
|
if (a.length) {
|
||||||
|
const clientId = a.attr("href").substring(1);
|
||||||
|
existingItems[clientId] = $(this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track which clientIds are still present
|
||||||
|
const seen = new Set();
|
||||||
|
|
||||||
|
Object.entries(data).forEach(([clientId, client]) => {
|
||||||
|
seen.add(clientId);
|
||||||
|
|
||||||
|
// Build icon
|
||||||
|
const iconElement = document.createElement("i");
|
||||||
|
switch (client.status) {
|
||||||
|
case "online":
|
||||||
|
iconElement.className = "fi fi-sr-play text-ctp-green";
|
||||||
|
break;
|
||||||
|
case "offline":
|
||||||
|
iconElement.className = "fi fi-sr-stop text-ctp-red";
|
||||||
|
break;
|
||||||
|
case "pending":
|
||||||
|
iconElement.className = "fi fi-sr-pending text-ctp-yellow";
|
||||||
|
break;
|
||||||
|
case "stale":
|
||||||
|
iconElement.className = "fi fi-sr-skull text-ctp-text";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
iconElement.className = "fi fi-rr-question text-ctp-gray";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time since last seen
|
||||||
|
const lastSeen = new Date(client.last_seen * 1000);
|
||||||
|
const now = new Date();
|
||||||
|
const timeSinceLastSeen = Math.floor((now - lastSeen) / 1000);
|
||||||
|
const days = Math.floor(timeSinceLastSeen / 86400);
|
||||||
|
const hours = Math.floor((timeSinceLastSeen % 86400) / 3600);
|
||||||
|
const minutes = Math.floor((timeSinceLastSeen % 3600) / 60);
|
||||||
|
const seconds = timeSinceLastSeen % 60;
|
||||||
|
let timeSinceLastSeenText = "";
|
||||||
|
if (days > 0) timeSinceLastSeenText += `${days}d `;
|
||||||
|
if (hours > 0) timeSinceLastSeenText += `${hours}h `;
|
||||||
|
if (minutes > 0) timeSinceLastSeenText += `${minutes}m `;
|
||||||
|
if (seconds > 0) timeSinceLastSeenText += `${seconds}s`;
|
||||||
|
|
||||||
|
timeSinceLastSeenText = timeSinceLastSeenText.trim() || "0s";
|
||||||
|
|
||||||
|
let statusText = `${client.id} (${timeSinceLastSeenText})`;
|
||||||
|
|
||||||
|
// check if <li> exists
|
||||||
|
if (existingItems[clientId]) {
|
||||||
|
// update if needed
|
||||||
|
const li = existingItems[clientId];
|
||||||
|
const a = li.find("a");
|
||||||
|
if (a.text() !== statusText) {
|
||||||
|
a.text(statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
li.attr(
|
||||||
|
"title",
|
||||||
|
`Status: ${client.status}\nLast Seen: ${lastSeen.toISOString()}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// update icon
|
||||||
|
const icon = li.find("i")[0];
|
||||||
|
if (icon) {
|
||||||
|
icon.className = iconElement.className;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// add new <li>
|
||||||
|
const li = $("<li></li>");
|
||||||
|
li.append(iconElement);
|
||||||
|
const a = $("<a></a>")
|
||||||
|
.text(statusText)
|
||||||
|
.attr("href", `?client=${clientId}`);
|
||||||
|
|
||||||
|
li.attr(
|
||||||
|
"title",
|
||||||
|
`Status: ${client.status}\nLast Seen: ${lastSeen.toISOString()}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
li.append(a);
|
||||||
|
clientList.append(li);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove <li> for clients no longer present
|
||||||
|
Object.keys(existingItems).forEach((clientId) => {
|
||||||
|
if (!seen.has(clientId)) {
|
||||||
|
existingItems[clientId].remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-bind click handlers
|
||||||
|
$("#client-list li > a")
|
||||||
|
.off("click")
|
||||||
|
.on("click", function (e) {
|
||||||
|
let clientId = $(this).attr("href").substring(1);
|
||||||
|
// this is client=clientId
|
||||||
|
|
||||||
|
clientId = clientId.replace("client=", "");
|
||||||
|
|
||||||
|
loadClientDetails(clientId);
|
||||||
|
$("#client-list a").removeClass("bg-ctp-surface0");
|
||||||
|
$(this).addClass("bg-ctp-surface0");
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
let newUrl = `${window.location.pathname}?client=${clientId}`;
|
||||||
|
window.history.pushState({ path: newUrl }, "", newUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
39
src/judas_server/web/templates/base/base.html
Normal file
39
src/judas_server/web/templates/base/base.html
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>{% block title %}{% endblock %} – judas</title>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="{{ url_for('static', filename='css/style.css') }}"
|
||||||
|
/>
|
||||||
|
<link rel="stylesheet" href="https://cdn-uicons.flaticon.com/2.6.0/uicons-regular-rounded/css/uicons-regular-rounded.css" />
|
||||||
|
<link rel="stylesheet" href="https://cdn-uicons.flaticon.com/2.6.0/uicons-regular-straight/css/uicons-regular-straight.css" />
|
||||||
|
<link rel="stylesheet" href="https://cdn-uicons.flaticon.com/2.6.0/uicons-solid-rounded/css/uicons-solid-rounded.css" />
|
||||||
|
<link rel="stylesheet" href="https://cdn-uicons.flaticon.com/2.6.0/uicons-solid-straight/css/uicons-solid-straight.css" />
|
||||||
|
<link rel="stylesheet" href="https://cdn-uicons.flaticon.com/2.6.0/uicons-bold-rounded/css/uicons-bold-rounded.css" />
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||||
|
<script src="https://cdn.socket.io/4.8.1/socket.io.min.js"></script>
|
||||||
|
<script>
|
||||||
|
const socket = io();
|
||||||
|
socket.on("connect", () => {
|
||||||
|
console.log("Connected to server");
|
||||||
|
$("#no-connection-message").hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("disconnect", () => {
|
||||||
|
console.log("Disconnected from server");
|
||||||
|
$("#no-connection-message").show();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% include "base/header.html" %}
|
||||||
|
<!-- -->
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
<!-- -->
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
13
src/judas_server/web/templates/base/header.html
Normal file
13
src/judas_server/web/templates/base/header.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<header class="flex items-center justify-between px-4 py-2 bg-ctp-crust">
|
||||||
|
<h2 class="text-2xl font-bold font-mono text-ctp-blue"><a href="{{ url_for('index.index') }}">judas</a></h2>
|
||||||
|
<p id="no-connection-message" class="flex align-center justify-center gap-2 text-ctp-red">
|
||||||
|
<i class="fi fi-rr-link-slash text-xl"></i>
|
||||||
|
<span> No connection to server </span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<a class="btn-primary" href="{{ url_for('auth.logout') }}">Logout</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="button" href="{{ url_for('auth.login') }}">Login</a>
|
||||||
|
{% endif %}
|
||||||
|
</header>
|
||||||
@@ -1,37 +1,46 @@
|
|||||||
<!doctype html>
|
<div class="details-header">
|
||||||
<html lang="en">
|
<p>Machine {{ client.id }}</p>
|
||||||
<head>
|
</div>
|
||||||
<meta charset="UTF-8" />
|
<div class="details-container">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<div class="details-sidebar">
|
||||||
<title>Details for client {{ client.id }}</title>
|
<ul>
|
||||||
<link
|
<li>
|
||||||
rel="stylesheet"
|
<a href="?tab=summary">
|
||||||
href="{{ url_for('static', filename='css/style.css') }}"
|
<i class="fi fi-ss-summary-check"></i>
|
||||||
/>
|
<span>Summary</span>
|
||||||
</head>
|
</a>
|
||||||
<body>
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="details-content">
|
||||||
<h1>Details for client {{ client.id }}</h1>
|
<h1>Details for client {{ client.id }}</h1>
|
||||||
<p><strong>ID:</strong> {{ client.id }}</p>
|
|
||||||
<p><strong>IP Address:</strong> {{ client.addr[0] }}</p>
|
|
||||||
<p><strong>Port:</strong> {{ client.addr[1] }}</p>
|
|
||||||
<p>
|
<p>
|
||||||
<strong>Last Seen:</strong>
|
<strong>Last Seen:</strong>
|
||||||
<span id="last-seen">{{ client.last_seen }}</span>
|
<span id="last-seen">{{ client.last_seen }}</span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
</div>
|
||||||
<strong>Initial:</strong>
|
</div>
|
||||||
<pre>{{ client.initial_telemetry | tojson(indent=2) }}</pre>
|
|
||||||
</p>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function updateLastSeen() {
|
function updateLastSeen() {
|
||||||
const lastSeenElement = document.getElementById("last-seen");
|
const lastSeenElement = document.getElementById("last-seen");
|
||||||
const lastSeenTimestamp = parseInt(lastSeenElement.textContent);
|
const lastSeenTimestamp = parseInt(lastSeenElement.textContent);
|
||||||
const lastSeenDate = new Date(lastSeenTimestamp * 1000);
|
const lastSeenDate = new Date(lastSeenTimestamp * 1000);
|
||||||
lastSeenElement.textContent = lastSeenDate.toLocaleString();
|
lastSeenElement.textContent = lastSeenDate.toLocaleString();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLastSeen();
|
$(".details-sidebar > ul > li > a").click(function (e) {
|
||||||
</script>
|
e.preventDefault();
|
||||||
</html>
|
const tab = $(this).attr("href").split("=")[1];
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
urlParams.set("tab", tab);
|
||||||
|
window.history.pushState(
|
||||||
|
{},
|
||||||
|
"",
|
||||||
|
`${window.location.pathname}?${urlParams}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateLastSeen();
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,56 +1,42 @@
|
|||||||
<!DOCTYPE html>
|
{% extends "base/base.html" %}
|
||||||
<html lang="en">
|
|
||||||
<head>
|
{% block title %}home{% endblock %}
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
{% block content %}
|
||||||
<title>judas</title>
|
<div id="content" class="center">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
<div style="margin-top: 2rem">
|
||||||
</head>
|
<p>Welcome to</p>
|
||||||
<body>
|
<h2 id="typing-text" style="font-size: 3rem">judas</h2>
|
||||||
<div id="wrapper">
|
<p>a remote PC fleet management system</p>
|
||||||
<header>
|
|
||||||
<h2><a href="{{ url_for('index.index') }}">judas</a></h2>
|
|
||||||
{% if logged %}
|
|
||||||
<p><a class="button" href="{{ url_for('auth.logout') }}">Logout</a></p>
|
|
||||||
{% else %}
|
|
||||||
<p><a class="button" href="{{ url_for('auth.login') }}">Login</a></p>
|
|
||||||
{% endif %}
|
|
||||||
</header>
|
|
||||||
<main class="center">
|
|
||||||
<div id="content">
|
|
||||||
<div>
|
|
||||||
<p>Welcome to</p>
|
|
||||||
<h2 id="typing-text" style="font-size: 3rem;">judas</h2>
|
|
||||||
<p>a remote PC fleet management system</p>
|
|
||||||
</div>
|
|
||||||
<p style="color: var(--ctp-red);"><strong>Notice:</strong> Please use this system responsibly and in accordance with all applicable laws and organizational policies.</p>
|
|
||||||
{% if logged %}
|
|
||||||
<p><a class="button" href="{{ url_for('panel.panel') }}">Go to panel</a></p>
|
|
||||||
{% else %}
|
|
||||||
<p>Please <a href="{{ url_for('auth.login')}}" class="link">log in</a> to manage your remote PCs.</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<p style="color: var(--ctp-red)"><strong>Notice:</strong> Please use this system responsibly and in accordance with all applicable laws and organizational policies.</p>
|
||||||
var i = 0;
|
{% if logged %}
|
||||||
var txt = document.getElementById("typing-text").innerHTML;
|
<p><a class="button" href="{{ url_for('panel.panel') }}">Go to panel</a></p>
|
||||||
var minSpeed = 50;
|
{% else %}
|
||||||
var maxSpeed = 200;
|
<p>Please <a href="{{ url_for('auth.login')}}" class="link">log in</a> to manage your remote PCs.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
document.getElementById("typing-text").innerHTML = "";
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
var i = 0;
|
||||||
|
var txt = document.getElementById("typing-text").innerHTML;
|
||||||
|
var minSpeed = 50;
|
||||||
|
var maxSpeed = 200;
|
||||||
|
|
||||||
function typeWriter() {
|
document.getElementById("typing-text").innerHTML = "";
|
||||||
if (i < txt.length) {
|
|
||||||
document.getElementById("typing-text").innerHTML += txt.charAt(i);
|
|
||||||
i++;
|
|
||||||
|
|
||||||
var randomDelay = Math.floor(Math.random() * (maxSpeed - minSpeed + 1)) + minSpeed;
|
function typeWriter() {
|
||||||
setTimeout(typeWriter, randomDelay);
|
if (i < txt.length) {
|
||||||
}
|
document.getElementById("typing-text").innerHTML += txt.charAt(i);
|
||||||
|
i++;
|
||||||
|
|
||||||
|
var randomDelay = Math.floor(Math.random() * (maxSpeed - minSpeed + 1)) + minSpeed;
|
||||||
|
setTimeout(typeWriter, randomDelay);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
typeWriter();
|
typeWriter();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
{% endblock %}
|
||||||
</html>
|
|
||||||
|
|||||||
@@ -1,203 +1,16 @@
|
|||||||
<!doctype html>
|
{% extends "base/base.html" %}
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>judas panel</title>
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="{{ url_for('static', filename='css/style.css') }}"
|
|
||||||
/>
|
|
||||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
|
||||||
<script src="https://cdn.socket.io/4.8.1/socket.io.min.js"></script>
|
|
||||||
<script>
|
|
||||||
$(document).ready(function () {
|
|
||||||
const socket = io();
|
|
||||||
|
|
||||||
const showNotify = (message) => {
|
{% block title %}panel{% endblock %}
|
||||||
$("#notify").stop().fadeIn();
|
|
||||||
$("#notify").text(message);
|
|
||||||
setTimeout(() => {
|
|
||||||
$("#notify").fadeOut();
|
|
||||||
}, 3000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadClientDetails = (clientId) => {
|
{% block content %}
|
||||||
fetch(`/client/${clientId}`)
|
<main class="flex grow h-full">
|
||||||
.then((response) => response.text())
|
<aside class="border-r-4 border-r-ctp-mantle">
|
||||||
.then((html) => {
|
<ul id="client-list"></ul>
|
||||||
$("#content").html(html);
|
</aside>
|
||||||
})
|
<div id="content" class="grow"></div>
|
||||||
.catch((error) => {
|
</main>
|
||||||
console.error("Error fetching client details:", error);
|
{% endblock %}
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// load client_details for the client specified in the URL hash
|
{% block scripts %}
|
||||||
const hash = window.location.hash;
|
<script src="{{ url_for('static', filename='js/panel.js') }}"></script>
|
||||||
if (hash) {
|
{% endblock %}
|
||||||
const clientId = hash.substring(1);
|
|
||||||
loadClientDetails(clientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#notify").hide();
|
|
||||||
|
|
||||||
socket.on("connect", () => {
|
|
||||||
console.log("Connected to server");
|
|
||||||
$("#no-connection-message").hide();
|
|
||||||
showNotify("Connected to server");
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
|
||||||
console.log("Disconnected from server");
|
|
||||||
$("#no-connection-message").show();
|
|
||||||
showNotify("Disconnected from server");
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("update_data", (data) => {
|
|
||||||
const clientList = $("#client-list");
|
|
||||||
const existingItems = {};
|
|
||||||
|
|
||||||
// Index current <li> by clientId
|
|
||||||
clientList.children("li").each(function () {
|
|
||||||
const a = $(this).find("a");
|
|
||||||
if (a.length) {
|
|
||||||
const clientId = a.attr("href").substring(1);
|
|
||||||
existingItems[clientId] = $(this);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Track which clientIds are still present
|
|
||||||
const seen = new Set();
|
|
||||||
|
|
||||||
Object.entries(data).forEach(([clientId, client]) => {
|
|
||||||
seen.add(clientId);
|
|
||||||
|
|
||||||
// Build icon
|
|
||||||
const iconElement = document.createElement("i");
|
|
||||||
switch (client.status) {
|
|
||||||
case "online":
|
|
||||||
iconElement.className = "fi fi-sr-play";
|
|
||||||
iconElement.style.color = "var(--ctp-green)";
|
|
||||||
break;
|
|
||||||
case "offline":
|
|
||||||
iconElement.className = "fi fi-sr-stop";
|
|
||||||
iconElement.style.color = "var(--ctp-red)";
|
|
||||||
break;
|
|
||||||
case "pending":
|
|
||||||
iconElement.className = "fi fi-sr-pending";
|
|
||||||
iconElement.style.color = "var(--ctp-yellow)";
|
|
||||||
break;
|
|
||||||
case "stale":
|
|
||||||
iconElement.className = "fi fi-sr-skull";
|
|
||||||
iconElement.style.color = "var(--ctp-orange)";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
iconElement.className = "fi fi-rr-question";
|
|
||||||
iconElement.style.color = "var(--ctp-gray)";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Time since last seen
|
|
||||||
const lastSeen = new Date(client.last_seen * 1000);
|
|
||||||
const now = new Date();
|
|
||||||
const timeSinceLastSeen = Math.floor((now - lastSeen) / 1000);
|
|
||||||
const days = Math.floor(timeSinceLastSeen / 86400);
|
|
||||||
const hours = Math.floor((timeSinceLastSeen % 86400) / 3600);
|
|
||||||
const minutes = Math.floor((timeSinceLastSeen % 3600) / 60);
|
|
||||||
const seconds = timeSinceLastSeen % 60;
|
|
||||||
let timeSinceLastSeenText = "";
|
|
||||||
if (days > 0) timeSinceLastSeenText += `${days}d `;
|
|
||||||
if (hours > 0) timeSinceLastSeenText += `${hours}h `;
|
|
||||||
if (minutes > 0) timeSinceLastSeenText += `${minutes}m `;
|
|
||||||
if (seconds > 0) timeSinceLastSeenText += `${seconds}s`;
|
|
||||||
|
|
||||||
timeSinceLastSeenText = timeSinceLastSeenText.trim() || "0s";
|
|
||||||
|
|
||||||
let statusText = `${client.id} (${timeSinceLastSeenText})`;
|
|
||||||
|
|
||||||
// check if <li> exists
|
|
||||||
if (existingItems[clientId]) {
|
|
||||||
// update if needed
|
|
||||||
const li = existingItems[clientId];
|
|
||||||
const a = li.find("a");
|
|
||||||
if (a.text() !== statusText) {
|
|
||||||
a.text(statusText);
|
|
||||||
}
|
|
||||||
|
|
||||||
li.attr(
|
|
||||||
"title",
|
|
||||||
`Status: ${client.status}\nLast Seen: ${lastSeen.toISOString()}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// update icon
|
|
||||||
const icon = li.find("i")[0];
|
|
||||||
if (icon) {
|
|
||||||
icon.className = iconElement.className;
|
|
||||||
icon.style.color = iconElement.style.color;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// add new <li>
|
|
||||||
const li = $("<li></li>");
|
|
||||||
li.append(iconElement);
|
|
||||||
const a = $("<a></a>")
|
|
||||||
.text(statusText)
|
|
||||||
.attr("href", `#${clientId}`);
|
|
||||||
|
|
||||||
li.attr(
|
|
||||||
"title",
|
|
||||||
`Status: ${client.status}\nLast Seen: ${lastSeen.toISOString()}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
li.append(a);
|
|
||||||
clientList.append(li);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove <li> for clients no longer present
|
|
||||||
Object.keys(existingItems).forEach((clientId) => {
|
|
||||||
if (!seen.has(clientId)) {
|
|
||||||
existingItems[clientId].remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Re-bind click handlers
|
|
||||||
$("#client-list li > a")
|
|
||||||
.off("click")
|
|
||||||
.on("click", function (e) {
|
|
||||||
const href = $(this).attr("href");
|
|
||||||
if (href.startsWith("#")) {
|
|
||||||
const clientId = href.substring(1);
|
|
||||||
loadClientDetails(clientId);
|
|
||||||
$("#client-list li > a").removeClass("active");
|
|
||||||
$(this).addClass("active");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.location.hash) {
|
|
||||||
const clientId = window.location.hash.substring(1);
|
|
||||||
loadClientDetails(clientId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="wrapper">
|
|
||||||
<header>
|
|
||||||
<h2><a href="{{ url_for('index.index') }}">judas</a></h2>
|
|
||||||
<p id="no-connection-message">
|
|
||||||
<i class="fi fi-rr-link-slash"></i>
|
|
||||||
<span> No connection to server </span>
|
|
||||||
</p>
|
|
||||||
<a class="button" href="{{ url_for('auth.logout') }}">Logout</a>
|
|
||||||
</header>
|
|
||||||
<div id="notify"></div>
|
|
||||||
<main>
|
|
||||||
<aside>
|
|
||||||
<ul id="client-list"></ul>
|
|
||||||
</aside>
|
|
||||||
<div id="content"></div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@@ -68,7 +68,9 @@ class JudasWebServer:
|
|||||||
self.app.register_blueprint(client_details.bp)
|
self.app.register_blueprint(client_details.bp)
|
||||||
api.emit_polled_data(self.app, self.socketio)
|
api.emit_polled_data(self.app, self.socketio)
|
||||||
|
|
||||||
def run(self, host: str = "0.0.0.0", port: int = 5000) -> None:
|
def run(
|
||||||
|
self, host: str = "0.0.0.0", port: int = 5000, debug: bool = False
|
||||||
|
) -> None:
|
||||||
self.logger.info(f"Starting web server on {host}:{port}...")
|
self.logger.info(f"Starting web server on {host}:{port}...")
|
||||||
self.socketio.run(app=self.app, host=host, port=port)
|
self.socketio.run(app=self.app, host=host, port=port, debug=debug)
|
||||||
self.logger.info("Server stopped.")
|
self.logger.info("Server stopped.")
|
||||||
|
|||||||
Reference in New Issue
Block a user