Compare commits
61 Commits
3911afff26
...
refactor/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
68bb05a482
|
|||
|
f49c33974d
|
|||
|
|
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
|
|||
|
88d349090e
|
|||
|
332238b403
|
|||
|
e54cc479b5
|
|||
|
97fc17fbb3
|
|||
|
f5b14fc610
|
|||
|
bf1ad0ead0
|
|||
|
a9bace8aca
|
|||
|
c88e39c735
|
|||
|
e308a07dab
|
|||
|
dafe418916
|
|||
|
c64a258243
|
|||
|
ead2224066
|
|||
|
ee381414a9
|
|||
|
0ed478a88e
|
|||
|
6446fe883c
|
|||
|
ec58a5257a
|
|||
|
c952413d91
|
|||
|
882c8780e1
|
|||
|
d3f68d3baf
|
|||
|
62acc4b181
|
|||
|
faecc38261
|
|||
|
3eb681e233
|
|||
|
bda10a6248
|
|||
|
fa2da207a9
|
|||
|
f41a7774ec
|
|||
|
|
2a8ac307d5 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -45,3 +45,6 @@ logs/
|
||||
# Sphinx
|
||||
docs/_build/
|
||||
docs/ref/modules/
|
||||
|
||||
# known clients
|
||||
config/known_clients.yaml
|
||||
|
||||
142
CHANGELOG.md
142
CHANGELOG.md
@@ -2,6 +2,148 @@
|
||||
|
||||
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
|
||||
|
||||
### Features
|
||||
|
||||
- [`956da02`](https://github.com/pufereq/template-repo/commit/956da024c3bbe88d83f3ef7bfa37e380c22a04d9) **style.css**: make client list elements flexboxes
|
||||
- [`54eec65`](https://github.com/pufereq/template-repo/commit/54eec657a5a4822f5b1211cbc6843045165fe25d) **style.css**: use monospaced font only for heading
|
||||
- [`1900bf4`](https://github.com/pufereq/template-repo/commit/1900bf46cc6f103f6e9d359b9fd6af9a74d4cf4d) **client_details.html**: add basic client details
|
||||
- [`c5771dc`](https://github.com/pufereq/template-repo/commit/c5771dc3718b447878633b252bdd94b3e793aec9) **panel.html**: use client_details.html after clicking on client on sidebar
|
||||
- [`de9240e`](https://github.com/pufereq/template-repo/commit/de9240e6e0c683643df5bb9495c46cae64b0bc68) **backend_server.py**: set client status to STALE if offline for >24h
|
||||
- [`b1656cd`](https://github.com/pufereq/template-repo/commit/b1656cdfa95673bb19605d9358cb3c98a69f15d2) **client.py**: redo ClientStatuses adding PENDING and STALE
|
||||
- [`563de5a`](https://github.com/pufereq/template-repo/commit/563de5aa199a64946d9f41b84c6e2803aff45714) **backend_server.py**: add known_clients.yaml file for storing clients between server restarts
|
||||
- [`6971548`](https://github.com/pufereq/template-repo/commit/6971548589f098f850896ee49e833c7c1bce5efc) **web_server.py**: use client_details blueprint
|
||||
- [`31c5157`](https://github.com/pufereq/template-repo/commit/31c51574f7a2b92a959ddf9ae4c8cbf336494725) **client_details.py**: add client_details route
|
||||
- [`b652db9`](https://github.com/pufereq/template-repo/commit/b652db930f9a299c467b52acb22420e7ca280fb7) **style.css**: style no-connection-icon and client list
|
||||
- [`1dfddd2`](https://github.com/pufereq/template-repo/commit/1dfddd2fc7f4436010ddad31f50355073c8e4abb) **panel.html**: add a side panel client list
|
||||
- [`d20ff9b`](https://github.com/pufereq/template-repo/commit/d20ff9be6eeb9e283304ca6d60a4635a2f9b0ab6) **panel.html**: add a no connection icon to header
|
||||
- [`2bbe118`](https://github.com/pufereq/template-repo/commit/2bbe118de6290b1e014aa9c1a6f5f9d5b134d2e0) rename `details.html` -> `client_details.html`
|
||||
- [`29b4f3a`](https://github.com/pufereq/template-repo/commit/29b4f3a2ff34493d245eb0eb144dd52b0cc9b134) **css/style.css**: add `.button:active` color
|
||||
- [`69bf4f1`](https://github.com/pufereq/template-repo/commit/69bf4f13585f06c561c0746966bf95762c188bd0) **css/style.css**: add #content styling, make `main`'s flex column to fit `aside`
|
||||
- [`563dc62`](https://github.com/pufereq/template-repo/commit/563dc626246f379f1179179faa36d13b019b6a4e) **css/style.css**: include flaticon icons
|
||||
- [`bb229dc`](https://github.com/pufereq/template-repo/commit/bb229dc724ef9cec4c147c6c3d9f67b28eb700ce) **css/style.css**: make UI more compact
|
||||
- [`3077a98`](https://github.com/pufereq/template-repo/commit/3077a98d6fe7f34219c861120bc217929dc919d9) **backend_server.py**: add `send_close()` method
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- [`7f904fd`](https://github.com/pufereq/template-repo/commit/7f904fdcd50ac114cc584d7438675d5490b61bcd) **uv.lock**: add pyyaml to depedencies
|
||||
- [`f11b442`](https://github.com/pufereq/template-repo/commit/f11b442eced4781ee3be0102bd8ea0614bc378c4) **style.css**: style no connection message correctly
|
||||
- [`72a7433`](https://github.com/pufereq/template-repo/commit/72a74334e74f1a1a73d2ae11dd08e72a8a98b094) **pyproject.toml**: add pyyaml to depedencies
|
||||
- [`5ca5d9f`](https://github.com/pufereq/template-repo/commit/5ca5d9fcf4e8095b93cac1d72877769064895189) **.vscode/launch.json**: set cwd to workspace folder
|
||||
- [`0580a6b`](https://github.com/pufereq/template-repo/commit/0580a6be5390eb08a13eec0ce7c41219b1dca21a) **css/style.css**: remove transition from button hover bg color
|
||||
- [`97221bc`](https://github.com/pufereq/template-repo/commit/97221bc1b71112ac54168138bf5adc10acb920b5) **uv.lock**: update depedencies for Python 3.14
|
||||
|
||||
### Refactor
|
||||
|
||||
- [`a5e1ba8`](https://github.com/pufereq/template-repo/commit/a5e1ba88aeb06d337587e9dfd0a46520ef6c8d95) adapt for client.id
|
||||
- [`1e38be5`](https://github.com/pufereq/template-repo/commit/1e38be5ec5d55a30fd715a650ea7402b9fb4c546) **backend_server.py**: adapt client's `id` after refactor
|
||||
- [`b265feb`](https://github.com/pufereq/template-repo/commit/b265feba7a79ecd418bf8849a68a0633ae7d4778) **client.py**: rename `Client.mac_id` -> `Client.id`
|
||||
- [`840d9ce`](https://github.com/pufereq/template-repo/commit/840d9ce3c1838a3d88264fe895f933aea6c8cb4f) **login.html**: move elements from `main` to `#content`
|
||||
- [`9971981`](https://github.com/pufereq/template-repo/commit/9971981f666d4099132235fe0ecfb8e679745d95) **index.html**: move elements from `main` to `#content`
|
||||
- [`5510e9d`](https://github.com/pufereq/template-repo/commit/5510e9dd0874a1e3448a96eb8b8472b5599458a1) **backend_server.py**: rewrite `_handle_connection()` to minimize indents
|
||||
- [`1e02da1`](https://github.com/pufereq/template-repo/commit/1e02da185111f0e05651df0bf5c1df735f7dc6ff) **backend_server.py**: rename `_send_ack()` -> `send_ack()`
|
||||
|
||||
### Styling
|
||||
|
||||
- [`f54d974`](https://github.com/pufereq/template-repo/commit/f54d974745c7a0f5d8db619d86d585709649b8ec) **style.css**: remove commented-out code
|
||||
|
||||
### Build
|
||||
|
||||
- [`f01eaec`](https://github.com/pufereq/template-repo/commit/f01eaecba346715dd0c50eb4b253656df5c89ad8) **uv.lock**: update judas_protocol to 0.6.0
|
||||
|
||||
## [0.4.0] - 2025-11-30
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
||||
|
||||
[project]
|
||||
name = "judas_server"
|
||||
version = "0.4.0"
|
||||
version = "0.7.0"
|
||||
description = "The backbone of the remote PC fleet management system."
|
||||
readme = "README.md"
|
||||
authors = []
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
__version__: str = "0.1.0"
|
||||
from .backend_server import BackendServer
|
||||
|
||||
__version__: str = "0.5.0"
|
||||
|
||||
__all__ = [
|
||||
"BackendServer",
|
||||
]
|
||||
|
||||
@@ -3,15 +3,16 @@
|
||||
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,
|
||||
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.info(LADY_GAGA)
|
||||
|
||||
@@ -20,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,3 +0,0 @@
|
||||
from .backend_server import BackendServer
|
||||
|
||||
__all__ = ["BackendServer"]
|
||||
@@ -6,16 +6,28 @@ import selectors
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
from judas_protocol.types import TelemetryAction
|
||||
import yaml
|
||||
|
||||
from typing import Any
|
||||
|
||||
from judas_protocol import Category, ControlAction, Message
|
||||
|
||||
from judas_server.backend.client import Client, ClientStatus
|
||||
from judas_server.client import Client, ClientStatus
|
||||
from judas_server.handler.hello_handler import HelloHandler
|
||||
from judas_server.handler.telemetry.initial_handler import (
|
||||
InitialTelemetryHandler,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
|
||||
from judas_protocol import ActionType
|
||||
|
||||
|
||||
class BackendServer:
|
||||
ACK_TIMEOUT: Final[float] = 5.0 # seconds
|
||||
HELLO_TIMEOUT: Final[float] = 3.0 # seconds
|
||||
|
||||
def __init__(self, host: str = "0.0.0.0", port: int = 3692) -> None:
|
||||
"""Initialize the backend server.
|
||||
|
||||
@@ -28,27 +40,6 @@ class BackendServer:
|
||||
)
|
||||
self.logger.debug("Initializing Server...")
|
||||
|
||||
self.known_clients: dict[str, dict[str, str | float]] = {}
|
||||
try:
|
||||
with open("cache/known_clients.yaml", "r") as f:
|
||||
self.known_clients = (
|
||||
yaml.safe_load(f).get("known_clients", {}) or {}
|
||||
)
|
||||
self.logger.debug(
|
||||
f"Loaded known clients: {self.known_clients}"
|
||||
)
|
||||
self.logger.info(
|
||||
f"Loaded {len(self.known_clients)} known clients"
|
||||
)
|
||||
except FileNotFoundError:
|
||||
self.logger.warning(
|
||||
"known_clients.yaml not found, creating empty known clients list"
|
||||
)
|
||||
with open("cache/known_clients.yaml", "w") as f:
|
||||
yaml.safe_dump({"known_clients": {}}, f)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error loading known clients: {e}")
|
||||
|
||||
self.selector = selectors.DefaultSelector()
|
||||
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.server_socket.setsockopt(
|
||||
@@ -63,20 +54,93 @@ class BackendServer:
|
||||
|
||||
self.clients: dict[str, Client] = {}
|
||||
|
||||
if self.known_clients:
|
||||
for client_id in self.known_clients:
|
||||
client = Client(id=client_id, addr=None, socket=None)
|
||||
client.status = ClientStatus.OFFLINE
|
||||
client.last_seen = float(
|
||||
self.known_clients[client_id].get("last_seen", 0.0)
|
||||
)
|
||||
self.clients[client_id] = client
|
||||
self.known_clients: dict[str, dict[str, str | float]] = {}
|
||||
self.known_clients = self._load_known_clients()
|
||||
|
||||
self.message_handlers: dict[
|
||||
tuple[Category, ActionType], Callable[[Client, Message], None]
|
||||
] = {}
|
||||
self._initialize_handlers()
|
||||
|
||||
self.pending_acks: list[tuple[Client, Message, float]] = []
|
||||
self.pending_hello: dict[Client, float] = {}
|
||||
|
||||
self.running: bool = False
|
||||
|
||||
def _initialize_handlers(self) -> None:
|
||||
"""Initialize message handlers."""
|
||||
|
||||
hello_handler = HelloHandler(self)
|
||||
initial_telemetry_handler = InitialTelemetryHandler(self)
|
||||
|
||||
self.message_handlers[(Category.CONTROL, ControlAction.HELLO)] = (
|
||||
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]]:
|
||||
"""Load the list of known clients from a YAML file and validate."""
|
||||
known_clients: dict[str, dict[str, str | float]] = {}
|
||||
|
||||
try:
|
||||
with open("config/known_clients.yaml", "r") as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("YAML root must be a dict")
|
||||
|
||||
known_clients = data.get("known_clients", {}) or {}
|
||||
|
||||
if not isinstance(known_clients, dict):
|
||||
raise ValueError("'known_clients' must be a dict")
|
||||
|
||||
for client_id, client_data in known_clients.items():
|
||||
if not isinstance(client_data, dict):
|
||||
raise ValueError(
|
||||
f"Client {client_id} data must be a dict"
|
||||
)
|
||||
last_seen = client_data.get("last_seen", 0.0)
|
||||
if not isinstance(last_seen, (float, int)):
|
||||
raise ValueError(
|
||||
f"Client {client_id} 'last_seen' must be a float or int"
|
||||
)
|
||||
|
||||
self.logger.debug(f"Loaded known clients: {known_clients}")
|
||||
self.logger.info(f"Loaded {len(known_clients)} known clients")
|
||||
|
||||
for client_id in known_clients:
|
||||
client = Client(id=client_id, addr=None, socket=None)
|
||||
client.status = ClientStatus.OFFLINE
|
||||
client.last_seen = float(
|
||||
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
|
||||
|
||||
except FileNotFoundError:
|
||||
self.logger.warning(
|
||||
"known_clients.yaml not found, creating empty known clients list"
|
||||
)
|
||||
self._save_known_clients()
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error loading known clients: {e}")
|
||||
raise
|
||||
|
||||
return known_clients
|
||||
|
||||
def _save_known_clients(self) -> None:
|
||||
"""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)
|
||||
self.logger.debug("Saved known clients")
|
||||
|
||||
@@ -99,6 +163,22 @@ class BackendServer:
|
||||
)
|
||||
time.sleep(1)
|
||||
|
||||
def send(self, client: Client, msg: Message) -> None:
|
||||
"""Send a message to a client.
|
||||
|
||||
Args:
|
||||
client (Client): The client to send the message to.
|
||||
msg (Message): The message to send.
|
||||
"""
|
||||
msg_bytes: bytes = msg.to_bytes()
|
||||
self.logger.info(
|
||||
f"[>] Sending message {msg.id} to {client}, category: {msg.category}, action: {msg.action}, ack_required: {msg.ack_required}"
|
||||
)
|
||||
self.logger.debug(f"[>] Message bytes: {msg_bytes!r}")
|
||||
if msg.ack_required:
|
||||
self.pending_acks.append((client, msg, time.time()))
|
||||
client.outbound += msg_bytes
|
||||
|
||||
def send_ack(self, client: Client, target_id: str) -> None:
|
||||
"""Send an ACK message to a client.
|
||||
|
||||
@@ -106,9 +186,9 @@ class BackendServer:
|
||||
client (Client): The client to send the ACK to.
|
||||
target_id (str): The id of the ACK'd message.
|
||||
"""
|
||||
ack: bytes = Message.ack(target_id=target_id).to_bytes()
|
||||
ack: Message = Message.Control.ack(target_id=target_id)
|
||||
self.logger.info(f"[>] Sending ACK to {client}")
|
||||
client.outbound += ack
|
||||
self.send(client, ack)
|
||||
|
||||
def send_close(self, client: Client) -> None:
|
||||
"""Send a CLOSE message to a client.
|
||||
@@ -116,9 +196,9 @@ class BackendServer:
|
||||
Args:
|
||||
client (Client): The client to send the CLOSE message to.
|
||||
"""
|
||||
close_msg: bytes = Message.close().to_bytes()
|
||||
close_msg: Message = Message.Control.close()
|
||||
self.logger.info(f"[>] Sending CLOSE to {client}")
|
||||
client.outbound += close_msg
|
||||
self.send(client, close_msg)
|
||||
|
||||
def _accept_connection(self, sock: socket.socket) -> None:
|
||||
"""Accept a new client connection.
|
||||
@@ -134,6 +214,8 @@ class BackendServer:
|
||||
events = selectors.EVENT_READ | selectors.EVENT_WRITE
|
||||
self.selector.register(conn, events, data=client)
|
||||
|
||||
self.pending_hello[client] = time.time()
|
||||
|
||||
self.logger.info(f"[+] Registered client {client}, HELLO pending...")
|
||||
|
||||
def _disconnect(self, client: Client) -> None:
|
||||
@@ -144,6 +226,12 @@ class BackendServer:
|
||||
"""
|
||||
self.logger.info(f"[-] Disconnecting {client}...")
|
||||
|
||||
if client.socket is None or client.socket._closed:
|
||||
self.logger.warning(
|
||||
f"Client {client} has no socket, nothing to disconnect."
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
self.selector.unregister(client.socket)
|
||||
except Exception as e:
|
||||
@@ -202,51 +290,6 @@ class BackendServer:
|
||||
try:
|
||||
if mask & selectors.EVENT_READ:
|
||||
self._receive_inbound(sock, client)
|
||||
if not client.inbound:
|
||||
self._disconnect(client)
|
||||
return
|
||||
|
||||
if client.id is None:
|
||||
# expect HELLO message
|
||||
try:
|
||||
msg = Message.from_bytes(client.inbound)
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f"Failed to parse HELLO message from {client}: {e}"
|
||||
)
|
||||
self._disconnect(client)
|
||||
return
|
||||
|
||||
if (
|
||||
msg.category == Category.CONTROL
|
||||
and msg.action == ControlAction.HELLO
|
||||
and msg.payload.get("id") is not None
|
||||
):
|
||||
client.id = msg.payload["id"]
|
||||
if (
|
||||
client.id in self.clients
|
||||
and self.clients[client.id].status == "connected"
|
||||
):
|
||||
old_client: Client = self.clients[client.id]
|
||||
self.logger.warning(
|
||||
f"Client {client.id} is already connected from {old_client.addr}, disconnecting old client..."
|
||||
)
|
||||
self.send_close(old_client)
|
||||
|
||||
self.clients[client.id] = client
|
||||
self.known_clients[client.id] = {
|
||||
"last_seen": client.last_seen
|
||||
}
|
||||
self._save_known_clients()
|
||||
client.status = ClientStatus.ONLINE
|
||||
|
||||
self.logger.info(f"[+] Registered new client {client}")
|
||||
else:
|
||||
self.logger.error(
|
||||
f"Expected HELLO message from {client}, got {msg}"
|
||||
)
|
||||
self._disconnect(client)
|
||||
return
|
||||
|
||||
while b"\n" in client.inbound:
|
||||
line, client.inbound = client.inbound.split(b"\n", 1)
|
||||
@@ -256,13 +299,40 @@ class BackendServer:
|
||||
try:
|
||||
msg = Message.from_bytes(line)
|
||||
self.logger.info(f"[.] Parsed message {msg.id}")
|
||||
|
||||
if client.id is None:
|
||||
self.logger.debug(
|
||||
f"Client {client} has no ID, expecting HELLO message..."
|
||||
)
|
||||
if (
|
||||
msg.category != Category.CONTROL
|
||||
or msg.action != ControlAction.HELLO
|
||||
):
|
||||
self.logger.warning(
|
||||
f"First message from {client} must be HELLO, disconnecting..."
|
||||
)
|
||||
self._disconnect(client)
|
||||
continue
|
||||
|
||||
handler: Callable[[Client, Message], None] | None = (
|
||||
self.message_handlers.get(
|
||||
(msg.category, msg.action), None
|
||||
)
|
||||
)
|
||||
if handler is not None:
|
||||
handler(client, msg)
|
||||
else:
|
||||
self.logger.warning(
|
||||
f"No handler for message {msg.id} with category {msg.category} and action {msg.action}"
|
||||
)
|
||||
continue
|
||||
|
||||
if msg.ack_required:
|
||||
self.send_ack(client, target_id=msg.id)
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f"Failed to parse message from {client}: {e}"
|
||||
)
|
||||
self._disconnect(client)
|
||||
return
|
||||
|
||||
if mask & selectors.EVENT_WRITE and client.outbound:
|
||||
@@ -278,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."""
|
||||
@@ -302,11 +370,33 @@ class BackendServer:
|
||||
and now - client.last_seen > 60 * 60 * 24 # 24 hours
|
||||
):
|
||||
self.clients[client.id].status = ClientStatus.STALE
|
||||
|
||||
# check pending ACKs
|
||||
for client, msg, timestamp in self.pending_acks[:]:
|
||||
if time.time() - timestamp > self.ACK_TIMEOUT:
|
||||
self.logger.warning(
|
||||
f"ACK timeout for message {msg.id} to {client}, resending..."
|
||||
)
|
||||
self.send(client, msg)
|
||||
self.pending_acks.remove((client, msg, timestamp))
|
||||
|
||||
# check pending HELLOs
|
||||
for client, timestamp in list(self.pending_hello.items()):
|
||||
if time.time() - timestamp > self.HELLO_TIMEOUT:
|
||||
self.logger.warning(
|
||||
f"HELLO timeout for {client}, disconnecting..."
|
||||
)
|
||||
self._disconnect(client)
|
||||
del self.pending_hello[client]
|
||||
|
||||
time.sleep(0.001) # prevent 100% CPU usage
|
||||
|
||||
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()
|
||||
@@ -322,4 +412,5 @@ class BackendServer:
|
||||
"addr": client.addr,
|
||||
"last_seen": client.last_seen,
|
||||
"status": client.status,
|
||||
"initial_telemetry": client.initial_telemetry,
|
||||
}
|
||||
@@ -5,24 +5,20 @@ from __future__ import annotations
|
||||
|
||||
import logging as lg
|
||||
import socket
|
||||
from enum import Enum
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
|
||||
class ClientStatus(str, Enum):
|
||||
"""Enumeration of client connection statuses."""
|
||||
|
||||
ONLINE = "online"
|
||||
PENDING = "pending"
|
||||
OFFLINE = "offline"
|
||||
STALE = "stale"
|
||||
from judas_server.client_status import ClientStatus
|
||||
|
||||
|
||||
class Client:
|
||||
"""Represents a client."""
|
||||
|
||||
def __init__(
|
||||
self, id: str | None, addr: tuple[str, int], socket: socket.socket
|
||||
self,
|
||||
id: str | None,
|
||||
addr: tuple[str, int] | None,
|
||||
socket: socket.socket | None,
|
||||
) -> None:
|
||||
"""Initialize the client.
|
||||
|
||||
@@ -41,13 +37,17 @@ class Client:
|
||||
self.last_seen: float = 0.0 # unix timestanp of last inbound message
|
||||
self.status: ClientStatus = ClientStatus.PENDING
|
||||
|
||||
self.socket: socket.socket = socket
|
||||
self.addr: tuple[str, int] = addr
|
||||
self.socket: socket.socket | None = socket
|
||||
self.addr: tuple[str, int] | None = addr
|
||||
self.inbound: bytes = b""
|
||||
self.outbound: bytes = b""
|
||||
|
||||
self.initial_telemetry: dict[str, Any] | None = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Client({self.id} ({self.addr[0]}:{self.addr[1]}))"
|
||||
if self.addr:
|
||||
return f"Client({self.id} ({self.addr[0]}:{self.addr[1]}))"
|
||||
return f"Client({self.id} (not connected))"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Client({self.id}, {self.addr})"
|
||||
@@ -55,6 +55,11 @@ class Client:
|
||||
def disconnect(self) -> None:
|
||||
"""Disconnect the client and close the socket."""
|
||||
self.logger.debug(f"Disconnecting Client {self}...")
|
||||
if self.socket is None:
|
||||
self.logger.warning(
|
||||
f"Client {self} not connected, nothing to disconnect."
|
||||
)
|
||||
return
|
||||
try:
|
||||
self.socket.close()
|
||||
except Exception as e:
|
||||
11
src/judas_server/client_status.py
Normal file
11
src/judas_server/client_status.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ClientStatus(str, Enum):
|
||||
"""Enumeration of client connection statuses."""
|
||||
|
||||
ONLINE = "online"
|
||||
PENDING = "pending"
|
||||
OFFLINE = "offline"
|
||||
STALE = "stale"
|
||||
5
src/judas_server/handler/__init__.py
Normal file
5
src/judas_server/handler/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .base_handler import BaseHandler
|
||||
from .hello_handler import HelloHandler
|
||||
from .ack_handler import AckHandler
|
||||
|
||||
__all__ = ["BaseHandler", "HelloHandler", "AckHandler"]
|
||||
28
src/judas_server/handler/ack_handler.py
Normal file
28
src/judas_server/handler/ack_handler.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .base_handler import BaseHandler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from judas_protocol import Message
|
||||
|
||||
from judas_server import BackendServer, Client
|
||||
|
||||
|
||||
class AckHandler(BaseHandler):
|
||||
def __init__(self, backend_server: BackendServer) -> None:
|
||||
super().__init__(backend_server)
|
||||
|
||||
def handle(self, client: Client, message: Message) -> None:
|
||||
pending_acks = self.backend_server.pending_acks
|
||||
if message.id in pending_acks:
|
||||
del pending_acks[message.id]
|
||||
self.logger.debug(
|
||||
f"[*] Received ACK for message {message.id} from {client}."
|
||||
)
|
||||
else:
|
||||
self.logger.warning(
|
||||
f"[!] Received ACK for unknown (or ACK'd) message {message.id} from {client}."
|
||||
)
|
||||
41
src/judas_server/handler/base_handler.py
Normal file
41
src/judas_server/handler/base_handler.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import logging as lg
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from judas_server.client import Client
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from judas_protocol import Message
|
||||
|
||||
from judas_server import BackendServer
|
||||
|
||||
|
||||
class BaseHandler:
|
||||
"""BaseHandler is the base class for all message handlers in the backend server.
|
||||
|
||||
It defines the interface for handling messages and provides common functionality for all handlers.
|
||||
"""
|
||||
|
||||
def __init__(self, backend_server: BackendServer) -> None:
|
||||
"""Initialize the BaseHandler with a reference to the backend server.
|
||||
|
||||
Args:
|
||||
backend_server (BackendServer): The backend server instance that this handler belongs to.
|
||||
"""
|
||||
self.logger: lg.Logger = lg.getLogger(
|
||||
f"{__name__}.{self.__class__.__name__}"
|
||||
)
|
||||
self.backend_server: BackendServer = backend_server
|
||||
|
||||
def handle(self, client: Client, message: Message) -> None:
|
||||
"""Handle a message from a client.
|
||||
|
||||
This method must be implemented by subclasses to define the specific handling logic for different message types.
|
||||
|
||||
Args:
|
||||
client (Client): The client that sent the message.
|
||||
message (Message): The message to be handled.
|
||||
"""
|
||||
raise NotImplementedError("handle() must be implemented by subclasses")
|
||||
66
src/judas_server/handler/hello_handler.py
Normal file
66
src/judas_server/handler/hello_handler.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
from judas_protocol import Category, ControlAction, Message
|
||||
|
||||
from judas_server.client import ClientStatus
|
||||
from judas_server.handler import BaseHandler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from judas_server.backend_server import BackendServer
|
||||
from judas_server.client import Client
|
||||
|
||||
|
||||
class HelloHandler(BaseHandler):
|
||||
def __init__(self, backend_server: BackendServer) -> None:
|
||||
super().__init__(backend_server)
|
||||
|
||||
@override
|
||||
def handle(self, client: Client, message: Message) -> None:
|
||||
if client.id is not None:
|
||||
return
|
||||
|
||||
if (
|
||||
message.category != Category.CONTROL
|
||||
or message.action != ControlAction.HELLO
|
||||
):
|
||||
self.logger.error(
|
||||
f"Expected HELLO message from {client}, got {message}, disconnecting client..."
|
||||
)
|
||||
self.backend_server._disconnect(client)
|
||||
return
|
||||
|
||||
if message.payload.get("id") is None:
|
||||
self.logger.error(
|
||||
f"HELLO message from {client} missing 'id' field, disconnecting client..."
|
||||
)
|
||||
self.backend_server._disconnect(client)
|
||||
return
|
||||
|
||||
client.id = message.payload["id"]
|
||||
|
||||
# check if client already connected, if so disconnect old client and register new one
|
||||
if (
|
||||
client.id in self.backend_server.clients
|
||||
and self.backend_server.clients[client.id].status
|
||||
== ClientStatus.ONLINE
|
||||
):
|
||||
old_client: Client = self.backend_server.clients[client.id]
|
||||
self.backend_server.logger.warning(
|
||||
f"Client {client.id} is already connected from {old_client.addr}, disconnecting old client..."
|
||||
)
|
||||
self.backend_server.send_close(old_client)
|
||||
return
|
||||
|
||||
self.backend_server.clients[client.id] = client # type: ignore
|
||||
self.backend_server.known_clients[client.id] = { # type: ignore
|
||||
"last_seen": client.last_seen
|
||||
}
|
||||
|
||||
del self.backend_server.pending_hello[client]
|
||||
self.backend_server._save_known_clients()
|
||||
client.status = ClientStatus.ONLINE
|
||||
|
||||
self.logger.info(f"[+] Registered new client {client}")
|
||||
0
src/judas_server/handler/telemetry/__init__.py
Normal file
0
src/judas_server/handler/telemetry/__init__.py
Normal file
33
src/judas_server/handler/telemetry/initial_handler.py
Normal file
33
src/judas_server/handler/telemetry/initial_handler.py
Normal file
@@ -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.handler.base_handler import BaseHandler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from judas_server import BackendServer
|
||||
from judas_server.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
|
||||
)
|
||||
@@ -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,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;
|
||||
}
|
||||
@@ -1,33 +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>
|
||||
</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>
|
||||
@@ -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>
|
||||
@@ -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,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>
|
||||
@@ -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,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.")
|
||||
6
uv.lock
generated
6
uv.lock
generated
@@ -358,12 +358,12 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "judas-protocol"
|
||||
version = "0.6.0"
|
||||
source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#d16c1914ba343aed300f1c5fae0201370c3274de" }
|
||||
version = "0.9.1"
|
||||
source = { git = "https://gitea.pufereq.pl/judas/judas_protocol.git#085c34f232f95313d66db48a7d17bc25c92a35ae" }
|
||||
|
||||
[[package]]
|
||||
name = "judas-server"
|
||||
version = "0.4.0"
|
||||
version = "0.7.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "flask" },
|
||||
|
||||
Reference in New Issue
Block a user