Compare commits
33 Commits
88d349090e
...
0.7.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a85084c8f | ||
| 6cc6dc5b42 | |||
| a4c07d9d2d | |||
|
a697ae6661
|
|||
|
acbcb3364e
|
|||
|
762256c3cd
|
|||
|
c442dca520
|
|||
|
12c5de9f11
|
|||
|
aa562a0eab
|
|||
|
865112c823
|
|||
|
1d764bd77d
|
|||
|
6f4bc3aa0f
|
|||
|
14ea136fbb
|
|||
|
78f9508753
|
|||
|
|
11bf344cb5 | ||
| 5ee8eca11b | |||
| f18935f793 | |||
|
d7b136851b
|
|||
|
7e9a9e6eed
|
|||
|
6ed03ab74d
|
|||
|
40c08d0169
|
|||
|
3d13d24116
|
|||
|
28b57b6964
|
|||
|
|
6a024bbfc6 | ||
| e5ca1066fd | |||
|
61be674258
|
|||
|
72d51b451f
|
|||
|
efbf99f356
|
|||
|
f46d27164b
|
|||
|
f13f243b5b
|
|||
|
fbb75c263c
|
|||
|
0e7c948668
|
|||
|
35899d3668
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -45,3 +45,6 @@ logs/
|
|||||||
# Sphinx
|
# Sphinx
|
||||||
docs/_build/
|
docs/_build/
|
||||||
docs/ref/modules/
|
docs/ref/modules/
|
||||||
|
|
||||||
|
# known clients
|
||||||
|
config/known_clients.yaml
|
||||||
|
|||||||
92
CHANGELOG.md
92
CHANGELOG.md
@@ -2,6 +2,98 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [0.7.0] - 2026-03-12
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- [`a697ae6`](https://github.com/pufereq/template-repo/commit/a697ae666114ed0adfa78db7accfd81d26850111) **ack_handler.py**: fix error by importing annotations
|
||||||
|
- [`762256c`](https://github.com/pufereq/template-repo/commit/762256c3cdee4bfc8dbdbfa6d2997656486f9557) **client_details.html**: fix up code for AJAX loading
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- [`acbcb33`](https://github.com/pufereq/template-repo/commit/acbcb3364e7b682bb67ee329d12626e9df0d627d) **web_server.py**: add `debug` parameter to `JudasWebServer.run()`
|
||||||
|
- [`1d764bd`](https://github.com/pufereq/template-repo/commit/1d764bd77d031123054ab86f073fb27823363532) **header.html**: add header template
|
||||||
|
- [`6f4bc3a`](https://github.com/pufereq/template-repo/commit/6f4bc3aa0f7f1a4aefe8b2d8405d4fc49c6c744d) **base.html**: add base template
|
||||||
|
- [`14ea136`](https://github.com/pufereq/template-repo/commit/14ea136fbbad447d1eb668fcc09aadb75de88ee3) **panel.js**: move js from inline to separate script
|
||||||
|
- [`78f9508`](https://github.com/pufereq/template-repo/commit/78f9508753ae47c681cff115fb960bea6bb12e20) **panel.html**: use ?query instead of #hash for client selection
|
||||||
|
|
||||||
|
### Miscellaneous Tasks
|
||||||
|
|
||||||
|
- [`aa562a0`](https://github.com/pufereq/template-repo/commit/aa562a0eab3a4ce10f225d03c9927ace6ee66678) **base.html**: remove leftover code
|
||||||
|
- [`865112c`](https://github.com/pufereq/template-repo/commit/865112c823a65d761d929c69574c47b57559804d) **style.css**: adjust styles
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- [`c442dca`](https://github.com/pufereq/template-repo/commit/c442dca520ff381b17cd49863e5189066882f73d) **panel.html**: use new base template
|
||||||
|
- [`12c5de9`](https://github.com/pufereq/template-repo/commit/12c5de9f114d955c6b309f6020829595d125bec2) **index.html**: use new base template
|
||||||
|
|
||||||
|
## [0.7.0-dev.1] - 2026-03-08
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- [`d7b1368`](https://github.com/pufereq/template-repo/commit/d7b136851bafa2c20e1634bd5568f4bac839177f) **client_details.html**: add temporary initial telemetry display
|
||||||
|
- [`7e9a9e6`](https://github.com/pufereq/template-repo/commit/7e9a9e6eede6cc926fef64c626434e65984befbc) **backend_server.py**: add `initial_telemetry` support
|
||||||
|
- [`6ed03ab`](https://github.com/pufereq/template-repo/commit/6ed03ab74de8e91d13b5f1971a3f4cec890e4fef) **client.py**: add `Client.initial_telemetry` property
|
||||||
|
- [`40c08d0`](https://github.com/pufereq/template-repo/commit/40c08d01693973f29f13c133a11fc5f166891a25) **initial_handler.py**: add handler for `TELEMETRY/INTIIAL` msgs
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
- [`3d13d24`](https://github.com/pufereq/template-repo/commit/3d13d241168b011c0044eb64db4b0fe70878d748) **uv.lock**: update judas_protocol to 0.9.1
|
||||||
|
- [`28b57b6`](https://github.com/pufereq/template-repo/commit/28b57b6964bfcd6ce78f2a77822f7221f6e4f7e5) **uv.lock**: update judas_protocol to 0.9.0
|
||||||
|
|
||||||
|
## [0.6.0] - 2026-03-05
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- [`fbb75c2`](https://github.com/pufereq/template-repo/commit/fbb75c263c13726d7e1c3fa28fb4e8f1f8cf0a17) **backend_server.py**: fix double-connection handling
|
||||||
|
- [`88d3490`](https://github.com/pufereq/template-repo/commit/88d349090e6e3605800656cc3e85779bf4b417c3) **backend_server.py**: fix double disconnect if client.inbound empty
|
||||||
|
- [`e308a07`](https://github.com/pufereq/template-repo/commit/e308a07dabcf20d9b080ce12a8928760dcde3057) **backend_server.py**: call `_initialize_handlers()` on init
|
||||||
|
- [`ead2224`](https://github.com/pufereq/template-repo/commit/ead22240660ff9f723c36ee35fa5918c33a0c66d) **backend_server.py**: do not disconnect a client if Exception raised on msg handling
|
||||||
|
- [`6446fe8`](https://github.com/pufereq/template-repo/commit/6446fe883cea4268992efc9f97cdb493af416576) **backend_server.py**: check if client to disconnect has an open socket
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- [`72d51b4`](https://github.com/pufereq/template-repo/commit/72d51b451fc11ebe2f38d5e50bf263f9f441b03c) **backend_server.py**: add notice to known_clients.yaml
|
||||||
|
- [`0e7c948`](https://github.com/pufereq/template-repo/commit/0e7c9486683be4ceb91203109986f0b9fc83eb8b) **config/**: add `config/` directory
|
||||||
|
- [`97fc17f`](https://github.com/pufereq/template-repo/commit/97fc17fbb3ed5d681a4f5a0d488b7a2b09ab35df) **hello_handler.py**: remove client from `pending_hello` if recv'd HELLO
|
||||||
|
- [`f5b14fc`](https://github.com/pufereq/template-repo/commit/f5b14fc6109e83730c4b13aaecf6f4314c0136a4) **backend_server.py**: add timeout on HELLO
|
||||||
|
- [`bf1ad0e`](https://github.com/pufereq/template-repo/commit/bf1ad0ead0e4fc35c20d0d747cca05388b223d35) **ack_handler.py**: add handling for ACKs
|
||||||
|
- [`a9bace8`](https://github.com/pufereq/template-repo/commit/a9bace8acaab07e6e5d02155a9966c6381ad107e) **backend_server.py**: add `ACK_TIMEOUT` constant
|
||||||
|
- [`c88e39c`](https://github.com/pufereq/template-repo/commit/c88e39c7355b6f8c982198e6e56da2d09d17c61f) **backend_server.py**: track message ACKs and resend if no ACK recv'd within 5 seconds
|
||||||
|
- [`dafe418`](https://github.com/pufereq/template-repo/commit/dafe418916485258694949538eda151d7da33695) **backend_server.py**: add warning if received an unknown message (no handler)
|
||||||
|
- [`0ed478a`](https://github.com/pufereq/template-repo/commit/0ed478a88efa46f2f28b24371ad68010ecbf88d0) **backend_server.py**: implement message handling
|
||||||
|
- [`c952413`](https://github.com/pufereq/template-repo/commit/c952413d912b7a5d2000bc48c11a4d9274533caf) **hello_handler.py**: add HELLO message handler
|
||||||
|
- [`882c878`](https://github.com/pufereq/template-repo/commit/882c8780e15ec827fac1854cf7419b71d9a41a4f) **base_handler.py**: add `BaseHandler` class for message handling
|
||||||
|
- [`faecc38`](https://github.com/pufereq/template-repo/commit/faecc382610daf890c90074809085560931fd178) **client_status.py**: move `ClientStatus` enum to own module
|
||||||
|
|
||||||
|
### Miscellaneous Tasks
|
||||||
|
|
||||||
|
- [`61be674`](https://github.com/pufereq/template-repo/commit/61be674258dcce8a5b54efddb7d1d856c6cba9bc) remove `cache/` directory
|
||||||
|
- [`f46d271`](https://github.com/pufereq/template-repo/commit/f46d27164b79107abe405b221c9d424add55a723) **.gitignore**: ignore `config/known_clients.yaml` as it's generated automatically
|
||||||
|
- [`f13f243`](https://github.com/pufereq/template-repo/commit/f13f243b5b389d84acaa4ab933a234a00c6a981c) **__main__.py**: set logging level for werkzeug to WARNING
|
||||||
|
- [`332238b`](https://github.com/pufereq/template-repo/commit/332238b403782a08a8974b9267071f8d0ff5f006) **__init__.py**: correct version
|
||||||
|
- [`e54cc47`](https://github.com/pufereq/template-repo/commit/e54cc479b5f6bb3fb887803122fea0c551e27ee2) **handler/__init__.py**: add `AckHandler` to `__all__`
|
||||||
|
- [`ee38141`](https://github.com/pufereq/template-repo/commit/ee381414a931ed9d7562410e08c9de5cdc7b94e7) **backend_server.py**: remove redundant HELLO msg handling
|
||||||
|
- [`ec58a52`](https://github.com/pufereq/template-repo/commit/ec58a5257a3007a6edd502eb3967d92a9b652358) **handler/__init__.py**: add module init
|
||||||
|
- [`d3f68d3`](https://github.com/pufereq/template-repo/commit/d3f68d3baff8b903be125b0da7a88b415cd56bac) **backend/__init__.py**: add `Client` and `ClientStatus` to `__all__`
|
||||||
|
- [`bda10a6`](https://github.com/pufereq/template-repo/commit/bda10a6248478e99dfb42d8c605996cafdaf1e6e) **cache/**: add cache/ directory
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- [`efbf99f`](https://github.com/pufereq/template-repo/commit/efbf99f356c20fbd48937f1c4e8df0a2d57f0651) **backend_server.py**: if known_clients.yaml not present, call `_save_known_clients()`
|
||||||
|
- [`35899d3`](https://github.com/pufereq/template-repo/commit/35899d366823c2878330eb06dc703f2965f61c3f) **backend_server.py**: move `known_clients.yaml` to `config/`
|
||||||
|
- [`3eb681e`](https://github.com/pufereq/template-repo/commit/3eb681e233fa151c2476581854c947bdc77b4900) **backend_server.py**: move loading known clients to its own method
|
||||||
|
- [`fa2da20`](https://github.com/pufereq/template-repo/commit/fa2da207a9cf3d90cdbfb5977033fee21d449528) **backend_server.py**: refactor calls to Message class constructors after protocol changes
|
||||||
|
|
||||||
|
### Styling
|
||||||
|
|
||||||
|
- [`62acc4b`](https://github.com/pufereq/template-repo/commit/62acc4b181cd359557909fa94f0cb0e6a4109255) **client.py**: correct property typing
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
- [`c64a258`](https://github.com/pufereq/template-repo/commit/c64a2582439e5ef1ab68bb9c588f7c4644bb4e07) **uv.lock**: update judas_protocol to 0.8.0
|
||||||
|
- [`f41a777`](https://github.com/pufereq/template-repo/commit/f41a7774ec5cb4997005ac8e31ed7bdc3541a8d6) **uv.lock**: update judas_protocol to 0.7.0
|
||||||
|
|
||||||
## [0.5.0] - 2026-02-28
|
## [0.5.0] - 2026-02-28
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "judas_server"
|
name = "judas_server"
|
||||||
version = "0.5.0"
|
version = "0.7.0"
|
||||||
description = "The backbone of the remote PC fleet management system."
|
description = "The backbone of the remote PC fleet management system."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = []
|
authors = []
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ if __name__ == "__main__":
|
|||||||
format="%(asctime)s : [%(levelname)s] : %(threadName)s : %(name)s :: %(message)s",
|
format="%(asctime)s : [%(levelname)s] : %(threadName)s : %(name)s :: %(message)s",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
lg.getLogger("werkzeug").setLevel(lg.WARNING)
|
||||||
|
|
||||||
ladygaga_logger = lg.getLogger(f"{__name__}.LAGA_DYGA")
|
ladygaga_logger = lg.getLogger(f"{__name__}.LAGA_DYGA")
|
||||||
ladygaga_logger.info(LADY_GAGA)
|
ladygaga_logger.info(LADY_GAGA)
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,15 @@ import threading
|
|||||||
import time
|
import time
|
||||||
from typing import TYPE_CHECKING, Any, Final
|
from typing import TYPE_CHECKING, Any, Final
|
||||||
|
|
||||||
|
from judas_protocol.types import TelemetryAction
|
||||||
import yaml
|
import yaml
|
||||||
from judas_protocol import Category, ControlAction, Message
|
from judas_protocol import Category, ControlAction, Message
|
||||||
|
|
||||||
from judas_server.backend.client import Client, ClientStatus
|
from judas_server.backend.client import Client, ClientStatus
|
||||||
from judas_server.backend.handler.hello_handler import HelloHandler
|
from judas_server.backend.handler.hello_handler import HelloHandler
|
||||||
|
from judas_server.backend.handler.telemetry.initial_handler import (
|
||||||
|
InitialTelemetryHandler,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
@@ -49,9 +53,9 @@ class BackendServer:
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.clients: dict[str, Client] = {}
|
self.clients: dict[str, Client] = {}
|
||||||
self.known_clients: dict[str, dict[str, str | float]] = (
|
|
||||||
self._load_known_clients()
|
self.known_clients: dict[str, dict[str, str | float]] = {}
|
||||||
)
|
self.known_clients = self._load_known_clients()
|
||||||
|
|
||||||
self.message_handlers: dict[
|
self.message_handlers: dict[
|
||||||
tuple[Category, ActionType], Callable[[Client, Message], None]
|
tuple[Category, ActionType], Callable[[Client, Message], None]
|
||||||
@@ -67,17 +71,22 @@ class BackendServer:
|
|||||||
"""Initialize message handlers."""
|
"""Initialize message handlers."""
|
||||||
|
|
||||||
hello_handler = HelloHandler(self)
|
hello_handler = HelloHandler(self)
|
||||||
|
initial_telemetry_handler = InitialTelemetryHandler(self)
|
||||||
|
|
||||||
self.message_handlers[(Category.CONTROL, ControlAction.HELLO)] = (
|
self.message_handlers[(Category.CONTROL, ControlAction.HELLO)] = (
|
||||||
hello_handler.handle
|
hello_handler.handle
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.message_handlers[
|
||||||
|
(Category.TELEMETRY, TelemetryAction.INITIAL)
|
||||||
|
] = initial_telemetry_handler.handle
|
||||||
|
|
||||||
def _load_known_clients(self) -> dict[str, dict[str, str | float]]:
|
def _load_known_clients(self) -> dict[str, dict[str, str | float]]:
|
||||||
"""Load the list of known clients from a YAML file and validate."""
|
"""Load the list of known clients from a YAML file and validate."""
|
||||||
known_clients: dict[str, dict[str, str | float]] = {}
|
known_clients: dict[str, dict[str, str | float]] = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open("cache/known_clients.yaml", "r") as f:
|
with open("config/known_clients.yaml", "r") as f:
|
||||||
data = yaml.safe_load(f)
|
data = yaml.safe_load(f)
|
||||||
|
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
@@ -108,14 +117,16 @@ class BackendServer:
|
|||||||
client.last_seen = float(
|
client.last_seen = float(
|
||||||
known_clients[client_id].get("last_seen", 0.0)
|
known_clients[client_id].get("last_seen", 0.0)
|
||||||
)
|
)
|
||||||
|
client.initial_telemetry = known_clients[client_id].get( # type: ignore
|
||||||
|
"initial_telemetry", None
|
||||||
|
)
|
||||||
self.clients[client_id] = client
|
self.clients[client_id] = client
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
"known_clients.yaml not found, creating empty known clients list"
|
"known_clients.yaml not found, creating empty known clients list"
|
||||||
)
|
)
|
||||||
with open("cache/known_clients.yaml", "w") as f:
|
self._save_known_clients()
|
||||||
yaml.safe_dump({"known_clients": {}}, f)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error loading known clients: {e}")
|
self.logger.error(f"Error loading known clients: {e}")
|
||||||
raise
|
raise
|
||||||
@@ -124,7 +135,12 @@ class BackendServer:
|
|||||||
|
|
||||||
def _save_known_clients(self) -> None:
|
def _save_known_clients(self) -> None:
|
||||||
"""Save the list of known clients to a YAML file."""
|
"""Save the list of known clients to a YAML file."""
|
||||||
with open("cache/known_clients.yaml", "w") as f:
|
with open("config/known_clients.yaml", "w") as f:
|
||||||
|
f.write(
|
||||||
|
"# This file is automatically generated by BackendServer.\n"
|
||||||
|
+ "# Do not edit manually.\n"
|
||||||
|
+ f"# Generated at: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())}\n\n"
|
||||||
|
)
|
||||||
yaml.safe_dump({"known_clients": self.known_clients}, f)
|
yaml.safe_dump({"known_clients": self.known_clients}, f)
|
||||||
self.logger.debug("Saved known clients")
|
self.logger.debug("Saved known clients")
|
||||||
|
|
||||||
@@ -395,4 +411,5 @@ class BackendServer:
|
|||||||
"addr": client.addr,
|
"addr": client.addr,
|
||||||
"last_seen": client.last_seen,
|
"last_seen": client.last_seen,
|
||||||
"status": client.status,
|
"status": client.status,
|
||||||
|
"initial_telemetry": client.initial_telemetry,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from __future__ import annotations
|
|||||||
import logging as lg
|
import logging as lg
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from judas_server.backend.client_status import ClientStatus
|
from judas_server.backend.client_status import ClientStatus
|
||||||
|
|
||||||
@@ -41,6 +42,8 @@ class Client:
|
|||||||
self.inbound: bytes = b""
|
self.inbound: bytes = b""
|
||||||
self.outbound: bytes = b""
|
self.outbound: bytes = b""
|
||||||
|
|
||||||
|
self.initial_telemetry: dict[str, Any] | None = None
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
if self.addr:
|
if self.addr:
|
||||||
return f"Client({self.id} ({self.addr[0]}:{self.addr[1]}))"
|
return f"Client({self.id} ({self.addr[0]}:{self.addr[1]}))"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ class HelloHandler(BaseHandler):
|
|||||||
# check if client already connected, if so disconnect old client and register new one
|
# check if client already connected, if so disconnect old client and register new one
|
||||||
if (
|
if (
|
||||||
client.id in self.backend_server.clients
|
client.id in self.backend_server.clients
|
||||||
and self.backend_server.clients[client.id].status == "connected"
|
and self.backend_server.clients[client.id].status
|
||||||
|
== ClientStatus.ONLINE
|
||||||
):
|
):
|
||||||
old_client: Client = self.backend_server.clients[client.id]
|
old_client: Client = self.backend_server.clients[client.id]
|
||||||
self.backend_server.logger.warning(
|
self.backend_server.logger.warning(
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Initial telemetry handler."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from judas_protocol import Message
|
||||||
|
|
||||||
|
from judas_server.backend.handler.base_handler import BaseHandler
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from judas_server.backend import BackendServer
|
||||||
|
from judas_server.backend.client import Client
|
||||||
|
|
||||||
|
|
||||||
|
class InitialTelemetryHandler(BaseHandler):
|
||||||
|
"""Handles the initial telemetry message from a client."""
|
||||||
|
|
||||||
|
def __init__(self, backend_server: BackendServer) -> None:
|
||||||
|
"""Initialize the handler."""
|
||||||
|
super().__init__(backend_server)
|
||||||
|
|
||||||
|
def handle(self, client: Client, message: Message) -> None:
|
||||||
|
"""Handle the initial telemetry message."""
|
||||||
|
self.logger.debug(
|
||||||
|
f"Handling initial telemetry message from {client}..."
|
||||||
|
)
|
||||||
|
|
||||||
|
client.initial_telemetry = message.payload
|
||||||
|
self.backend_server.known_clients[client.id]["initial_telemetry"] = ( # type: ignore
|
||||||
|
message.payload
|
||||||
|
)
|
||||||
@@ -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 {
|
||||||
|
|||||||
163
src/judas_server/web/static/js/panel.js
Normal file
163
src/judas_server/web/static/js/panel.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
39
src/judas_server/web/templates/base/base.html
Normal file
39
src/judas_server/web/templates/base/base.html
Normal 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>
|
||||||
13
src/judas_server/web/templates/base/header.html
Normal file
13
src/judas_server/web/templates/base/header.html
Normal 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>
|
||||||
@@ -1,33 +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>
|
||||||
</body>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -1,203 +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 hash
|
{% block scripts %}
|
||||||
const hash = window.location.hash;
|
<script src="{{ url_for('static', filename='js/panel.js') }}"></script>
|
||||||
if (hash) {
|
{% endblock %}
|
||||||
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>
|
|
||||||
|
|||||||
@@ -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.")
|
||||||
|
|||||||
6
uv.lock
generated
6
uv.lock
generated
@@ -358,12 +358,12 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "judas-protocol"
|
name = "judas-protocol"
|
||||||
version = "0.8.0"
|
version = "0.9.1"
|
||||||
source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#a805ccf38edffadc1b8c8b276e60758c86516cd3" }
|
source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#085c34f232f95313d66db48a7d17bc25c92a35ae" }
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "judas-server"
|
name = "judas-server"
|
||||||
version = "0.5.0"
|
version = "0.7.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "flask" },
|
{ name = "flask" },
|
||||||
|
|||||||
Reference in New Issue
Block a user