16 Commits

Author SHA1 Message Date
68bb05a482 feat: move modules from judas_server/backend to judas_server/ 2026-03-12 21:55:07 +01:00
f49c33974d chore(web): remove web code 2026-03-12 21:47:23 +01:00
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
29 changed files with 53 additions and 990 deletions

View File

@@ -2,6 +2,31 @@
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

View File

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

View File

@@ -1 +1,7 @@
from .backend_server import BackendServer
__version__: str = "0.5.0"
__all__ = [
"BackendServer",
]

View File

@@ -3,9 +3,8 @@
import logging as lg
if __name__ == "__main__":
from judas_server.backend import BackendServer
from judas_server import BackendServer
from judas_server.gaga import LADY_GAGA
from judas_server.web.web_server import JudasWebServer
lg.basicConfig(
level=lg.DEBUG,
@@ -22,11 +21,3 @@ if __name__ == "__main__":
port=3692,
)
backend_server.run()
web_server: JudasWebServer = JudasWebServer(
backend=backend_server, secret_key="dildo"
)
web_server.run(
host="0.0.0.0",
port=5000,
)

View File

@@ -1,5 +0,0 @@
from .backend_server import BackendServer
from .client import Client
from .client_status import ClientStatus
__all__ = ["BackendServer", "Client", "ClientStatus"]

View File

