Compare commits
2 Commits
main
...
refactor/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
68bb05a482
|
|||
|
f49c33974d
|
@@ -1 +1,7 @@
|
||||
from .backend_server import BackendServer
|
||||
|
||||
__version__: str = "0.5.0"
|
||||
|
||||
__all__ = [
|
||||
"BackendServer",
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
from .backend_server import BackendServer
|
||||
from .client import Client
|
||||
from .client_status import ClientStatus
|
||||
|
||||
__all__ = ["BackendServer", "Client", "ClientStatus"]
|
||||
@@ -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()
|
||||
@@ -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:
|
||||
@@ -8,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):
|
||||
@@ -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:
|
||||
@@ -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):
|
||||
@@ -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):
|
||||
@@ -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"]
|
||||
@@ -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()
|
||||
@@ -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"))
|
||||
@@ -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)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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")
|
||||
@@ -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%;
|
||||
}
|
||||
@@ -1,262 +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);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.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: 4px solid var(--ctp-mantle);
|
||||
}
|
||||
|
||||
#content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
$(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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,13 +0,0 @@
|
||||
<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,46 +0,0 @@
|
||||
<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>Last Seen:</strong>
|
||||
<span id="last-seen">{{ client.last_seen }}</span>
|
||||
</p>
|
||||
</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();
|
||||
}
|
||||
|
||||
$(".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>
|
||||
@@ -1,42 +0,0 @@
|
||||
{% 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>
|
||||
<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 %}
|
||||
|
||||
{% block scripts %}
|
||||
<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>
|
||||
{% endblock %}
|
||||
@@ -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>
|
||||
@@ -1,16 +0,0 @@
|
||||
{% extends "base/base.html" %}
|
||||
|
||||
{% block title %}panel{% endblock %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/panel.js') }}"></script>
|
||||
{% endblock %}
|
||||
@@ -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
|
||||
@@ -1,76 +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, debug: bool = False
|
||||
) -> None:
|
||||
self.logger.info(f"Starting web server on {host}:{port}...")
|
||||
self.socketio.run(app=self.app, host=host, port=port, debug=debug)
|
||||
self.logger.info("Server stopped.")
|
||||
Reference in New Issue
Block a user