Merge pull request 'feat: improve templates' (#17) from feat/improve-templates into develop

Reviewed-on: #17
This commit is contained in:
2026-03-12 20:16:05 +00:00
9 changed files with 310 additions and 286 deletions

View File

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

View File

@@ -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 {

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

View File

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

View File

@@ -1,204 +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 {% block scripts %}
const urlParams = new URLSearchParams(window.location.search); <script src="{{ url_for('static', filename='js/panel.js') }}"></script>
const clientId = urlParams.get("client"); {% endblock %}
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";
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", `?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("active");
$(this).addClass("active");
e.preventDefault();
let newUrl = `${window.location.pathname}?client=${clientId}`;
window.history.pushState({ path: newUrl }, "", newUrl);
});
});
});
</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>

View File

@@ -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.")