diff --git a/src/judas_server/backend/handler/ack_handler.py b/src/judas_server/backend/handler/ack_handler.py index 8204be0..1756b80 100644 --- a/src/judas_server/backend/handler/ack_handler.py +++ b/src/judas_server/backend/handler/ack_handler.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import annotations from typing import TYPE_CHECKING diff --git a/src/judas_server/web/static/css/style.css b/src/judas_server/web/static/css/style.css index 2f7664d..202b855 100644 --- a/src/judas_server/web/static/css/style.css +++ b/src/judas_server/web/static/css/style.css @@ -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 { diff --git a/src/judas_server/web/static/js/panel.js b/src/judas_server/web/static/js/panel.js new file mode 100644 index 0000000..4e11461 --- /dev/null +++ b/src/judas_server/web/static/js/panel.js @@ -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
  • 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
  • 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
  • + const li = $("
  • "); + li.append(iconElement); + const a = $("") + .text(statusText) + .attr("href", `?client=${clientId}`); + + li.attr( + "title", + `Status: ${client.status}\nLast Seen: ${lastSeen.toISOString()}`, + ); + + li.append(a); + clientList.append(li); + } + }); + + // Remove
  • 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); + }); + }); +}); diff --git a/src/judas_server/web/templates/base/base.html b/src/judas_server/web/templates/base/base.html new file mode 100644 index 0000000..27c5dfb --- /dev/null +++ b/src/judas_server/web/templates/base/base.html @@ -0,0 +1,39 @@ + + + + + + {% block title %}{% endblock %} – judas + + + + + + + + + + + + + {% include "base/header.html" %} + + {% block content %}{% endblock %} + + {% block scripts %}{% endblock %} + + diff --git a/src/judas_server/web/templates/base/header.html b/src/judas_server/web/templates/base/header.html new file mode 100644 index 0000000..faabfaf --- /dev/null +++ b/src/judas_server/web/templates/base/header.html @@ -0,0 +1,13 @@ +
    +

    judas

    +

    + + No connection to server +

    + + {% if current_user.is_authenticated %} + Logout + {% else %} + Login + {% endif %} +
    diff --git a/src/judas_server/web/templates/client_details.html b/src/judas_server/web/templates/client_details.html index 2fec335..9f3fd6c 100644 --- a/src/judas_server/web/templates/client_details.html +++ b/src/judas_server/web/templates/client_details.html @@ -1,37 +1,46 @@ - - - - - - Details for client {{ client.id }} - - - +
    +

    Machine {{ client.id }}

    +
    +
    +
    + +
    +

    Details for client {{ client.id }}

    -

    ID: {{ client.id }}

    -

    IP Address: {{ client.addr[0] }}

    -

    Port: {{ client.addr[1] }}

    +

    Last Seen: {{ client.last_seen }}

    -

    - Initial: -

    {{ client.initial_telemetry | tojson(indent=2) }}
    -

    - +
    +
    - - + $(".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(); + diff --git a/src/judas_server/web/templates/index.html b/src/judas_server/web/templates/index.html index e4629b8..7c93963 100644 --- a/src/judas_server/web/templates/index.html +++ b/src/judas_server/web/templates/index.html @@ -1,56 +1,42 @@ - - - - - - judas - - - -
    -
    -

    judas

    - {% if logged %} -

    Logout

    - {% else %} -

    Login

    - {% endif %} -
    -
    -
    -
    -

    Welcome to

    -

    judas

    -

    a remote PC fleet management system

    -
    -

    Notice: Please use this system responsibly and in accordance with all applicable laws and organizational policies.

    - {% if logged %} -

    Go to panel

    - {% else %} -

    Please log in to manage your remote PCs.

    - {% endif %} -
    -
    +{% extends "base/base.html" %} + +{% block title %}home{% endblock %} + +{% block content %} +
    +
    +

    Welcome to

    +

    judas

    +

    a remote PC fleet management system

    - - - + typeWriter(); + +{% endblock %} diff --git a/src/judas_server/web/templates/panel.html b/src/judas_server/web/templates/panel.html index d5100b3..2582ef6 100644 --- a/src/judas_server/web/templates/panel.html +++ b/src/judas_server/web/templates/panel.html @@ -1,204 +1,16 @@ - - - - - - judas panel - - - - - - -
    -
    -

    judas

    -

    - - No connection to server -

    - Logout -
    -
    -
    - -
    -
    -
    - - +{% block scripts %} + +{% endblock %} diff --git a/src/judas_server/web/web_server.py b/src/judas_server/web/web_server.py index 595b009..81ae471 100644 --- a/src/judas_server/web/web_server.py +++ b/src/judas_server/web/web_server.py @@ -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.")