23 Commits
0.6.0 ... 0.7.0

Author SHA1 Message Date
github-actions[bot]
2a85084c8f chore(release): 0.7.0 2026-03-12 20:26:12 +00:00
6cc6dc5b42 Merge pull request 'chore(release): 0.7.0' (#18) from release/0.7.0 into main
Reviewed-on: #18
2026-03-12 20:17:15 +00:00
a4c07d9d2d Merge pull request 'feat: improve templates' (#17) from feat/improve-templates into develop
Reviewed-on: #17
2026-03-12 20:16:05 +00:00
a697ae6661 fix(ack_handler.py): fix error by importing annotations 2026-03-12 21:14:34 +01:00
acbcb3364e feat(web_server.py): add debug parameter to JudasWebServer.run() 2026-03-12 20:43:29 +01:00
762256c3cd fix(client_details.html): fix up code for AJAX loading 2026-03-12 20:42:45 +01:00
c442dca520 refactor(panel.html): use new base template 2026-03-12 20:42:15 +01:00
12c5de9f11 refactor(index.html): use new base template 2026-03-12 20:42:03 +01:00
aa562a0eab chore(base.html): remove leftover code 2026-03-12 20:41:33 +01:00
865112c823 chore(style.css): adjust styles 2026-03-12 20:41:08 +01:00
1d764bd77d feat(header.html): add header template 2026-03-12 20:34:30 +01:00
6f4bc3aa0f feat(base.html): add base template 2026-03-12 20:34:17 +01:00
14ea136fbb feat(panel.js): move js from inline to separate script 2026-03-12 20:33:38 +01:00
78f9508753 feat(panel.html): use ?query instead of #hash for client selection 2026-03-09 21:19:05 +01:00
github-actions[bot]
11bf344cb5 chore(release): 0.7.0-dev.1 2026-03-08 19:47:31 +00:00
5ee8eca11b Merge pull request 'chore(release): 0.7.0-dev.1' (#16) from release/0.7.0-dev.1 into main
Reviewed-on: #16
2026-03-08 19:46:56 +00:00
f18935f793 Merge pull request 'feat/add-basic-telemetry' (#15) from feat/add-basic-telemetry into develop
Reviewed-on: #15
2026-03-08 19:43:13 +00:00
d7b136851b feat(client_details.html): add temporary initial telemetry display 2026-03-08 20:38:59 +01:00
7e9a9e6eed feat(backend_server.py): add initial_telemetry support 2026-03-08 20:38:32 +01:00
6ed03ab74d feat(client.py): add Client.initial_telemetry property 2026-03-08 20:38:01 +01:00
40c08d0169 feat(initial_handler.py): add handler for TELEMETRY/INTIIAL msgs 2026-03-08 20:37:26 +01:00
3d13d24116 build(uv.lock): update judas_protocol to 0.9.1 2026-03-05 22:29:48 +01:00
28b57b6964 build(uv.lock): update judas_protocol to 0.9.0 2026-03-05 21:26:34 +01:00
15 changed files with 402 additions and 285 deletions

View File

@@ -2,6 +2,45 @@
All notable changes to this project will be documented in this file.
## [0.7.0] - 2026-03-12
### Bug Fixes
- [`a697ae6`](https://github.com/pufereq/template-repo/commit/a697ae666114ed0adfa78db7accfd81d26850111) **ack_handler.py**: fix error by importing annotations
- [`762256c`](https://github.com/pufereq/template-repo/commit/762256c3cdee4bfc8dbdbfa6d2997656486f9557) **client_details.html**: fix up code for AJAX loading
### Features
- [`acbcb33`](https://github.com/pufereq/template-repo/commit/acbcb3364e7b682bb67ee329d12626e9df0d627d) **web_server.py**: add `debug` parameter to `JudasWebServer.run()`
- [`1d764bd`](https://github.com/pufereq/template-repo/commit/1d764bd77d031123054ab86f073fb27823363532) **header.html**: add header template
- [`6f4bc3a`](https://github.com/pufereq/template-repo/commit/6f4bc3aa0f7f1a4aefe8b2d8405d4fc49c6c744d) **base.html**: add base template
- [`14ea136`](https://github.com/pufereq/template-repo/commit/14ea136fbbad447d1eb668fcc09aadb75de88ee3) **panel.js**: move js from inline to separate script
- [`78f9508`](https://github.com/pufereq/template-repo/commit/78f9508753ae47c681cff115fb960bea6bb12e20) **panel.html**: use ?query instead of #hash for client selection
### Miscellaneous Tasks
- [`aa562a0`](https://github.com/pufereq/template-repo/commit/aa562a0eab3a4ce10f225d03c9927ace6ee66678) **base.html**: remove leftover code
- [`865112c`](https://github.com/pufereq/template-repo/commit/865112c823a65d761d929c69574c47b57559804d) **style.css**: adjust styles
### Refactor
- [`c442dca`](https://github.com/pufereq/template-repo/commit/c442dca520ff381b17cd49863e5189066882f73d) **panel.html**: use new base template
- [`12c5de9`](https://github.com/pufereq/template-repo/commit/12c5de9f114d955c6b309f6020829595d125bec2) **index.html**: use new base template
## [0.7.0-dev.1] - 2026-03-08
### Features
- [`d7b1368`](https://github.com/pufereq/template-repo/commit/d7b136851bafa2c20e1634bd5568f4bac839177f) **client_details.html**: add temporary initial telemetry display
- [`7e9a9e6`](https://github.com/pufereq/template-repo/commit/7e9a9e6eede6cc926fef64c626434e65984befbc) **backend_server.py**: add `initial_telemetry` support
- [`6ed03ab`](https://github.com/pufereq/template-repo/commit/6ed03ab74de8e91d13b5f1971a3f4cec890e4fef) **client.py**: add `Client.initial_telemetry` property
- [`40c08d0`](https://github.com/pufereq/template-repo/commit/40c08d01693973f29f13c133a11fc5f166891a25) **initial_handler.py**: add handler for `TELEMETRY/INTIIAL` msgs
### Build
- [`3d13d24`](https://github.com/pufereq/template-repo/commit/3d13d241168b011c0044eb64db4b0fe70878d748) **uv.lock**: update judas_protocol to 0.9.1
- [`28b57b6`](https://github.com/pufereq/template-repo/commit/28b57b6964bfcd6ce78f2a77822f7221f6e4f7e5) **uv.lock**: update judas_protocol to 0.9.0
## [0.6.0] - 2026-03-05
### Bug Fixes

View File

@@ -4,7 +4,7 @@ build-backend = "uv_build"
[project]
name = "judas_server"
version = "0.6.0"
version = "0.7.0"
description = "The backbone of the remote PC fleet management system."
readme = "README.md"
authors = []

View File

@@ -8,11 +8,15 @@ import threading
import time
from typing import TYPE_CHECKING, Any, Final
from judas_protocol.types import TelemetryAction
import yaml
from judas_protocol import Category, ControlAction, Message
from judas_server.backend.client import Client, ClientStatus
from judas_server.backend.handler.hello_handler import HelloHandler
from judas_server.backend.handler.telemetry.initial_handler import (
InitialTelemetryHandler,
)
if TYPE_CHECKING:
from typing import Callable
@@ -67,11 +71,16 @@ class BackendServer:
"""Initialize message handlers."""
hello_handler = HelloHandler(self)
initial_telemetry_handler = InitialTelemetryHandler(self)
self.message_handlers[(Category.CONTROL, ControlAction.HELLO)] = (
hello_handler.handle
)
self.message_handlers[
(Category.TELEMETRY, TelemetryAction.INITIAL)
] = initial_telemetry_handler.handle
def _load_known_clients(self) -> dict[str, dict[str, str | float]]:
"""Load the list of known clients from a YAML file and validate."""
known_clients: dict[str, dict[str, str | float]] = {}
@@ -108,6 +117,9 @@ class BackendServer:
client.last_seen = float(
known_clients[client_id].get("last_seen", 0.0)
)
client.initial_telemetry = known_clients[client_id].get( # type: ignore
"initial_telemetry", None
)
self.clients[client_id] = client
except FileNotFoundError:
@@ -399,4 +411,5 @@ class BackendServer:
"addr": client.addr,
"last_seen": client.last_seen,
"status": client.status,
"initial_telemetry": client.initial_telemetry,
}

View File

@@ -6,6 +6,7 @@ from __future__ import annotations
import logging as lg
import socket
import time
from typing import Any
from judas_server.backend.client_status import ClientStatus
@@ -41,6 +42,8 @@ class Client:
self.inbound: bytes = b""
self.outbound: bytes = b""
self.initial_telemetry: dict[str, Any] | None = None
def __str__(self) -> str:
if self.addr:
return f"Client({self.id} ({self.addr[0]}:{self.addr[1]}))"

View File

@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING

View File

@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
"""Initial telemetry handler."""
from __future__ import annotations
from typing import TYPE_CHECKING
from judas_protocol import Message
from judas_server.backend.handler.base_handler import BaseHandler
if TYPE_CHECKING:
from judas_server.backend import BackendServer
from judas_server.backend.client import Client
class InitialTelemetryHandler(BaseHandler):
"""Handles the initial telemetry message from a client."""
def __init__(self, backend_server: BackendServer) -> None:
"""Initialize the handler."""
super().__init__(backend_server)
def handle(self, client: Client, message: Message) -> None:
"""Handle the initial telemetry message."""
self.logger.debug(
f"Handling initial telemetry message from {client}..."
)
client.initial_telemetry = message.payload
self.backend_server.known_clients[client.id]["initial_telemetry"] = ( # type: ignore
message.payload
)

View File

@@ -16,6 +16,7 @@ body {
background-color: var(--ctp-base);
font-family: sans-serif;
color: var(--ctp-text);
min-height: 100vh;
}
.fi {
@@ -62,15 +63,13 @@ main {
aside {
width: 20rem;
background-color: var(--ctp-base);
border-right: 2px solid var(--ctp-mantle);
border-right: 4px solid var(--ctp-mantle);
}
#content {
display: flex;
flex-direction: column;
gap: 1rem;
flex-grow: 1;
padding: 1rem;
}
header a {

View 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);
});
});
});

View 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>

View 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>

View File

@@ -1,33 +1,46 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Details for client {{ client.id }}</title>
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/style.css') }}"
/>
</head>
<body>
<div class="details-header">
<p>Machine {{ client.id }}</p>
</div>
<div class="details-container">
<div class="details-sidebar">
<ul>
<li>
<a href="?tab=summary">
<i class="fi fi-ss-summary-check"></i>
<span>Summary</span>
</a>
</li>
</ul>
</div>
<div class="details-content">
<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>
<strong>Last Seen:</strong>
<span id="last-seen">{{ client.last_seen }}</span>
</p>
</body>
</div>
</div>
<script>
function updateLastSeen() {
const lastSeenElement = document.getElementById("last-seen");
const lastSeenTimestamp = parseInt(lastSeenElement.textContent);
const lastSeenDate = new Date(lastSeenTimestamp * 1000);
lastSeenElement.textContent = lastSeenDate.toLocaleString();
}
<script>
function updateLastSeen() {
const lastSeenElement = document.getElementById("last-seen");
const lastSeenTimestamp = parseInt(lastSeenElement.textContent);
const lastSeenDate = new Date(lastSeenTimestamp * 1000);
lastSeenElement.textContent = lastSeenDate.toLocaleString();
}
updateLastSeen();
</script>
</html>
$(".details-sidebar > ul > li > a").click(function (e) {
e.preventDefault();
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>

View File

@@ -1,56 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>judas</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<div id="wrapper">
<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>
{% extends "base/base.html" %}
{% block title %}home{% endblock %}
{% block content %}
<div id="content" class="center">
<div style="margin-top: 2rem">
<p>Welcome to</p>
<h2 id="typing-text" style="font-size: 3rem">judas</h2>
<p>a remote PC fleet management system</p>
</div>
<script>
var i = 0;
var txt = document.getElementById("typing-text").innerHTML;
var minSpeed = 50;
var maxSpeed = 200;
<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>
{% 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() {
if (i < txt.length) {
document.getElementById("typing-text").innerHTML += txt.charAt(i);
i++;
document.getElementById("typing-text").innerHTML = "";
var randomDelay = Math.floor(Math.random() * (maxSpeed - minSpeed + 1)) + minSpeed;
setTimeout(typeWriter, randomDelay);
}
function typeWriter() {
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();
</script>
</body>
</html>
typeWriter();
</script>
{% endblock %}

View File

@@ -1,203 +1,16 @@
<!doctype 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();
{% extends "base/base.html" %}
const showNotify = (message) => {
$("#notify").stop().fadeIn();
$("#notify").text(message);
setTimeout(() => {
$("#notify").fadeOut();
}, 3000);
};
{% block title %}panel{% endblock %}
const loadClientDetails = (clientId) => {
fetch(`/client/${clientId}`)
.then((response) => response.text())
.then((html) => {
$("#content").html(html);
})
.catch((error) => {
console.error("Error fetching client details:", error);
});
};
{% block content %}
<main class="flex grow h-full">
<aside class="border-r-4 border-r-ctp-mantle">
<ul id="client-list"></ul>
</aside>
<div id="content" class="grow"></div>
</main>
{% endblock %}
// load client_details for the client specified in the URL hash
const hash = window.location.hash;
if (hash) {
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>
{% block scripts %}
<script src="{{ url_for('static', filename='js/panel.js') }}"></script>
{% endblock %}

View File

@@ -68,7 +68,9 @@ class JudasWebServer:
self.app.register_blueprint(client_details.bp)
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.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.")

6
uv.lock generated
View File

@@ -358,12 +358,12 @@ wheels = [
[[package]]
name = "judas-protocol"
version = "0.8.0"
source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#a805ccf38edffadc1b8c8b276e60758c86516cd3" }
version = "0.9.1"
source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#085c34f232f95313d66db48a7d17bc25c92a35ae" }
[[package]]
name = "judas-server"
version = "0.6.0"
version = "0.7.0"
source = { editable = "." }
dependencies = [
{ name = "flask" },