@@ -12,9 +12,9 @@ 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 (
from judas_server.client import Client, ClientStatus
from judas_server.handler.hello_handler import HelloHandler
from judas_server.handler.telemetry.initial_handler import (
InitialTelemetryHandler,
)
@@ -348,9 +348,7 @@ class BackendServer:
def run(self) -> None:
"""Start the backend server."""
self.running = True
threading.Thread(
name="BackendServer thread", target=self._loop, daemon=True
).start()
self._loop()
def _loop(self) -> None:
"""Main server loop to handle incoming connections and data."""
@@ -396,6 +394,9 @@ class BackendServer:
except Exception as e:
self.logger.error(f"Server error: {e}")
raise e
except KeyboardInterrupt:
self.logger.info("Keyboard interrupt received, stopping server...")
self.running = False
finally:
self.selector.close()
self.server_socket.close()

View File

@@ -8,7 +8,7 @@ import socket
import time
from typing import Any
from judas_server.backend.client_status import ClientStatus
from judas_server.client_status import ClientStatus
class Client:

View File

@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING
@@ -7,7 +8,7 @@ from .base_handler import BaseHandler
if TYPE_CHECKING:
from judas_protocol import Message
from judas_server.backend import BackendServer, Client
from judas_server import BackendServer, Client
class AckHandler(BaseHandler):

View File

@@ -4,12 +4,12 @@ from __future__ import annotations
import logging as lg
from typing import TYPE_CHECKING
from judas_server.backend.client import Client
from judas_server.client import Client
if TYPE_CHECKING:
from judas_protocol import Message
from judas_server.backend import BackendServer
from judas_server import BackendServer
class BaseHandler:

View File

@@ -5,12 +5,12 @@ from typing import TYPE_CHECKING, override
from judas_protocol import Category, ControlAction, Message
from judas_server.backend.client import ClientStatus
from judas_server.backend.handler import BaseHandler
from judas_server.client import ClientStatus
from judas_server.handler import BaseHandler
if TYPE_CHECKING:
from judas_server.backend.backend_server import BackendServer
from judas_server.backend.client import Client
from judas_server.backend_server import BackendServer
from judas_server.client import Client
class HelloHandler(BaseHandler):

View File

@@ -7,11 +7,11 @@ from typing import TYPE_CHECKING
from judas_protocol import Message
from judas_server.backend.handler.base_handler import BaseHandler
from judas_server.handler.base_handler import BaseHandler
if TYPE_CHECKING:
from judas_server.backend import BackendServer
from judas_server.backend.client import Client
from judas_server import BackendServer
from judas_server.client import Client
class InitialTelemetryHandler(BaseHandler):

View File

@@ -1,5 +0,0 @@
from .auth import auth_bp
from .index import index_bp
from .panel import panel_bp
__all__ = ["auth_bp", "index_bp", "panel_bp"]

View File

@@ -1,61 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import threading
from typing import TYPE_CHECKING
import flask
import flask_login
if TYPE_CHECKING:
from werkzeug.wrappers import Response
bp: flask.Blueprint = flask.Blueprint("api", __name__, url_prefix="/api")
@bp.route("/client/<client_id>", methods=["GET"])
@flask_login.login_required
def get_client_data(client_id: str) -> tuple[Response, int]:
"""API endpoint to get client data by ID.
Args:
client_id (str): The ID of the client.
Returns:
Response: JSON response with client data or error message.
"""
backend = flask.current_app.config["BACKEND"]
data = backend.get_client_data(client_id)
if data is None:
return flask.jsonify({"error": "Client not found"}), 404
return flask.jsonify(data), 200
@bp.route("/clients", methods=["GET"])
@flask_login.login_required
def list_clients() -> tuple[Response, int]:
"""API endpoint to list all clients.
Returns:
Response: JSON response with list of client IDs.
"""
backend = flask.current_app.config["BACKEND"]
client_ids = list(backend.clients.keys())
return flask.jsonify({"clients": client_ids}), 200
def emit_polled_data(app, socketio):
backend = app.config["BACKEND"]
def poll_loop():
import time
while True:
data = {}
for client_id in backend.clients.keys():
data[client_id] = backend.get_client_data(client_id)
socketio.emit("update_data", data)
time.sleep(1)
threading.Thread(name="Socketio", target=poll_loop, daemon=True).start()

View File

@@ -1,43 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING
import flask
import flask_login
from judas_server.web.user import User
if TYPE_CHECKING:
from werkzeug.wrappers import Response
auth_bp: flask.Blueprint = flask.Blueprint(
"auth", __name__, url_prefix="/auth"
)
@auth_bp.route("/login", methods=["GET", "POST"])
def login() -> Response | str:
"""Handles user login via password form."""
if flask.request.method == "POST":
password = flask.request.form.get("password", "")
if password == flask.current_app.config["PASSWORD"]:
user = User("admin")
flask_login.login_user(user)
next_page = flask.request.args.get("next")
return flask.redirect(next_page or flask.url_for("index.index"))
# return flask.redirect(flask.url_for("panel.panel"))
else:
return flask.render_template(
"login.html",
error="Invalid credentials.",
)
return flask.render_template("login.html")
@auth_bp.route("/logout")
@flask_login.login_required
def logout() -> Response:
"""Logs out the current user."""
flask_login.logout_user()
return flask.redirect(flask.url_for("index.index"))

View File

@@ -1,31 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING
import flask
import flask_login
if TYPE_CHECKING:
from judas_server.backend import BackendServer
from judas_server.backend.client import Client
bp: flask.Blueprint = flask.Blueprint(
"client_details", __name__, url_prefix="/client"
)
@bp.route("/<client_id>")
@flask_login.login_required
def client_details(client_id: str) -> str:
"""Renders the client details page for a specific client.
Args:
client_id: The ID of the client to display details for.
"""
backend: BackendServer = flask.current_app.config["BACKEND"]
client: Client | None = backend.clients.get(client_id)
if not client:
flask.abort(404, description="Client not found")
return flask.render_template("client_details.html", client=client)

View File

@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING
import flask
import flask_login
if TYPE_CHECKING:
from werkzeug.wrappers import Response
index_bp: flask.Blueprint = flask.Blueprint("index", __name__)
@index_bp.route("/")
def index() -> Response | str:
"""Renders the index page."""
return flask.render_template(
"index.html", logged=flask_login.current_user.is_authenticated
)

View File

@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import flask
import flask_login
panel_bp: flask.Blueprint = flask.Blueprint(
"panel", __name__, url_prefix="/panel"
)
@panel_bp.route("")
@flask_login.login_required
def panel() -> str:
"""Renders the main panel page with PC details.
Returns:
Rendered HTML template with PC details.
"""
return flask.render_template("panel.html")

View File

@@ -1,80 +0,0 @@
:root {
--ctp-rosewater: #f5e0dc;
--ctp-rosewater-rgb: 245 224 220;
--ctp-rosewater-hsl: 9.600 55.556% 91.176%;
--ctp-flamingo: #f2cdcd;
--ctp-flamingo-rgb: 242 205 205;
--ctp-flamingo-hsl: 0.000 58.730% 87.647%;
--ctp-pink: #f5c2e7;
--ctp-pink-rgb: 245 194 231;
--ctp-pink-hsl: 316.471 71.831% 86.078%;
--ctp-mauve: #cba6f7;
--ctp-mauve-rgb: 203 166 247;
--ctp-mauve-hsl: 267.407 83.505% 80.980%;
--ctp-red: #f38ba8;
--ctp-red-rgb: 243 139 168;
--ctp-red-hsl: 343.269 81.250% 74.902%;
--ctp-maroon: #eba0ac;
--ctp-maroon-rgb: 235 160 172;
--ctp-maroon-hsl: 350.400 65.217% 77.451%;
--ctp-peach: #fab387;
--ctp-peach-rgb: 250 179 135;
--ctp-peach-hsl: 22.957 92.000% 75.490%;
--ctp-yellow: #f9e2af;
--ctp-yellow-rgb: 249 226 175;
--ctp-yellow-hsl: 41.351 86.047% 83.137%;
--ctp-green: #a6e3a1;
--ctp-green-rgb: 166 227 161;
--ctp-green-hsl: 115.455 54.098% 76.078%;
--ctp-teal: #94e2d5;
--ctp-teal-rgb: 148 226 213;
--ctp-teal-hsl: 170.000 57.353% 73.333%;
--ctp-sky: #89dceb;
--ctp-sky-rgb: 137 220 235;
--ctp-sky-hsl: 189.184 71.014% 72.941%;
--ctp-sapphire: #74c7ec;
--ctp-sapphire-rgb: 116 199 236;
--ctp-sapphire-hsl: 198.500 75.949% 69.020%;
--ctp-blue: #89b4fa;
--ctp-blue-rgb: 137 180 250;
--ctp-blue-hsl: 217.168 91.870% 75.882%;
--ctp-lavender: #b4befe;
--ctp-lavender-rgb: 180 190 254;
--ctp-lavender-hsl: 231.892 97.368% 85.098%;
--ctp-text: #cdd6f4;
--ctp-text-rgb: 205 214 244;
--ctp-text-hsl: 226.154 63.934% 88.039%;
--ctp-subtext1: #bac2de;
--ctp-subtext1-rgb: 186 194 222;
--ctp-subtext1-hsl: 226.667 35.294% 80.000%;
--ctp-subtext0: #a6adc8;
--ctp-subtext0-rgb: 166 173 200;
--ctp-subtext0-hsl: 227.647 23.611% 71.765%;
--ctp-overlay2: #9399b2;
--ctp-overlay2-rgb: 147 153 178;
--ctp-overlay2-hsl: 228.387 16.757% 63.725%;
--ctp-overlay1: #7f849c;
--ctp-overlay1-rgb: 127 132 156;
--ctp-overlay1-hsl: 229.655 12.775% 55.490%;
--ctp-overlay0: #6c7086;
--ctp-overlay0-rgb: 108 112 134;
--ctp-overlay0-hsl: 230.769 10.744% 47.451%;
--ctp-surface2: #585b70;
--ctp-surface2-rgb: 88 91 112;
--ctp-surface2-hsl: 232.500 12.000% 39.216%;
--ctp-surface1: #45475a;
--ctp-surface1-rgb: 69 71 90;
--ctp-surface1-hsl: 234.286 13.208% 31.176%;
--ctp-surface0: #313244;
--ctp-surface0-rgb: 49 50 68;
--ctp-surface0-hsl: 236.842 16.239% 22.941%;
--ctp-base: #1e1e2e;
--ctp-base-rgb: 30 30 46;
--ctp-base-hsl: 240.000 21.053% 14.902%;
--ctp-mantle: #181825;
--ctp-mantle-rgb: 24 24 37;
--ctp-mantle-hsl: 240.000 21.311% 11.961%;
--ctp-crust: #11111b;
--ctp-crust-rgb: 17 17 27;
--ctp-crust-hsl: 240.000 22.727% 8.627%;
}

View File

@@ -1,263 +0,0 @@
@import url("https://cdn-uicons.flaticon.com/2.6.0/uicons-regular-rounded/css/uicons-regular-rounded.css");
@import url("https://cdn-uicons.flaticon.com/2.6.0/uicons-regular-straight/css/uicons-regular-straight.css");
@import url("https://cdn-uicons.flaticon.com/2.6.0/uicons-solid-rounded/css/uicons-solid-rounded.css");
@import url("https://cdn-uicons.flaticon.com/2.6.0/uicons-solid-straight/css/uicons-solid-straight.css");
@import url("https://cdn-uicons.flaticon.com/2.6.0/uicons-bold-rounded/css/uicons-bold-rounded.css");
@import "palette.css";
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: var(--ctp-base);
font-family: sans-serif;
color: var(--ctp-text);
}
.fi {
vertical-align: middle;
}
input {
font-family: inherit;
color: #eceff4;
background-color: var(--ctp-crust);
border: none;
border-radius: 0.25rem;
padding: 0.25rem;
}
a {
color: var(--ctp-blue);
text-decoration: none;
}
#wrapper {
display: flex;
flex-direction: column;
min-height: 100vh;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: var(--ctp-mantle);
padding: 0.5rem 1rem;
}
header h2 {
font-family: monospace, sans-serif;
}
main {
display: flex;
flex-grow: 1;
}
aside {
width: 20rem;
background-color: var(--ctp-base);
border-right: 2px solid var(--ctp-mantle);
}
#content {
display: flex;
flex-direction: column;
gap: 1rem;
flex-grow: 1;
padding: 1rem;
}
header a {
text-decoration: none;
/* color: var(--nord-fg0); */
}
.button {
display: inline-block;
padding: 0.25rem 0.5rem;
background-color: var(--ctp-lavender);
color: var(--ctp-base);
border: none;
border-radius: 0.25rem;
text-decoration: none;
}
.button:hover {
background-color: var(--ctp-mauve);
cursor: pointer;
}
.button:active {
background-color: var(--ctp-sapphire);
}
.center {
text-align: center;
}
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 1rem;
padding: 1rem;
background-color: var(--ctp-surface0);
color: var(--ctp-red);
border: 6px solid var(--ctp-red);
border-radius: 24px;
}
.center-table {
margin: 0 auto;
width: 100%;
}
.select-table {
border-collapse: collapse;
border: 2px solid var(--ctp-surface0);
}
.select-table thead {
position: sticky;
top: -1px;
z-index: 2;
}
.select-table th,
.select-table td {
padding: 0.5rem;
text-align: center;
border: 1px solid var(--ctp-surface0);
border-collapse: collapse;
}
.select-table th {
background-color: var(--ctp-surface1);
color: var(--nord-fg0);
}
.select-table a {
display: block;
text-decoration: none;
transition: 0.1s ease-in-out;
}
.select-table tr {
transition: 0.1s ease-in-out;
}
.select-table tr:hover {
background-color: var(--ctp-surface0);
}
.select-table tr:hover a {
color: var(--ctp-base);
}
.red-bg {
background-color: var(--ctp-red);
}
.yellow-bg {
background-color: var(--ctp-yellow);
}
.green-bg {
background-color: var(--ctp-green);
}
.button-container {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.link {
transition: 0.3s ease-in-out;
}
.link:hover {
color: var(--ctp-blue);
}
.details-table tr td:first-child {
font-weight: 900;
background-color: var(--ctp-surface0);
white-space: nowrap;
width: 1%;
}
.details-table {
border-collapse: collapse;
border: 2px solid var(--ctp-surface1);
}
.details-table th,
.details-table td {
padding: 0.5rem;
text-align: left;
border: 1px solid var(--ctp-text);
border-collapse: collapse;
}
#notify {
display: none;
position: fixed;
top: 3rem;
right: 50%;
transform: translateX(50%);
background-color: var(--ctp-surface1);
padding: 1rem 2rem;
border: 4px solid var(--ctp-text);
border-radius: 1rem;
opacity: 0.8;
}
#no-connection-message {
color: var(--ctp-red);
}
#no-connection-message i {
padding-right: 0.5rem;
font-size: 1.5rem;
vertical-align: middle;
color: var(--ctp-red);
}
ul#client-list {
list-style: none;
display: flex;
flex-direction: column;
}
ul#client-list li {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0 0.5rem;
cursor: default;
}
ul#client-list li:hover {
background-color: var(--ctp-surface0);
}
ul#client-list li i {
line-height: 0;
}
ul#client-list li a {
display: block;
width: 100%;
color: var(--ctp-text);
text-decoration: none;
cursor: default;
}

View File

@@ -1,37 +0,0 @@
<!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>
<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>
<p>
<strong>Initial:</strong>
<pre>{{ client.initial_telemetry | tojson(indent=2) }}</pre>
</p>
</body>
<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>

View File

@@ -1,56 +0,0 @@
<!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>
</div>
<script>
var i = 0;
var txt = document.getElementById("typing-text").innerHTML;
var minSpeed = 50;
var maxSpeed = 200;
document.getElementById("typing-text").innerHTML = "";
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>

View File

@@ -1,34 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>judas - login page</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>
</header>
<main>
<div id="content">
<h1>Login</h1>
<form method="post" class="center">
<label for="password">Password:</label>
<input type="password" id="password" name="password" required autofocus>
<br>
<br>
<input type="submit" value="Login" class="button">
{% if error %}
<div class="error-container">
<h1>Login failure</h1>
<p>{{ error }}</p>
</div>
{% endif %}
</form>
</div>
</main>
</div>
</body>
</html>

View File

@@ -1,203 +0,0 @@
<!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();
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 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>

View File

@@ -1,29 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import flask_login
class User(flask_login.UserMixin):
"""Represents a user for authentication purposes."""
def __init__(self, id: str) -> None:
super().__init__()
self.id = id
def get_id(self) -> str:
"""Return the unique identifier for the user."""
return self.id
def __str__(self) -> str:
return f"User(id={self.id})"
def __repr__(self) -> str:
return f"User(id={self.id})"
def load_user(user_id):
if user_id == "admin":
return User("admin")
return None

View File

@@ -1,74 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import logging as lg
from typing import TYPE_CHECKING
from flask import Flask
from flask_login import LoginManager
from flask_socketio import SocketIO
from judas_server.web.user import load_user
if TYPE_CHECKING:
from judas_server.backend import BackendServer
class JudasWebServer:
def __init__(self, backend: BackendServer, secret_key: str) -> None:
self.logger: lg.Logger = lg.getLogger(
f"{__name__}.{self.__class__.__name__}"
)
self.logger.debug("Initializing JudasWebServer...")
self.backend: BackendServer = backend
self.app: Flask = Flask(
__name__, static_folder="static", template_folder="templates"
)
self.app.secret_key = secret_key
self.app.config["WEB_SERVER"] = self
self.app.config["BACKEND"] = self.backend
# hard-code password
self.app.config["PASSWORD"] = "123"
# extensions
self.login_manager: LoginManager = LoginManager()
self.socketio: SocketIO = SocketIO(self.app, cors_allowed_origins="*")
self.configure_extensions()
self.init_routes()
def configure_extensions(self) -> None:
self.logger.debug("Configuring extensions...")
self.login_manager.init_app(self.app)
self.login_manager.user_loader(load_user)
self.login_manager.login_view = "auth.login"
# TODO: add login page
def init_routes(self) -> None:
self.logger.debug("Initializing routes...")
from judas_server.web.routes import (
api,
auth_bp,
index_bp,
panel_bp,
client_details,
)
self.app.register_blueprint(index_bp)
self.app.register_blueprint(auth_bp)
self.app.register_blueprint(panel_bp)
self.app.register_blueprint(api.bp)
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:
self.logger.info(f"Starting web server on {host}:{port}...")
self.socketio.run(app=self.app, host=host, port=port)
self.logger.info("Server stopped.")

2
uv.lock generated
View File

@@ -363,7 +363,7 @@ source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#085c34f232f9
[[package]]
name = "judas-server"
version = "0.7.0.dev1"
version = "0.7.0"
source = { editable = "." }
dependencies = [
{ name = "flask" },